Files
erofs-freebsd-out-tree/tests/pre15/fixtures/B25-codec-oracle.py
T
2026-08-18 09:20:44 +02:00

435 lines
15 KiB
Python
Executable File

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from pathlib import Path
import re
import subprocess
SOURCE_NAMES = (
"compress.h",
"decompressor.c",
"decompressor_lz4.c",
"decompressor_lzma.c",
"decompressor_deflate.c",
"decompressor_zstd.c",
"zdata.c",
)
def fail(message: str) -> None:
raise SystemExit(message)
def replace_once(source: str, old: str, new: str, label: str) -> str:
count = source.count(old)
if count != 1:
fail(f"{label}: expected one source occurrence, found {count}")
return source.replace(old, new, 1)
def extract_function(source: str, name: str) -> str:
match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
if not match:
fail(f"missing function: {name}")
name_line = source.rfind("\n", 0, match.start()) + 1
start = source.rfind("\n", 0, name_line - 1) + 1
brace = source.find("{", match.end())
if brace < 0:
fail(f"missing function body: {name}")
depth = 0
state = "code"
index = brace
while index < len(source):
char = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code":
if char == "/" and following == "*":
state = "block"
index += 2
continue
if char == "/" and following == "/":
state = "line"
index += 2
continue
if char == '"':
state = "string"
elif char == "'":
state = "character"
elif char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return source[start : index + 1]
elif state == "block" and char == "*" and following == "/":
state = "code"
index += 2
continue
elif state == "line" and char == "\n":
state = "code"
elif state in {"string", "character"}:
if char == "\\":
index += 2
continue
if (state == "string" and char == '"') or (
state == "character" and char == "'"
):
state = "code"
index += 1
fail(f"unterminated function: {name}")
def committed(root: Path, baseline: str, name: str) -> str:
completed = subprocess.run(
["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/src/{name}"],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
fail(f"cannot read B25 baseline {name}: {completed.stderr.strip()}")
return completed.stdout
def require_once(source: str, needle: str, label: str) -> None:
count = source.count(needle)
if count != 1:
fail(f"{label}: expected one occurrence, found {count}")
def verify_exact_small_transforms(current: dict[str, str], base: dict[str, str]) -> None:
expected = replace_once(
base["compress.h"],
"\tconst char *name;\n",
"\tconst char *name;\n"
"\t/* Callbacks return zero or a positive FreeBSD errno. */\n",
"callback errno contract",
)
if current["compress.h"] != expected:
fail("compress.h differs from the declared callback contract annotation")
expected = base["decompressor.c"]
expected = replace_once(
expected,
"\t\tif (size < sizeof(*lz4))\n\t\t\treturn (EINVAL);",
"\t\tif (size < sizeof(*lz4))\n\t\t\treturn (EINTEGRITY);",
"short LZ4 config",
)
expected = replace_once(
expected,
"\t\t (Z_EROFS_PCLUSTER_MAX_SIZE >> sbi->blkszbits))\n"
"\t\t\treturn (EINVAL);",
"\t\t (Z_EROFS_PCLUSTER_MAX_SIZE >> sbi->blkszbits))\n"
"\t\t\treturn (EOPNOTSUPP);",
"unsupported LZ4 pcluster size",
)
if expected.count("\t\treturn (EOVERFLOW);\n") != 2:
fail("baseline config arithmetic errno count changed")
expected = expected.replace(
"\t\treturn (EOVERFLOW);\n", "\t\treturn (EINTEGRITY);\n"
)
expected = replace_once(expected, "\tint ret;\n", "", "folding temporary")
expected = replace_once(
expected,
"\tret = decompressor->decompress(&rq);\n"
"\treturn (ret == 0 ? 0 : EIO);",
"\treturn (decompressor->decompress(&rq));",
"dispatch preservation",
)
if current["decompressor.c"] != expected:
fail("decompressor.c differs from the declared B25 dispatch/config transforms")
expected = base["decompressor_lz4.c"]
if expected.count("return (-1);") != 12 or expected.count(": -1);") != 1:
fail("baseline LZ4 private-status count changed")
expected = expected.replace("return (-1);", "return (EINTEGRITY);")
expected = expected.replace(": -1);", ": EINTEGRITY);")
if current["decompressor_lz4.c"] != expected:
fail("LZ4 differs from the exact private-status conversion")
if current["zdata.c"] != base["zdata.c"]:
fail("B25 changed the already-correct zdata I/O or cleanup path")
def verify_source_contracts(
source: dict[str, str], fixture: dict[str, object]
) -> dict[str, object]:
joined = "\n".join(source[name] for name in SOURCE_NAMES)
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", joined):
fail("negative Linux errno entered the FreeBSD codec path")
for name in SOURCE_NAMES:
if "return (-1)" in source[name] or re.search(r":\s*-1\s*\)", source[name]):
fail(f"private -1 codec status remains in {name}")
dispatch = extract_function(source["decompressor.c"], "z_erofs_decompress")
if dispatch.count("return (decompressor->decompress(&rq));") != 2:
fail("typed backend dispatch does not have two direct callback exits")
if "ret == 0 ? 0 : EIO" in dispatch:
fail("dispatcher still folds typed backend errors")
require_once(dispatch, "return (EOPNOTSUPP);", "unsupported algorithm")
if dispatch.count("return (EINTEGRITY);") != 2:
fail("dispatch-local corruption checks changed")
for case in fixture["dispatch"]:
if case.get("backend_errno") != case.get("expected"):
fail("dispatch fixture does not require exact errno preservation")
config_expectations = {
"z_erofs_load_lz4_config": ("decompressor.c", "EINTEGRITY", "EOPNOTSUPP"),
"z_erofs_load_lzma_config": (
"decompressor_lzma.c",
"EINTEGRITY",
"EOPNOTSUPP",
),
"z_erofs_load_deflate_config": (
"decompressor_deflate.c",
"EINTEGRITY",
"EOPNOTSUPP",
),
"z_erofs_load_zstd_config": (
"decompressor_zstd.c",
"EINTEGRITY",
"EOPNOTSUPP",
),
}
for function, (name, malformed, unsupported) in config_expectations.items():
body = extract_function(source[name], function)
if f"return ({malformed});" not in body:
fail(f"{function} lacks malformed-config errno")
if f"return ({unsupported});" not in body:
fail(f"{function} lacks unsupported-config errno")
if fixture["config"] != [
{"class": "malformed", "errno": "EINTEGRITY"},
{"class": "unsupported", "errno": "EOPNOTSUPP"},
]:
fail("config fixture does not encode the B25 typed classes")
lzma = extract_function(source["decompressor_lzma.c"], "z_erofs_lzma_decompress")
require_once(lzma, "return (EOVERFLOW);", "LZMA ABI overflow")
require_once(lzma, "return (ENOMEM);", "LZMA allocation failure")
if not (
lzma.index("xz_dec_microlzma_run")
< lzma.index("xz_dec_microlzma_end")
< lzma.index("return (error);")
):
fail("LZMA state is not released before status publication")
deflate = extract_function(
source["decompressor_deflate.c"], "z_erofs_deflate_decompress"
)
require_once(deflate, "inflateEnd(&strm)", "Deflate cleanup")
require_once(
deflate,
"if (error == 0 && endret != Z_OK)",
"Deflate primary-error preservation",
)
if deflate.index("inflateEnd(&strm)") > deflate.index("return (error);"):
fail("Deflate returns before releasing initialized state")
zstd_source = source["decompressor_zstd.c"]
enabled = extract_function(zstd_source, "z_erofs_zstd_decompress")
require_once(enabled, "return (ENOMEM);", "Zstd allocation failure")
require_once(
enabled,
"if (error == 0 && ZSTD_isError(ret))",
"Zstd primary-error preservation",
)
require_once(enabled, "error = EOPNOTSUPP;", "Zstd parameter mapping")
if enabled.count("error = EINTEGRITY;") != 4:
fail("Zstd stream corruption/completion mappings changed")
require_once(enabled, "error = EIO;", "Zstd release mapping")
if "ZSTD_getErrorCode" in zstd_source:
fail("B25 added a Zstd provider symbol outside the existing ABI")
if zstd_source.count("return (EOPNOTSUPP);") < 3:
fail("Zstd config/runtime disabled paths are not typed unsupported")
expected_zstd = {
"create_context": "ENOMEM",
"set_window_limit": "EOPNOTSUPP",
"decompress_stream": "EINTEGRITY",
"free_context": "EIO",
}
actual_zstd = {
case.get("operation"): case.get("expected") for case in fixture["zstd"]
}
if actual_zstd != expected_zstd or len(fixture["zstd"]) != len(expected_zstd):
fail("Zstd phase fixture does not match the source contract")
read_extent = extract_function(source["zdata.c"], "z_erofs_read_extent")
decode_at = read_extent.index("error = z_erofs_decompress")
meta_release_at = read_extent.index("erofs_put_metabuf(&buf)", decode_at)
physical_release_at = read_extent.index("erofs_brelse(compressed)", decode_at)
error_at = read_extent.index("if (error != 0)", decode_at)
free_at = read_extent.index("free(decoded, M_EROFS)", error_at)
if not (
decode_at < meta_release_at < error_at < free_at
and decode_at < physical_release_at < error_at < free_at
):
fail("zdata input/output cleanup ordering changed")
return {
"callbacks": 2,
"config_loaders": len(config_expectations),
"dispatch_direct": True,
"dispatch_fixture_cases": len(fixture["dispatch"]),
"negative_errno": False,
"zdata_unchanged": True,
}
def compile_status_harness(
source: dict[str, str], fixture: dict[str, object], artifacts: Path
) -> dict[str, int]:
xz_helper = extract_function(source["decompressor_lzma.c"], "z_erofs_lzma_error")
zlib_helper = extract_function(
source["decompressor_deflate.c"], "z_erofs_deflate_error"
)
checks: list[str] = []
for group, function in (
("xz", "z_erofs_lzma_error"),
("zlib", "z_erofs_deflate_error"),
):
for case in fixture[group]:
checks.append(
f'\tcheck("{group}:{case["status"]}", '
f'{function}({case["status"]}), {case["expected"]});'
)
program = f'''#include <stddef.h>
#include <stdio.h>
#define EIO 5
#define ENOMEM 12
#define EOVERFLOW 75
#define EOPNOTSUPP 95
#define EINTEGRITY 97
enum xz_ret {{
\tXZ_OK,
\tXZ_STREAM_END,
\tXZ_UNSUPPORTED_CHECK,
\tXZ_MEM_ERROR,
\tXZ_MEMLIMIT_ERROR,
\tXZ_FORMAT_ERROR,
\tXZ_OPTIONS_ERROR,
\tXZ_DATA_ERROR,
\tXZ_BUF_ERROR
}};
#define Z_OK 0
#define Z_STREAM_END 1
#define Z_NEED_DICT 2
#define Z_ERRNO (-1)
#define Z_STREAM_ERROR (-2)
#define Z_DATA_ERROR (-3)
#define Z_MEM_ERROR (-4)
#define Z_BUF_ERROR (-5)
#define Z_VERSION_ERROR (-6)
{xz_helper}
{zlib_helper}
static int failures;
static void
check(const char *name, int actual, int expected)
{{
\tif (actual != expected) {{
\t\tfprintf(stderr, "%s: %d != %d\\n", name, actual, expected);
\t\t++failures;
\t}}
}}
int
main(void)
{{
{chr(10).join(checks)}
\tif (failures != 0)
\t\treturn (1);
\tprintf("status-map cases={len(checks)}\\n");
\treturn (0);
}}
'''
harness = artifacts / "B25-status-map.c"
binary = artifacts / "B25-status-map"
harness.write_text(program, encoding="ascii")
compile_run = subprocess.run(
["cc", "-std=c11", "-Wall", "-Wextra", "-Werror", str(harness), "-o", str(binary)],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
(artifacts / "B25-status-map-build.stdout").write_text(
compile_run.stdout, encoding="utf-8"
)
(artifacts / "B25-status-map-build.stderr").write_text(
compile_run.stderr, encoding="utf-8"
)
if compile_run.returncode != 0:
fail("B25 status-map harness did not compile")
execute = subprocess.run(
[str(binary)],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
(artifacts / "B25-status-map.stdout").write_text(execute.stdout, encoding="utf-8")
(artifacts / "B25-status-map.stderr").write_text(execute.stderr, encoding="utf-8")
if execute.returncode != 0:
fail("B25 status-map harness failed")
binary.unlink()
return {
"xz": len(fixture["xz"]),
"zlib": len(fixture["zlib"]),
"zstd": len(fixture["zstd"]),
}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--artifacts", required=True, type=Path)
parser.add_argument("--baseline", required=True)
parser.add_argument("--dut", required=True, type=Path)
parser.add_argument("--fixture", required=True, type=Path)
parser.add_argument("--root", required=True, type=Path)
args = parser.parse_args()
fixture = json.loads(args.fixture.read_text(encoding="ascii"))
if fixture.get("schema") != 1 or fixture.get("candidate") != "P15-023":
fail("invalid B25 fixture identity")
for key in ("config", "dispatch", "xz", "zlib", "zstd"):
if not isinstance(fixture.get(key), list) or not fixture[key]:
fail(f"empty B25 fixture group: {key}")
source_dir = args.dut / "src"
current = {
name: (source_dir / name).read_text(encoding="utf-8") for name in SOURCE_NAMES
}
base = {name: committed(args.root, args.baseline, name) for name in SOURCE_NAMES}
verify_exact_small_transforms(current, base)
contracts = verify_source_contracts(current, fixture)
mappings = compile_status_harness(current, fixture, args.artifacts)
report = {
"candidate": "P15-023",
"contracts": contracts,
"mapping_cases": mappings,
"status": "PASS",
}
(args.artifacts / "B25-codec-errors.json").write_text(
json.dumps(report, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
encoding="ascii",
)
print(
"B25 codec errno contract: "
f"xz={mappings['xz']} zlib={mappings['zlib']} zstd={mappings['zstd']}"
)
if __name__ == "__main__":
main()