#!/usr/bin/env python3 import math import os import struct import sys from hb_fuzzing_tools import ( HB_FUZZING_AXIS_PIN_TO_DEFAULT, HB_FUZZING_AXIS_SET_RANGE, HB_FUZZING_EXTENDED_MAGIC, HB_FUZZING_OP_AXIS_PIN_ALL_TO_DEFAULT, HB_FUZZING_OP_AXIS_SET, HB_FUZZING_OP_KEEP_EVERYTHING, HB_FUZZING_OP_SET_ADD_RANGES, HB_FUZZING_OP_SET_CLEAR, HB_FUZZING_OP_SET_DEL_RANGES, HB_FUZZING_OP_SET_FLAGS, HB_FUZZING_OP_SET_INVERT, HB_FUZZING_OP_TEXT_ADD, HB_FUZZING_OP_TEXT_DEL, UserError, fail, main_with_user_errors, parse_output_arg, ) SET_TYPE_NAMES = { 0: "glyph-index", 1: "unicode", 3: "drop-table", 4: "name-id", 5: "name-lang-id", 6: "layout-feature", 7: "layout-script", } def usage(prog: str) -> str: return f"""Usage: {prog} SEED_FILE... Decode a shared HarfBuzz fuzzing seed in plain text. Supports the extended HBSUBFZ2 trailer format used by the shape, raster, vector, and subset fuzzers. """ def format_tag(tag: int) -> str: raw = tag.to_bytes(4, byteorder="big") try: text = raw.decode("ascii") if all(32 <= b <= 126 for b in raw): return f"{text!r}" except UnicodeDecodeError: pass return "0x" + raw.hex() def format_float(value: float) -> str: if math.isnan(value): return "nan" if math.isinf(value): return "inf" if value > 0 else "-inf" return f"{value:g}" def format_codepoints(codepoints: list[int]) -> str: text = "".join(chr(cp) for cp in codepoints) cps = " ".join(f"U+{cp:04X}" for cp in codepoints) return f"text={text!r} cps=[{cps}]" def split_leading_comment(data: bytes) -> tuple[str | None, bytes]: if not data.startswith(b"$"): return None, data newline = data.find(b"\n") if newline < 0: return None, data return data[:newline].decode("utf-8", errors="replace"), data[newline + 1 :] def split_extended_payload(data: bytes) -> tuple[bytes, bytes | None]: if len(data) < len(HB_FUZZING_EXTENDED_MAGIC) + 4: return data, None if not data.endswith(HB_FUZZING_EXTENDED_MAGIC): return data, None ops_len = struct.unpack(" tuple[int, int]: if offset + 1 > len(ops): fail("Malformed ops payload: truncated u8.") return ops[offset], offset + 1 def read_u32(ops: bytes, offset: int) -> tuple[int, int]: if offset + 4 > len(ops): fail("Malformed ops payload: truncated u32.") return struct.unpack(" tuple[float, int]: if offset + 4 > len(ops): fail("Malformed ops payload: truncated f32.") return struct.unpack(" list[str]: out: list[str] = [] offset = 0 while offset < len(ops): op, offset = read_u8(ops, offset) if op in (HB_FUZZING_OP_TEXT_ADD, HB_FUZZING_OP_TEXT_DEL): count, offset = read_u32(ops, offset) cps: list[int] = [] for _ in range(count): cp, offset = read_u32(ops, offset) cps.append(cp) name = "TEXT_ADD" if op == HB_FUZZING_OP_TEXT_ADD else "TEXT_DEL" out.append(f"{name} {format_codepoints(cps)}") continue if op == HB_FUZZING_OP_AXIS_PIN_ALL_TO_DEFAULT: out.append("AXIS_PIN_ALL_TO_DEFAULT") continue if op == HB_FUZZING_OP_AXIS_SET: count, offset = read_u32(ops, offset) records: list[str] = [] for _ in range(count): tag, offset = read_u32(ops, offset) mode, offset = read_u8(ops, offset) minimum, offset = read_f32(ops, offset) middle, offset = read_f32(ops, offset) maximum, offset = read_f32(ops, offset) if mode == HB_FUZZING_AXIS_PIN_TO_DEFAULT: mode_text = "pin-to-default" elif mode == HB_FUZZING_AXIS_SET_RANGE: mode_text = ( f"set-range min={format_float(minimum)} " f"mid={format_float(middle)} max={format_float(maximum)}" ) else: mode_text = f"unknown-mode({mode})" records.append(f"tag={format_tag(tag)} {mode_text}") out.append("AXIS_SET " + "; ".join(records)) continue if op == HB_FUZZING_OP_SET_FLAGS: flags, offset = read_u32(ops, offset) out.append(f"SET_FLAGS 0x{flags:08X}") continue if op == HB_FUZZING_OP_KEEP_EVERYTHING: out.append("KEEP_EVERYTHING") continue if op in (HB_FUZZING_OP_SET_CLEAR, HB_FUZZING_OP_SET_INVERT): set_type, offset = read_u8(ops, offset) name = "SET_CLEAR" if op == HB_FUZZING_OP_SET_CLEAR else "SET_INVERT" out.append(f"{name} {SET_TYPE_NAMES.get(set_type, f'unknown({set_type})')}") continue if op in (HB_FUZZING_OP_SET_ADD_RANGES, HB_FUZZING_OP_SET_DEL_RANGES): set_type, offset = read_u8(ops, offset) count, offset = read_u32(ops, offset) ranges: list[str] = [] for _ in range(count): start, offset = read_u32(ops, offset) end, offset = read_u32(ops, offset) if start == end: ranges.append(str(start)) else: ranges.append(f"{start}-{end}") name = "SET_ADD_RANGES" if op == HB_FUZZING_OP_SET_ADD_RANGES else "SET_DEL_RANGES" out.append(f"{name} {SET_TYPE_NAMES.get(set_type, f'unknown({set_type})')} [{', '.join(ranges)}]") continue fail(f"Unknown opcode {op} at byte offset {offset - 1}.") return out def decode_file(path: str) -> None: with open(path, "rb") as fp: data = fp.read() comment, payload = split_leading_comment(data) prefix, ops = split_extended_payload(payload) print(f"file: {path}") if comment is not None: print(f"leading comment: {comment}") print(f"extended: {'yes' if ops is not None else 'no'}") print(f"prefix bytes: {len(prefix)}") if ops is not None: print(f"ops bytes: {len(ops)}") decoded = decode_ops(ops) if decoded: print("ops:") for line in decoded: print(f" {line}") else: print("ops: ") print() def decode_seed_main(argv: list[str]) -> int: usage_text = usage(argv[0]) _, args = parse_output_arg(argv, usage_text) if not args: print(usage_text, end="", file=sys.stderr) return 1 for path in args: if not os.path.isfile(path): fail(f"Seed file not found: '{path}'.") decode_file(path) return 0 if __name__ == "__main__": raise SystemExit(main_with_user_errors(decode_seed_main, sys.argv))