| #!/usr/bin/env python3 | |
| import math | |
| import os | |
| import sys | |
| from hb_fuzzing_tools import ( | |
| FLAG_BITS, | |
| HB_FUZZING_AXIS_PIN_TO_DEFAULT, | |
| HB_FUZZING_AXIS_SET_RANGE, | |
| HB_FUZZING_OP_SET_ADD_RANGES, | |
| HB_FUZZING_OP_TEXT_ADD, | |
| HB_FUZZING_OP_TEXT_DEL, | |
| HB_SUBSET_SETS_DROP_TABLE_TAG, | |
| HB_SUBSET_SETS_GLYPH_INDEX, | |
| HB_SUBSET_SETS_LAYOUT_FEATURE_TAG, | |
| HB_SUBSET_SETS_LAYOUT_SCRIPT_TAG, | |
| HB_SUBSET_SETS_NAME_ID, | |
| HB_SUBSET_SETS_NAME_LANG_ID, | |
| HB_SUBSET_SETS_UNICODE, | |
| add_set_option, | |
| emit_axis_records, | |
| emit_keep_everything, | |
| emit_pin_all_axes_to_default, | |
| emit_set_flags, | |
| emit_set_ranges, | |
| emit_text, | |
| fail, | |
| format_leading_comment, | |
| main_with_user_errors, | |
| parse_decimal_ranges, | |
| parse_hex_ranges, | |
| parse_output_arg, | |
| parse_tag_value, | |
| parse_tags, | |
| parse_text_codepoints, | |
| print_recorded_seed_path, | |
| read_range_file, | |
| read_text_file, | |
| write_seed_file, | |
| ) | |
| KEEP_EVERYTHING_FLAGS = ( | |
| 0x00000040 | | |
| 0x00000080 | | |
| 0x00000008 | | |
| 0x00000100 | | |
| 0x00000020 | |
| ) | |
| SET_OPTION_TYPES = { | |
| "--gids": HB_SUBSET_SETS_GLYPH_INDEX, | |
| "--name-IDs": HB_SUBSET_SETS_NAME_ID, | |
| "--name-languages": HB_SUBSET_SETS_NAME_LANG_ID, | |
| "--layout-features": HB_SUBSET_SETS_LAYOUT_FEATURE_TAG, | |
| "--layout-scripts": HB_SUBSET_SETS_LAYOUT_SCRIPT_TAG, | |
| "--drop-tables": HB_SUBSET_SETS_DROP_TABLE_TAG, | |
| "--unicodes": HB_SUBSET_SETS_UNICODE, | |
| } | |
| VALUE_OPTIONS_TO_IGNORE = { | |
| "-o", | |
| "--output-file", | |
| "--num-iterations", | |
| } | |
| IGNORED_OPTIONS = { | |
| "--batch", | |
| "--preprocess", | |
| } | |
| def usage(prog: str) -> str: | |
| return f"""Usage: {prog} [-o OUTPUT_OR_DIR] HB_SUBSET_COMMAND [HB_SUBSET_ARGS...] | |
| Generate an hb-subset-fuzzer seed by appending a versioned operation trailer to | |
| the input font bytes. Existing hb-subset-fuzzer corpus files remain valid; this | |
| script always emits the newer extended encoding. | |
| Supported hb-subset inputs: | |
| - font file via positional argument or --font-file=FILE | |
| - boolean subset flags | |
| - --keep-everything | |
| - --text / --text-file and additive/subtractive forms | |
| - --unicodes / --unicodes-file and additive/subtractive forms | |
| - --gids / --gids-file | |
| - --name-IDs | |
| - --name-languages | |
| - --layout-features | |
| - --layout-scripts | |
| - --drop-tables | |
| - --instance / --variations with fixed values, ranges, and drop/default pins | |
| Still unsupported: | |
| - --glyphs / --glyphs-file | |
| - --gid-map | |
| Options: | |
| -o OUTPUT_OR_DIR | |
| Write the seed to this file, or into this directory using | |
| <sha1(font)>.susbset-seed. | |
| Default: test/fuzzing/fonts/<sha1(font)>.susbset-seed | |
| -h, --help Show this help. | |
| """ | |
| def parse_axis_number(text: str) -> float: | |
| try: | |
| return float(text) | |
| except ValueError: | |
| fail(f"Failed parsing axis value '{text}'.") | |
| def parse_axis_range(value: str) -> tuple[int, float, float, float]: | |
| if value == "drop": | |
| return HB_FUZZING_AXIS_PIN_TO_DEFAULT, 0.0, 0.0, 0.0 | |
| parts = value.split(":") | |
| if len(parts) == 1: | |
| v = parse_axis_number(parts[0]) | |
| return HB_FUZZING_AXIS_SET_RANGE, v, v, v | |
| if len(parts) == 2: | |
| minimum = math.nan if parts[0] == "" else parse_axis_number(parts[0]) | |
| maximum = math.nan if parts[1] == "" else parse_axis_number(parts[1]) | |
| return HB_FUZZING_AXIS_SET_RANGE, minimum, math.nan, maximum | |
| if len(parts) == 3: | |
| minimum = math.nan if parts[0] == "" else parse_axis_number(parts[0]) | |
| middle = math.nan if parts[1] == "" else parse_axis_number(parts[1]) | |
| maximum = math.nan if parts[2] == "" else parse_axis_number(parts[2]) | |
| return HB_FUZZING_AXIS_SET_RANGE, minimum, middle, maximum | |
| fail(f"Failed parsing axis range '{value}'.") | |
| def parse_variations(value: str) -> tuple[bool, list[tuple[int, int, float, float, float]]]: | |
| pin_all = False | |
| records: list[tuple[int, int, float, float, float]] = [] | |
| for item in value.replace(" ", ",").split(","): | |
| if not item: | |
| continue | |
| if "=" not in item: | |
| fail(f"Unsupported variation item '{item}'. Expected tag=value.") | |
| tag_text, axis_value_text = item.split("=", 1) | |
| if tag_text == "*": | |
| if axis_value_text != "drop": | |
| fail("Only *=drop is supported for wildcard axis pinning.") | |
| pin_all = True | |
| continue | |
| tag = parse_tag_value(tag_text, "Axis tag") | |
| mode, minimum, middle, maximum = parse_axis_range(axis_value_text) | |
| records.append((tag, mode, minimum, middle, maximum)) | |
| return pin_all, records | |
| def parser_for_set_option(base_name: str): | |
| if base_name == "--unicodes": | |
| return parse_hex_ranges | |
| if base_name in ("--layout-features", "--layout-scripts", "--drop-tables"): | |
| return parse_tags | |
| return parse_decimal_ranges | |
| def handle_file_backed_set_option(ops: bytearray, name: str, path: str) -> None: | |
| if name == "--gids-file": | |
| emit_set_ranges(ops, HB_FUZZING_OP_SET_ADD_RANGES, HB_SUBSET_SETS_GLYPH_INDEX, read_range_file(path, parse_decimal_ranges)) | |
| return | |
| if name == "--unicodes-file": | |
| emit_set_ranges(ops, HB_FUZZING_OP_SET_ADD_RANGES, HB_SUBSET_SETS_UNICODE, read_range_file(path, parse_hex_ranges)) | |
| return | |
| fail(f"Unsupported file-backed option '{name}'.") | |
| def record_subset_main(argv: list[str]) -> int: | |
| usage_text = usage(argv[0]) | |
| out, args = parse_output_arg(argv, usage_text) | |
| if len(args) < 2: | |
| print(usage_text, end="", file=sys.stderr) | |
| return 1 | |
| hb_subset = args[0] | |
| if "hb-subset" not in os.path.basename(hb_subset): | |
| fail(f"First argument does not look like hb-subset: '{hb_subset}'.") | |
| font_file = None | |
| flags = 0 | |
| ops = bytearray() | |
| positionals: list[str] = [] | |
| i = 1 | |
| while i < len(args): | |
| arg = args[i] | |
| if arg in FLAG_BITS: | |
| flags |= FLAG_BITS[arg] | |
| emit_set_flags(ops, flags) | |
| i += 1 | |
| continue | |
| if arg == "--keep-everything": | |
| emit_keep_everything(ops) | |
| flags = KEEP_EVERYTHING_FLAGS | |
| i += 1 | |
| continue | |
| if arg in IGNORED_OPTIONS: | |
| i += 1 | |
| continue | |
| ignored_value = False | |
| for prefix in VALUE_OPTIONS_TO_IGNORE: | |
| if arg == prefix: | |
| if i + 1 >= len(args): | |
| fail(f"Missing value for '{arg}'.") | |
| i += 2 | |
| ignored_value = True | |
| break | |
| if arg.startswith(prefix + "="): | |
| i += 1 | |
| ignored_value = True | |
| break | |
| if ignored_value: | |
| continue | |
| if arg == "--font-file": | |
| if i + 1 >= len(args): | |
| fail("Missing value for '--font-file'.") | |
| font_file = args[i + 1] | |
| i += 2 | |
| continue | |
| if arg.startswith("--font-file="): | |
| font_file = arg.split("=", 1)[1] | |
| i += 1 | |
| continue | |
| if arg in ("--text", "--text+", "--text-"): | |
| if i + 1 >= len(args): | |
| fail(f"Missing value for '{arg}'.") | |
| emit_text(ops, HB_FUZZING_OP_TEXT_DEL if arg.endswith("-") else HB_FUZZING_OP_TEXT_ADD, parse_text_codepoints(args[i + 1])) | |
| i += 2 | |
| continue | |
| if arg.startswith("--text=") or arg.startswith("--text+=") or arg.startswith("--text-="): | |
| name, value = arg.split("=", 1) | |
| emit_text(ops, HB_FUZZING_OP_TEXT_DEL if name.endswith("-") else HB_FUZZING_OP_TEXT_ADD, parse_text_codepoints(value)) | |
| i += 1 | |
| continue | |
| if arg == "--text-file": | |
| if i + 1 >= len(args): | |
| fail("Missing value for '--text-file'.") | |
| emit_text(ops, HB_FUZZING_OP_TEXT_ADD, read_text_file(args[i + 1])) | |
| i += 2 | |
| continue | |
| if arg.startswith("--text-file="): | |
| emit_text(ops, HB_FUZZING_OP_TEXT_ADD, read_text_file(arg.split("=", 1)[1])) | |
| i += 1 | |
| continue | |
| handled_set_option = False | |
| for base_name, set_type in SET_OPTION_TYPES.items(): | |
| for modifier in ("", "+", "-"): | |
| full_name = base_name + modifier | |
| if arg == full_name: | |
| if i + 1 >= len(args): | |
| fail(f"Missing value for '{arg}'.") | |
| add_set_option(ops, set_type, modifier, args[i + 1], parser_for_set_option(base_name)) | |
| i += 2 | |
| handled_set_option = True | |
| break | |
| if arg.startswith(full_name + "="): | |
| add_set_option(ops, set_type, modifier, arg.split("=", 1)[1], parser_for_set_option(base_name)) | |
| i += 1 | |
| handled_set_option = True | |
| break | |
| if handled_set_option: | |
| break | |
| if handled_set_option: | |
| continue | |
| if arg == "--gids-file": | |
| if i + 1 >= len(args): | |
| fail("Missing value for '--gids-file'.") | |
| handle_file_backed_set_option(ops, arg, args[i + 1]) | |
| i += 2 | |
| continue | |
| if arg == "--unicodes-file": | |
| if i + 1 >= len(args): | |
| fail("Missing value for '--unicodes-file'.") | |
| handle_file_backed_set_option(ops, arg, args[i + 1]) | |
| i += 2 | |
| continue | |
| if arg.startswith("--gids-file=") or arg.startswith("--unicodes-file="): | |
| name, value = arg.split("=", 1) | |
| handle_file_backed_set_option(ops, name, value) | |
| i += 1 | |
| continue | |
| if arg in ("--instance", "--variations"): | |
| if i + 1 >= len(args): | |
| fail(f"Missing value for '{arg}'.") | |
| pin_all, records = parse_variations(args[i + 1]) | |
| if pin_all: | |
| emit_pin_all_axes_to_default(ops) | |
| if records: | |
| emit_axis_records(ops, records) | |
| i += 2 | |
| continue | |
| if arg.startswith("--instance=") or arg.startswith("--variations="): | |
| pin_all, records = parse_variations(arg.split("=", 1)[1]) | |
| if pin_all: | |
| emit_pin_all_axes_to_default(ops) | |
| if records: | |
| emit_axis_records(ops, records) | |
| i += 1 | |
| continue | |
| if arg.startswith("--glyphs") or arg.startswith("--gid-map"): | |
| fail(f"Unsupported hb-subset option '{arg}'.") | |
| if arg.startswith("-"): | |
| fail(f"Unsupported option '{arg}'.") | |
| positionals.append(arg) | |
| i += 1 | |
| if font_file is None: | |
| if not positionals: | |
| fail("No font file found in hb-subset invocation.") | |
| font_file = positionals[0] | |
| positionals = positionals[1:] | |
| for text_arg in positionals: | |
| emit_text(ops, HB_FUZZING_OP_TEXT_ADD, parse_text_codepoints(text_arg)) | |
| print_recorded_seed_path(write_seed_file(font_file, out, ops, leading_comment=format_leading_comment(args))) | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main_with_user_errors(record_subset_main, sys.argv)) |