#!/bin/sh set -eu : "${PRE15_DUT:?PRE15_DUT is required}" : "${PRE15_ROOT:?PRE15_ROOT is required}" : "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}" : "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}" . "$PRE15_LIB_DIR/runner.sh" baseline=1b4b8abc249d9bbbb75e19b5259c5f46f91909ef artifacts=$PRE15_RUN_DIR/artifacts for tool in git python3 sha256sum; do command -v "$tool" >/dev/null 2>&1 || \ pre15_infra_blocked "missing B30 host tool: $tool" done for source in internal.h compress.h decompressor.c decompressor_lz4.c \ decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c zmap.c; do pre15_record_fixture "b30-$source" "$PRE15_DUT/src/$source" done pre15_record_fixture b30-linux-compress "$PRE15_ROOT/src-linux/compress.h" pre15_record_fixture b30-linux-decompressor "$PRE15_ROOT/src-linux/decompressor.c" pre15_record_fixture b30-case \ "$PRE15_DUT/tests/pre15/cases/B30-codec-structure.sh" mkdir -p "$artifacts" pre15_target_reached if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \ "$artifacts/B30-codec-structure.json" <<'PY' from __future__ import annotations from collections import Counter import json from pathlib import Path import re import subprocess import sys root = Path(sys.argv[1]) dut = Path(sys.argv[2]) baseline = sys.argv[3] report_path = Path(sys.argv[4]) src = dut / "src" def committed(path: str) -> str: completed = subprocess.run( ["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"], check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) if completed.returncode != 0: raise SystemExit(f"cannot read B30 baseline {path}: {completed.stderr}") return completed.stdout def replace_once(source: str, old: str, new: str, label: str) -> str: count = source.count(old) if count != 1: raise SystemExit(f"B30 baseline anchor count for {label}: {count}") return source.replace(old, new, 1) expected_changed = { "repo-pre-15/src/compress.h", "repo-pre-15/src/decompressor.c", "repo-pre-15/src/decompressor_deflate.c", "repo-pre-15/src/decompressor_lzma.c", "repo-pre-15/src/decompressor_zstd.c", "repo-pre-15/src/internal.h", "repo-pre-15/src/zdata.c", "repo-pre-15/tests/pre15/cases/B30-codec-structure.sh", } changed = subprocess.run( ["git", "-C", str(root), "diff", "--name-only", baseline, "--", "repo-pre-15"], check=True, text=True, stdout=subprocess.PIPE, ).stdout.splitlines() changed.extend( subprocess.run( [ "git", "-C", str(root), "ls-files", "--others", "--exclude-standard", "--", "repo-pre-15", ], check=True, text=True, stdout=subprocess.PIPE, ).stdout.splitlines() ) if set(changed) != expected_changed or len(changed) != len(expected_changed): raise SystemExit(f"B30 repo-pre-15 write set mismatch: {changed!r}") current = { path: (src / path).read_text(encoding="utf-8") for path in ( "internal.h", "compress.h", "decompressor.c", "decompressor_lz4.c", "decompressor_lzma.c", "decompressor_deflate.c", "decompressor_zstd.c", "zdata.c", "zmap.c", ) } before = { path: committed(f"src/{path}") for path in current } expected = before["internal.h"] expected = replace_once( expected, "struct erofs_sb_lz4_info {\n" "\tuint16_t max_distance_pages;\n" "\tuint16_t max_pclusterblks;\n" "};\n\n", "", "LZ4 state type", ) expected = replace_once( expected, "\tstruct erofs_sb_lz4_info lz4;\n", "", "LZ4 state member", ) if current["internal.h"] != expected: raise SystemExit("internal.h differs from exact B30 dead-state removal") expected = before["compress.h"] expected = replace_once( expected, "struct z_erofs_decompressor {\n" "\tconst char *name;\n" "\tbool supports_subextent;\n" "\t/* Callbacks return zero or a positive FreeBSD errno. */\n" "\tint (*config)(struct erofs_sb_info *, const struct erofs_super_block *,\n" "\t const void *, size_t);\n" "\tint (*decompress)(const struct z_erofs_decompress_req *);\n" "};", "struct z_erofs_decompressor {\n" "\t/* Callbacks return zero or a positive FreeBSD errno. */\n" "\tint (*config)(struct erofs_sb_info *, const struct erofs_super_block *,\n" "\t const void *, size_t);\n" "\tint (*decompress)(const struct z_erofs_decompress_req *);\n" "\tbool supports_subextent;\n" "\tconst char *name;\n" "};", "descriptor fields", ) if current["compress.h"] != expected: raise SystemExit("compress.h differs from exact B30 descriptor alignment") expected = before["decompressor.c"] for old, new, label in ( ("\t\tdistance = le16toh(lz4->max_distance);\n", "", "unused cfg distance"), ("\t\tmax_pclusterblks = 1;\n\t\tsbi->available_compr_algs", "\t\tsbi->available_compr_algs", "legacy dead local store"), ( "\tsbi->lz4.max_pclusterblks = max_pclusterblks;\n" "\tsbi->lz4.max_distance_pages = distance != 0 ?\n" "\t howmany(distance, PAGE_SIZE) + 1 :\n" "\t howmany(UINT16_MAX, PAGE_SIZE) + 1;\n", "", "runtime LZ4 stores", ), ( "static const struct z_erofs_decompressor z_erofs_shifted_decomp = {\n" "\t.name = \"shifted\",\n" "\t.decompress = z_erofs_transform_plain,\n" "};", "static const struct z_erofs_decompressor z_erofs_shifted_decomp = {\n" "\t.decompress = z_erofs_transform_plain,\n" "\t.name = \"shifted\",\n" "};", "shifted descriptor", ), ( "static const struct z_erofs_decompressor z_erofs_interlaced_decomp = {\n" "\t.name = \"interlaced\",\n" "\t.decompress = z_erofs_transform_plain,\n" "};", "static const struct z_erofs_decompressor z_erofs_interlaced_decomp = {\n" "\t.decompress = z_erofs_transform_plain,\n" "\t.name = \"interlaced\",\n" "};", "interlaced descriptor", ), ( "static const struct z_erofs_decompressor z_erofs_lz4_decomp = {\n" "\t.name = \"lz4\",\n" "\t.supports_subextent = 1,\n" "\t.config = z_erofs_load_lz4_config,\n" "\t.decompress = z_erofs_lz4_decompress,\n" "};", "static const struct z_erofs_decompressor z_erofs_lz4_decomp = {\n" "\t.config = z_erofs_load_lz4_config,\n" "\t.decompress = z_erofs_lz4_decompress,\n" "\t.supports_subextent = 1,\n" "\t.name = \"lz4\",\n" "};", "LZ4 descriptor", ), ( "static const struct z_erofs_decompressor * const z_erofs_decomp[] = {\n" "\t[Z_EROFS_COMPRESSION_LZ4] = &z_erofs_lz4_decomp,\n" "\t[Z_EROFS_COMPRESSION_LZMA] = &z_erofs_lzma_decomp,\n" "\t[Z_EROFS_COMPRESSION_DEFLATE] = &z_erofs_deflate_decomp,\n" "\t[Z_EROFS_COMPRESSION_ZSTD] = &z_erofs_zstd_decomp,\n" "\t[Z_EROFS_COMPRESSION_SHIFTED] = &z_erofs_shifted_decomp,\n" "\t[Z_EROFS_COMPRESSION_INTERLACED] = &z_erofs_interlaced_decomp,\n" "};", "static const struct z_erofs_decompressor * const z_erofs_decomp[] = {\n" "\t[Z_EROFS_COMPRESSION_SHIFTED] = &z_erofs_shifted_decomp,\n" "\t[Z_EROFS_COMPRESSION_INTERLACED] = &z_erofs_interlaced_decomp,\n" "\t[Z_EROFS_COMPRESSION_LZ4] = &z_erofs_lz4_decomp,\n" "\t[Z_EROFS_COMPRESSION_LZMA] = &z_erofs_lzma_decomp,\n" "\t[Z_EROFS_COMPRESSION_DEFLATE] = &z_erofs_deflate_decomp,\n" "\t[Z_EROFS_COMPRESSION_ZSTD] = &z_erofs_zstd_decomp,\n" "};", "decompressor table", ), ): expected = replace_once(expected, old, new, label) if current["decompressor.c"] != expected: raise SystemExit("decompressor.c differs from exact B30 structural edits") for path, name, capability in ( ("decompressor_lzma.c", "lzma", "1"), ("decompressor_deflate.c", "deflate", "1"), ("decompressor_zstd.c", "zstd", "0"), ): expected = before[path] expected = replace_once( expected, f"const struct z_erofs_decompressor z_erofs_{name}_decomp = {{\n" f"\t.name = \"{name}\",\n" f"\t.supports_subextent = {capability},\n" f"\t.config = z_erofs_load_{name}_config,\n" f"\t.decompress = z_erofs_{name}_decompress,\n" "};", f"const struct z_erofs_decompressor z_erofs_{name}_decomp = {{\n" f"\t.config = z_erofs_load_{name}_config,\n" f"\t.decompress = z_erofs_{name}_decompress,\n" f"\t.supports_subextent = {capability},\n" f"\t.name = \"{name}\",\n" "};", f"{name} descriptor", ) if current[path] != expected: raise SystemExit(f"{path} differs from exact B30 descriptor reorder") if current["decompressor_lz4.c"] != before["decompressor_lz4.c"]: raise SystemExit("B30 changed the independent FreeBSD LZ4 backend") expected = before["zdata.c"] if expected.count("z_erofs_read_extent") != 2: raise SystemExit("B30 zdata baseline rename denominator changed") expected = expected.replace("z_erofs_read_extent", "z_erofs_decode_extent") if current["zdata.c"] != expected: raise SystemExit("zdata.c differs from the exact two-token decode rename") if current["zmap.c"] != before["zmap.c"]: raise SystemExit("B30 changed the zmap record reader") descriptor_markers = [ "int (*config)(", "int (*decompress)(", "bool supports_subextent;", "const char *name;", ] positions = [current["compress.h"].index(marker) for marker in descriptor_markers] if positions != sorted(positions): raise SystemExit("FreeBSD descriptor field order drifted") def initializer_fields(source: str, object_name: str) -> list[tuple[str, str]]: match = re.search( rf"(?:static )?const struct z_erofs_decompressor {object_name} = \{{\n" rf"(?P.*?)\n\}};", source, re.DOTALL, ) if match is None: raise SystemExit(f"missing descriptor initializer: {object_name}") return re.findall(r"^\s*\.([a-z_]+)\s*=\s*([^,]+),$", match.group("body"), re.MULTILINE) descriptors = { "shifted": initializer_fields(current["decompressor.c"], "z_erofs_shifted_decomp"), "interlaced": initializer_fields(current["decompressor.c"], "z_erofs_interlaced_decomp"), "lz4": initializer_fields(current["decompressor.c"], "z_erofs_lz4_decomp"), "lzma": initializer_fields(current["decompressor_lzma.c"], "z_erofs_lzma_decomp"), "deflate": initializer_fields(current["decompressor_deflate.c"], "z_erofs_deflate_decomp"), "zstd": initializer_fields(current["decompressor_zstd.c"], "z_erofs_zstd_decomp"), } expected_descriptors = { "shifted": [("decompress", "z_erofs_transform_plain"), ("name", '"shifted"')], "interlaced": [("decompress", "z_erofs_transform_plain"), ("name", '"interlaced"')], "lz4": [ ("config", "z_erofs_load_lz4_config"), ("decompress", "z_erofs_lz4_decompress"), ("supports_subextent", "1"), ("name", '"lz4"'), ], "lzma": [ ("config", "z_erofs_load_lzma_config"), ("decompress", "z_erofs_lzma_decompress"), ("supports_subextent", "1"), ("name", '"lzma"'), ], "deflate": [ ("config", "z_erofs_load_deflate_config"), ("decompress", "z_erofs_deflate_decompress"), ("supports_subextent", "1"), ("name", '"deflate"'), ], "zstd": [ ("config", "z_erofs_load_zstd_config"), ("decompress", "z_erofs_zstd_decompress"), ("supports_subextent", "0"), ("name", '"zstd"'), ], } if descriptors != expected_descriptors: raise SystemExit(f"backend descriptor initializer mismatch: {descriptors!r}") table_match = re.search( r"static const struct z_erofs_decompressor \* const z_erofs_decomp\[\] = \{\n" r"(?P.*?)\n\};", current["decompressor.c"], re.DOTALL, ) if table_match is None: raise SystemExit("missing FreeBSD decompressor table") table_order = re.findall(r"\[(Z_EROFS_COMPRESSION_[A-Z0-9_]+)\]", table_match.group("body")) expected_order = [ "Z_EROFS_COMPRESSION_SHIFTED", "Z_EROFS_COMPRESSION_INTERLACED", "Z_EROFS_COMPRESSION_LZ4", "Z_EROFS_COMPRESSION_LZMA", "Z_EROFS_COMPRESSION_DEFLATE", "Z_EROFS_COMPRESSION_ZSTD", ] if table_order != expected_order: raise SystemExit(f"FreeBSD decompressor table order mismatch: {table_order!r}") linux_decompressor = (root / "src-linux/decompressor.c").read_text(encoding="utf-8") linux_table = linux_decompressor[linux_decompressor.index("z_erofs_decomp[] = {") :] linux_order = re.findall(r"\[(Z_EROFS_COMPRESSION_[A-Z0-9_]+)\]", linux_table)[:6] if linux_order != expected_order: raise SystemExit(f"Linux decompressor table anchor drifted: {linux_order!r}") callbacks = [] for path in sorted(src.glob("decompressor*.c")): for line in path.read_text(encoding="utf-8").splitlines(): match = re.match(r"\s*\.(config|decompress)\s*=\s*([^,]+),", line) if match: callbacks.append((path.name, match.group(1), match.group(2))) expected_callbacks = [ ("decompressor.c", "decompress", "z_erofs_transform_plain"), ("decompressor.c", "decompress", "z_erofs_transform_plain"), ("decompressor.c", "config", "z_erofs_load_lz4_config"), ("decompressor.c", "decompress", "z_erofs_lz4_decompress"), ("decompressor_deflate.c", "config", "z_erofs_load_deflate_config"), ("decompressor_deflate.c", "decompress", "z_erofs_deflate_decompress"), ("decompressor_lzma.c", "config", "z_erofs_load_lzma_config"), ("decompressor_lzma.c", "decompress", "z_erofs_lzma_decompress"), ("decompressor_zstd.c", "config", "z_erofs_load_zstd_config"), ("decompressor_zstd.c", "decompress", "z_erofs_zstd_decompress"), ] if Counter(callbacks) != Counter(expected_callbacks) or len(callbacks) != 10: raise SystemExit(f"G01 decompressor callback multiset changed: {callbacks!r}") all_source = "\n".join(current.values()) for removed in ("erofs_sb_lz4_info", "max_distance_pages", "sbi->lz4"): if removed in all_source: raise SystemExit(f"removed LZ4 runtime state remains: {removed}") config = current["decompressor.c"][: current["decompressor.c"].index("static int\nz_erofs_transform_plain")] for marker in ( "uint32_t max_pclusterblks;", "max_pclusterblks = le16toh(lz4->max_pclusterblks);", "if (max_pclusterblks == 0)", "max_pclusterblks = 1;", "max_pclusterblks >\n\t\t (Z_EROFS_PCLUSTER_MAX_SIZE >> sbi->blkszbits)", "return (EOPNOTSUPP);", "distance = le16toh(dsb->u1.lz4_max_distance);", "if (distance == 0 && !erofs_sb_has_lz4_0padding(sbi))", ): if marker not in config: raise SystemExit(f"LZ4 config validation marker missing: {marker!r}") if current["zdata.c"].count("z_erofs_decode_extent") != 2: raise SystemExit("zdata decode helper rename denominator changed") if "z_erofs_read_extent" in current["zdata.c"]: raise SystemExit("old zdata decode helper name remains") if current["zmap.c"].count("z_erofs_read_extent") != 4: raise SystemExit("zmap record reader definition/calls changed") if "z_erofs_decode_extent" in current["zmap.c"]: raise SystemExit("zmap record reader was incorrectly renamed") if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", all_source): raise SystemExit("B30 introduced Linux negative errno") for path, load_name, decode_name, descriptor_name in ( ("decompressor_lzma.c", "z_erofs_load_lzma_config", "z_erofs_lzma_decompress", "z_erofs_lzma_decomp"), ("decompressor_deflate.c", "z_erofs_load_deflate_config", "z_erofs_deflate_decompress", "z_erofs_deflate_decomp"), ("decompressor_zstd.c", "z_erofs_load_zstd_config", "z_erofs_zstd_decompress", "z_erofs_zstd_decomp"), ): positions = [current[path].index(name) for name in (load_name, decode_name, descriptor_name)] if positions != sorted(positions): raise SystemExit(f"backend definition order drifted: {path}") report = { "status": "PASS", "baseline": baseline, "changed_paths": sorted(changed), "descriptor_fields": ["config", "decompress", "supports_subextent", "name"], "descriptor_table_order": table_order, "callback_field_count": len(callbacks), "callback_target_multiset": sorted(callbacks), "removed_runtime_state": ["erofs_sb_lz4_info", "max_distance_pages", "max_pclusterblks store"], "retained_config_validation": ["max_pclusterblks local", "format cap", "legacy distance branch"], "rename": {"zdata_decode": 2, "zmap_record_reader": 4}, "preserved": ["B25 typed errno", "B27 trailing policy", "B28 partial fallback ownership"], } with report_path.open("x", encoding="ascii") as stream: json.dump(report, stream, ensure_ascii=True, indent=2, sort_keys=True) stream.write("\n") print("B30-descriptors PASS fields=4 backends=6 callbacks=10") print("B30-write-only PASS dead-state=2 config-local=1 rename=2/4") PY then : else pre15_dut_fail 'B30 descriptor, dead-state, or rename oracle failed' fi sha256sum "$artifacts/B30-codec-structure.json" > "$artifacts/SHA256SUMS" printf 'B30 PASS codec structure aligned without behavior changes\n'