#!/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=145d892afc8a425294029d4349f03f1b17cf8f92 artifacts=$PRE15_RUN_DIR/artifacts xattr=$PRE15_DUT/src/xattr.c linux_xattr=$PRE15_ROOT/src-linux/xattr.c xattr_header=$PRE15_DUT/src/xattr.h vnops=$PRE15_DUT/src/erofs_vnops.c fixture_dir=$PRE15_DUT/tests/pre15/fixtures fixture_spec=$fixture_dir/B17-xattr-spec.json fixture_generator=$fixture_dir/B17-xattr-generate.py fixture_oracle=$fixture_dir/B17-xattr-oracle.py fixture_seed=$fixture_dir/B17-xattr-seed.erofs for tool in git python3 sha256sum; do command -v "$tool" >/dev/null 2>&1 || \ pre15_infra_blocked "missing B18 host tool: $tool" done pre15_record_fixture b18-freebsd-xattr "$xattr" pre15_record_fixture b18-linux-xattr "$linux_xattr" pre15_record_fixture b18-xattr-header "$xattr_header" pre15_record_fixture b18-vnops "$vnops" pre15_record_fixture b18-b17-spec "$fixture_spec" pre15_record_fixture b18-b17-generator "$fixture_generator" pre15_record_fixture b18-b17-oracle "$fixture_oracle" pre15_record_fixture b18-b17-seed "$fixture_seed" pre15_record_fixture b18-case \ "$PRE15_DUT/tests/pre15/cases/B18-xattr-order.sh" mkdir -p "$artifacts" pre15_target_reached if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \ "$artifacts/B18-xattr-order.json" <<'PY' from __future__ import annotations from collections import Counter import hashlib 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] output = Path(sys.argv[4]) xattr_path = dut / "src/xattr.c" current = xattr_path.read_text(encoding="utf-8") linux = (root / "src-linux/xattr.c").read_text(encoding="utf-8") def committed_bytes(path: str) -> bytes: completed = subprocess.run( ["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"], check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) if completed.returncode != 0: raise SystemExit( f"cannot read B18 baseline {path}: " f"{completed.stderr.decode(errors='replace')}" ) return completed.stdout def require_once(source: str, marker: str, label: str) -> int: count = source.count(marker) if count != 1: raise SystemExit(f"{label}: expected one marker, found {count}: {marker!r}") return source.index(marker) def extract_function(source: str, name: str) -> str: match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE) if not match: raise SystemExit(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: raise SystemExit(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 raise SystemExit(f"unterminated function: {name}") def require_order(source: str, markers: tuple[str, ...], label: str) -> None: position = -1 for marker in markers: position = source.find(marker, position + 1) if position < 0: raise SystemExit(f"{label}: missing ordered marker: {marker}") base = committed_bytes("src/xattr.c").decode("utf-8") iterator_start = require_once(base, "struct erofs_xattr_iter {", "iterator") iterator_end_marker = "\n};\n\n" iterator_end = base.find(iterator_end_marker, iterator_start) if iterator_end < 0: raise SystemExit("B18 baseline iterator terminator is absent") iterator_end += len(iterator_end_marker) iterator_block = base[iterator_start:iterator_end] without_iterator = base[:iterator_start] + base[iterator_end:] acl_start = require_once( without_iterator, "static int\nerofs_inode_has_noacl(", "ACL/filter block", ) acl_end = require_once( without_iterator, "static int\nerofs_listxattr_foreach(", "common iterator block", ) if acl_end <= acl_start: raise SystemExit("B18 baseline ACL/filter block is not before the iterator core") acl_block = without_iterator[acl_start:acl_end] expected = without_iterator[:acl_start] + without_iterator[acl_end:] core_start = require_once( expected, "static int\nerofs_xattr_backing_size(", "backing core", ) expected = expected[:core_start] + iterator_block + expected[core_start:] acl_adapter = require_once(expected, "int\nerofs_get_acl(", "ACL adapter") expected = expected[:acl_adapter] + acl_block + expected[acl_adapter:] if current != expected: raise SystemExit("xattr.c differs from the two exact B18 definition moves") base_names = re.findall(r"^(erofs_[A-Za-z0-9_]+)\s*\(", base, re.MULTILINE) current_names = re.findall( r"^(erofs_[A-Za-z0-9_]+)\s*\(", current, re.MULTILINE ) if Counter(base_names) != Counter(current_names): raise SystemExit("B18 changed the xattr function definition multiset") base_functions = {name: extract_function(base, name) for name in base_names} current_functions = {name: extract_function(current, name) for name in current_names} for name in base_functions: if current_functions[name] != base_functions[name]: raise SystemExit(f"B18 changed function text instead of moving it: {name}") if current_functions[name].startswith("static ") != base_functions[name].startswith( "static " ): raise SystemExit(f"B18 changed symbol visibility: {name}") def direct_calls(functions: dict[str, str]) -> Counter[tuple[str, str]]: calls: Counter[tuple[str, str]] = Counter() for caller, function in functions.items(): brace = function.find("{") for callee in re.findall( r"\b(erofs_[A-Za-z0-9_]+)\s*\(", function[brace + 1 :] ): calls[(caller, callee)] += 1 return calls base_calls = direct_calls(base_functions) current_calls = direct_calls(current_functions) if current_calls != base_calls: raise SystemExit("B18 changed the direct-call multiset") expected_order = [ "erofs_xattr_backing_size", "erofs_xattr_read_backing", "erofs_xattr_read_metadata", "erofs_xattr_move", "erofs_xattr_load_body", "erofs_xattr_validate_entry", "erofs_xattr_prefix", "erofs_xattr_namespace_prefix", "erofs_xattr_list_move", "erofs_xattr_resolve_name", "erofs_xattr_name_match", "erofs_xattr_shared_entry_offset", "erofs_xattr_load_shared_entry", "erofs_listxattr_foreach", "erofs_getxattr_foreach", "erofs_xattr_iter_inline", "erofs_xattr_iter_shared", "erofs_getxattr", "erofs_listxattr", "erofs_xattr_prefixes_cleanup", "erofs_xattr_prefixes_init", "erofs_inode_has_noacl", "erofs_acl_from_mode", "erofs_posix_acl_from_xattr", "erofs_get_acl", ] if current_names != expected_order: raise SystemExit(f"B18 xattr definition order mismatch: {current_names}") if current.index("struct erofs_xattr_iter {") > current.index( "erofs_xattr_backing_size(" ): raise SystemExit("xattr iterator contract is not before the backing/core helpers") linux_aligned_core = ( "erofs_listxattr_foreach(", "erofs_getxattr_foreach(", "erofs_xattr_iter_inline(", "erofs_xattr_iter_shared(", "erofs_getxattr(", "erofs_listxattr(", "erofs_xattr_prefixes_cleanup(", "erofs_xattr_prefixes_init(", ) require_order(current, linux_aligned_core, "FreeBSD common xattr core") require_order(linux, linux_aligned_core, "Linux common xattr core") readonly_paths = ( "src/xattr.h", "src/erofs_vnops.c", "tests/pre15/fixtures/B17-xattr-spec.json", "tests/pre15/fixtures/B17-xattr-generate.py", "tests/pre15/fixtures/B17-xattr-oracle.py", "tests/pre15/fixtures/B17-xattr-seed.erofs", ) for path in readonly_paths: if (dut / path).read_bytes() != committed_bytes(path): raise SystemExit(f"B18 changed a read-only xattr/VOP/fixture contract: {path}") vnops = (dut / "src/erofs_vnops.c").read_text(encoding="utf-8") getextattr = extract_function(vnops, "erofs_getextattr") listextattr = extract_function(vnops, "erofs_listextattr") require_order( getextattr, ("extattr_check_cred(", "switch (ap->a_attrnamespace)", "erofs_getxattr("), "FreeBSD getextattr adapter", ) require_order( listextattr, ("extattr_check_cred(", "switch (ap->a_attrnamespace)", "erofs_listxattr("), "FreeBSD listextattr adapter", ) if vnops.count("extattr_check_cred(") != 2: raise SystemExit("FreeBSD extattr credential boundary changed") if "ERANGE" in current: raise SystemExit("Linux all-or-nothing xattr buffer semantics entered FreeBSD") if "uiomove(value, value_size, uio)" not in current: raise SystemExit("FreeBSD partial extattr transfer path is absent") if re.search(r"return\s*(?:\(\s*)?-E[A-Z0-9_]+", current): raise SystemExit("Linux negative errno entered FreeBSD xattr.c") load_body = extract_function(current, "erofs_xattr_load_body") require_order( load_body, ( "vi->xattr_isize < sizeof(*ih)", "vi->xattr_isize == sizeof(*ih)", "error = EOPNOTSUPP", "header_size = sizeof(*ih)", ), "exact-header compatibility", ) spec_path = dut / "tests/pre15/fixtures/B17-xattr-spec.json" spec = json.loads(spec_path.read_text(encoding="ascii")) cases = spec.get("cases", []) legal = sum(item.get("class") == "legal" for item in cases) damaged = sum(item.get("class") == "damaged" for item in cases) if len(cases) != 25 or legal != 10 or damaged != 15: raise SystemExit("B17 xattr fixture order/cardinality changed") seed_path = dut / "tests/pre15/fixtures" / spec["seed"]["path"] seed_hash = hashlib.sha256(seed_path.read_bytes()).hexdigest() if seed_hash != spec["seed"]["sha256"]: raise SystemExit("B17 xattr seed no longer matches its frozen specification") result = { "status": "PASS", "baseline": baseline, "source_sha256": hashlib.sha256(current.encode("utf-8")).hexdigest(), "definition_count": len(current_names), "direct_call_edges": len(current_calls), "direct_call_sites": sum(current_calls.values()), "static_visibility_unchanged": True, "function_text_unchanged": True, "linux_core_order": list(linux_aligned_core), "freebsd_contracts": [ "positive errno", "partial uiomove", "extattr_check_cred", "USER and SYSTEM namespaces", "exact-header EOPNOTSUPP", "uncached xattr lookup", ], "b17_fixture_cases": len(cases), "b17_fixture_legal": legal, "b17_fixture_damaged": damaged, "b17_seed_sha256": seed_hash, } output.write_text( json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="ascii" ) print( "B18 xattr order: exact moves, " f"{len(current_names)} definitions, " f"{sum(current_calls.values())} direct calls" ) print(f"B18 B17 fixture contract: {len(cases)} cases, seed {seed_hash}") PY then : else pre15_dut_fail 'B18 xattr ordering or FreeBSD contract equivalence failed' fi printf '%s\n' 'B18 xattr order: PASS'