#!/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'
