#!/usr/bin/env python3 from __future__ import annotations import argparse import hashlib import json import os from pathlib import Path import shutil import subprocess import tempfile def sha256_bytes(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def sha256_path(path: Path) -> str: return sha256_bytes(path.read_bytes()) def load_spec(path: Path) -> dict[str, object]: spec = json.loads(path.read_text(encoding="ascii")) if ( spec.get("schema") != 1 or spec.get("batch") != "B28" or spec.get("candidate") != "P15-086" or spec.get("test") != "TC176-stream-runtime" ): raise SystemExit("invalid B28 partial fixture spec") if set(spec.get("codecs", {})) != {"lz4", "lzma", "deflate", "zstd"}: raise SystemExit("B28 codec set changed") return spec def make_sources(root: Path, spec: dict[str, object]) -> dict[str, bytes]: sources: dict[str, bytes] = {} lz4_spec = spec["sources"]["lz4"] sources["lz4"] = b"".join( bytes([segment["byte"]]) * segment["length"] for segment in lz4_spec["segments"] ) stream_spec = spec["sources"]["stream"] sources["stream"] = b"".join( f"P15-086-{index % 64:02d}:alpha-beta-gamma-delta:{(index * 17) % 256:02x}\n".encode( "ascii" ) for index in range(stream_spec["line_count"]) ) for name, content in sources.items(): expected = spec["sources"][name] if len(content) != expected["size"] or sha256_bytes(content) != expected["sha256"]: raise SystemExit(f"B28 {name} source identity changed") directory = root / name directory.mkdir(parents=True) payload = directory / "payload.bin" payload.write_bytes(content) os.utime(payload, (0, 0)) os.utime(directory, (0, 0)) return sources def run(argv: list[str], cwd: Path, expected: set[int] = {0}) -> subprocess.CompletedProcess[str]: completed = subprocess.run( argv, cwd=cwd, check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=120, ) if completed.returncode not in expected: raise SystemExit( f"command failed ({completed.returncode}): {' '.join(argv)}\n{completed.stdout}" ) return completed def verify_fsck(image: Path, expected: bytes, label: str) -> None: with tempfile.TemporaryDirectory(prefix=f"b28-{label}-fsck-") as temporary: destination = Path(temporary) run(["fsck.erofs", f"--extract={destination}", str(image)], image.parent) if (destination / "payload.bin").read_bytes() != expected: raise SystemExit(f"B28 {label} fsck extraction mismatch") def build_codec( codec: str, codec_spec: dict[str, object], sources: dict[str, bytes], source_root: Path, images: Path, ) -> list[dict[str, object]]: valid = images / f"{codec}-valid.erofs" run( [ "mkfs.erofs", *codec_spec["mkfs_args"], str(valid), str(source_root / codec_spec["source"]), ], images, ) valid_data = valid.read_bytes() actual_image_sha256 = sha256_bytes(valid_data) source = sources[codec_spec["source"]] verify_fsck(valid, source, f"{codec}-valid") extent = codec_spec["extent"] start = extent["physical_offset"] end = start + extent["physical_length"] block = valid_data[start:end] leading = next((index for index, value in enumerate(block) if value), len(block)) stream = block[leading:] if ( leading != extent["leading_zero_bytes"] or len(stream) != extent["stream_bytes"] or not (extent["prefix_consumed"] < codec_spec["corruption_start"] < len(stream)) ): raise SystemExit(f"{codec} pcluster or corruption boundary changed") corrupted_data = bytearray(valid_data) corruption = start + leading + codec_spec["corruption_start"] corrupted_data[corruption:end] = b"\0" * (end - corruption) if corrupted_data == valid_data: raise SystemExit(f"{codec} corruption mutation changed no bytes") corrupted = images / f"{codec}-corrupt.erofs" corrupted.write_bytes(corrupted_data) with tempfile.TemporaryDirectory(prefix=f"b28-{codec}-corrupt-") as temporary: failed = run( ["fsck.erofs", f"--extract={temporary}", str(corrupted)], images, set(range(1, 256)), ) return [ { "class": "valid", "codec": codec, "expected_errno": 0, "gate_image_sha256": codec_spec["gate_image_sha256"], "path": valid.name, "sha256": actual_image_sha256, }, { "class": "corrupt", "codec": codec, "expected_full_errno": 97, "expected_prefix_errno": 0 if codec_spec["decision"] == "GO" else 97, "fsck_exit": failed.returncode, "path": corrupted.name, "sha256": sha256_path(corrupted), }, ] def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--spec", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() spec = load_spec(args.spec) if args.output.exists(): raise SystemExit(f"refusing existing B28 output: {args.output}") for tool in ("mkfs.erofs", "fsck.erofs"): if shutil.which(tool) is None: raise SystemExit(f"{tool} is required") args.output.mkdir(parents=True) source_root = args.output / "sources" images = args.output / "images" images.mkdir() sources = make_sources(source_root, spec) records = [] for codec in sorted(spec["codecs"]): records.extend( build_codec(codec, spec["codecs"][codec], sources, source_root, images) ) (args.output / "spec.json").write_bytes(args.spec.read_bytes()) index = { "batch": "B28", "candidate": "P15-086", "fixture_count": len(records), "fixtures": records, "schema": 1, "source_sha256": { name: sha256_bytes(content) for name, content in sorted(sources.items()) }, "status": "READY", "test": "TC176-stream-runtime", } (args.output / "fixture-index.json").write_text( json.dumps(index, indent=2, sort_keys=True) + "\n", encoding="ascii" ) print(json.dumps(index, indent=2, sort_keys=True)) if __name__ == "__main__": main()