#!/usr/bin/env python3 from __future__ import annotations import argparse import hashlib import json import os from pathlib import Path import re import shutil import subprocess EXTENT_RE = re.compile( r"^\s*0:\s*(\d+)\.\.\s*(\d+)\s*\|\s*(\d+)\s*:\s*" r"(\d+)\.\.\s*(\d+)\s*\|\s*(\d+)\s*$", re.MULTILINE, ) def sha256_path(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def run(argv: list[str], cwd: Path) -> str: completed = subprocess.run( argv, cwd=cwd, check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=120, ) if completed.returncode != 0: raise SystemExit( f"command failed ({completed.returncode}): {' '.join(argv)}\n" f"{completed.stdout}" ) return completed.stdout 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 = json.loads(args.spec.read_text(encoding="ascii")) if ( spec.get("schema") != 1 or spec.get("batch") != "B33" or spec.get("test") != "TC168-cache-inflight" ): raise SystemExit("invalid B33 cache fixture spec") if args.output.exists(): raise SystemExit(f"refusing existing B33 output: {args.output}") for tool in ("dump.erofs", "mkfs.erofs"): if shutil.which(tool) is None: raise SystemExit(f"{tool} is required") pattern = b"Pre15-B33-cache-policy:0123456789abcdef\n" expected = spec["source"] source = (pattern * ((expected["size"] + len(pattern) - 1) // len(pattern)))[ : expected["size"] ] if hashlib.sha256(source).hexdigest() != expected["sha256"]: raise SystemExit("B33 deterministic source identity changed") source_dir = args.output / "source" images = args.output / "images" source_dir.mkdir(parents=True) images.mkdir() payload = source_dir / "payload.bin" payload.write_bytes(source) os.utime(payload, (0, 0)) os.utime(source_dir, (0, 0)) valid = images / "lz4-valid.erofs" run(["mkfs.erofs", *spec["mkfs_args"], str(valid), str(source_dir)], images) extent_text = run( ["dump.erofs", "-e", "--path=/payload.bin", str(valid)], images ) match = EXTENT_RE.search(extent_text) if match is None: raise SystemExit("B33 single extent could not be parsed") logical, logical_end, logical_length, physical, physical_end, physical_length = ( map(int, match.groups()) ) if ( logical != 0 or logical_end != expected["size"] or logical_length != expected["size"] or physical_end - physical != physical_length or physical_length != 4096 ): raise SystemExit("B33 fixture is not one 64 KiB logical pcluster") valid_data = valid.read_bytes() if physical + physical_length > len(valid_data): raise SystemExit("B33 physical extent exceeds image") pcluster = valid_data[physical : physical + physical_length] leading = next( (index for index, value in enumerate(pcluster) if value), len(pcluster) ) stream = pcluster[leading:] if leading == len(pcluster) or len(stream) < 2: raise SystemExit("B33 LZ4 stream boundary is empty") truncated_data = bytearray(valid_data) truncated_data[physical + leading : physical + physical_length] = ( b"\0" + stream[:-1] ) if truncated_data == valid_data: raise SystemExit("B33 truncated mutation changed no bytes") truncated = images / "lz4-truncated.erofs" truncated.write_bytes(truncated_data) records = [] for image_class, path, expected_errno in ( ("valid", valid, 0), ("truncated", truncated, 97), ): records.append( { "class": image_class, "expected_errno": expected_errno, "path": path.name, "sha256": sha256_path(path), } ) index = { "batch": "B33", "decoded_size": len(source), "fixture_count": len(records), "fixtures": records, "leading_zero_bytes": leading, "physical_length": physical_length, "physical_offset": physical, "schema": 1, "source_sha256": sha256_path(payload), "status": "READY", "test": "TC168-cache-inflight", } (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()