233 lines
7.1 KiB
Python
Executable File
233 lines
7.1 KiB
Python
Executable File
#!/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("<I", data[-12:-8])[0]
|
|
prefix_len = len(data) - len(HB_FUZZING_EXTENDED_MAGIC) - 4 - ops_len
|
|
if prefix_len < 0:
|
|
fail("Malformed extended trailer: negative prefix length.")
|
|
return data[:prefix_len], data[prefix_len:-12]
|
|
|
|
|
|
def read_u8(ops: bytes, offset: int) -> 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("<I", ops[offset : offset + 4])[0], offset + 4
|
|
|
|
|
|
def read_f32(ops: bytes, offset: int) -> tuple[float, int]:
|
|
if offset + 4 > len(ops):
|
|
fail("Malformed ops payload: truncated f32.")
|
|
return struct.unpack("<f", ops[offset : offset + 4])[0], offset + 4
|
|
|
|
|
|
def decode_ops(ops: bytes) -> 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: <empty>")
|
|
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))
|