#!/bin/sh
set -eu

: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_CASE_TMP:?PRE15_CASE_TMP is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"

baseline=e769e4ae32d4967c1f69067dc92628b60b62b648
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
spec=$fixture_dir/B21-super-spec.json
generator=$fixture_dir/B21-super-generate.py
oracle=$fixture_dir/B21-super-oracle.py
qemu_probe=$fixture_dir/B21-qemu-probe.c
kld_builder=$fixture_dir/B28-build-kld.sh
artifacts=$PRE15_RUN_DIR/artifacts
first=$PRE15_CASE_TMP/first
second=$PRE15_CASE_TMP/second

for tool in cmp fsck.erofs git mkfs.erofs python3 sha256sum; do
	command -v "$tool" >/dev/null 2>&1 || \
		pre15_infra_blocked "missing B21 host tool: $tool"
done

pre15_record_fixture b21-spec "$spec"
pre15_record_fixture b21-generator "$generator"
pre15_record_fixture b21-oracle "$oracle"
pre15_record_fixture b21-qemu-probe "$qemu_probe"
pre15_record_fixture b21-kld-builder "$kld_builder"
pre15_record_fixture b21-case "$PRE15_DUT/tests/pre15/cases/B21-super.sh"
pre15_record_fixture b21-super "$PRE15_DUT/src/super.c"
pre15_record_fixture b21-internal "$PRE15_DUT/src/internal.h"
pre15_record_fixture b21-xattr "$PRE15_DUT/src/xattr.c"
mkdir -p "$artifacts"

if python3 -B "$generator" --spec "$spec" --output "$first" \
	--work "$PRE15_CASE_TMP/first-work" \
	>"$artifacts/generate-first.json" \
	2>"$artifacts/generate-first.stderr"; then
	:
else
	pre15_runner_fail 'B21 first fixture generation failed'
fi
if python3 -B "$generator" --spec "$spec" --output "$second" \
	--work "$PRE15_CASE_TMP/second-work" \
	>"$artifacts/generate-second.json" \
	2>"$artifacts/generate-second.stderr"; then
	:
else
	pre15_runner_fail 'B21 second fixture generation failed'
fi

if cmp "$first/fixture-index.json" "$second/fixture-index.json" && \
	find "$first" -type f ! -name fixture-index.json -printf '%f\n' | sort | \
	while IFS= read -r name; do cmp "$first/$name" "$second/$name" || exit 1; done
then
	:
else
	pre15_runner_fail 'B21 fixture generation is not byte reproducible'
fi

if python3 -B "$oracle" --fixtures "$first" \
	--report "$artifacts/oracle-first.json" \
	>"$artifacts/oracle-first.stdout" 2>"$artifacts/oracle-first.stderr" && \
	python3 -B "$oracle" --fixtures "$second" \
	--report "$artifacts/oracle-second.json" \
	>"$artifacts/oracle-second.stdout" 2>"$artifacts/oracle-second.stderr" && \
	cmp "$artifacts/oracle-first.json" "$artifacts/oracle-second.json"
then
	:
else
	pre15_runner_fail 'B21 independent oracle replay failed'
fi

if fsck.erofs -d0 "$first/legal-control.erofs" \
	>"$artifacts/legal-fsck.stdout" 2>"$artifacts/legal-fsck.stderr"; then
	:
else
	pre15_runner_fail 'B21 legal control is not accepted by fsck.erofs'
fi

cp "$first/fixture-index.json" "$artifacts/B21-fixture-index.json"
pre15_record_fixture b21-generated-index "$artifacts/B21-fixture-index.json"
pre15_target_reached

if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
	:
else
if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \
	"$artifacts/B21-source-check.json" <<'PY'
from __future__ import annotations

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])
src = dut / "src"


def committed(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:
        raise SystemExit(f"cannot read B21 baseline {name}: {completed.stderr}")
    return completed.stdout


def replace_once(source: str, old: str, new: str, label: str) -> str:
    count = source.count(old)
    if count != 1:
        raise SystemExit(f"{label}: expected one transform source, 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:
        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())
    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: list[str], label: str) -> None:
    position = -1
    for marker in markers:
        position = source.find(marker, position + 1)
        if position < 0:
            raise SystemExit(f"{label} is missing ordered marker: {marker}")


current = {
    name: (src / name).read_text(encoding="utf-8")
    for name in ("internal.h", "super.c", "xattr.c")
}
base = {name: committed(name) for name in current}

if current["internal.h"] != base["internal.h"]:
    raise SystemExit("B21 changed internal.h despite all required helpers existing")
for helper in (
    "EROFS_FEATURE_FUNCS(fragments, incompat, INCOMPAT_FRAGMENTS)",
    "EROFS_FEATURE_FUNCS(sb_chksum, compat, COMPAT_SB_CHKSUM)",
    "EROFS_FEATURE_FUNCS(plain_xattr_pfx, compat, COMPAT_PLAIN_XATTR_PFX)",
):
    if current["internal.h"].count(helper) != 1:
        raise SystemExit(f"B21 feature helper is missing or duplicated: {helper}")

expected_super = replace_once(
    base["super.c"],
    "\tif ((le32toh(dsb->feature_compat) & EROFS_FEATURE_COMPAT_SB_CHKSUM) == 0)\n",
    "\tif (!erofs_sb_has_sb_chksum(sbi))\n",
    "checksum helper",
)
expected_super = replace_once(
    expected_super,
    "\tif ((sbi->feature_incompat & EROFS_FEATURE_INCOMPAT_FRAGMENTS) != 0 &&\n"
    "\t    sbi->packed_nid > 0) {\n",
    "\tif (erofs_sb_has_fragments(sbi) && sbi->packed_nid > 0) {\n",
    "fragments helper",
)
old_read = extract_function(expected_super, "erofs_read_superblock")
new_read = extract_function(current["super.c"], "erofs_read_superblock")
expected_read = replace_once(
    old_read,
    "\tif (dsb->dirblkbits != 0)\n"
    "\t\treturn (EOPNOTSUPP);\n"
    "\tsbi->feature_compat = le32toh(dsb->feature_compat);\n",
    "\tsbi->blkszbits = dsb->blkszbits;\n"
    "\tsbi->block_size = 1u << sbi->blkszbits;\n"
    "\tsbi->feature_compat = le32toh(dsb->feature_compat);\n"
    "\terror = erofs_superblock_csum_verify(sbi, dsb);\n"
    "\tif (error != 0)\n"
    "\t\treturn (error);\n\n"
    "\tif (dsb->dirblkbits != 0)\n"
    "\t\treturn (EOPNOTSUPP);\n",
    "checksum trust order",
)
dead_exception = """\t/*
\t * Narrowly allow one extra combination: long xattr prefixes enabled
\t * with non-plain prefix table stored in a packed inode, which adds
\t * the FRAGMENTS (0x20) incompat bit.  This is NOT a declaration of
\t * general fragments support; per-inode data layout is still gated
\t * by plain/inline checks in erofs_read_inode().
\t */
\tif (unsupported != 0) {
\t\tif (unsupported != EROFS_FEATURE_INCOMPAT_FRAGMENTS ||
\t\t    (sbi->feature_incompat &
\t\t\tEROFS_FEATURE_INCOMPAT_XATTR_PREFIXES) == 0 ||
\t\t    (sbi->feature_compat &
\t\t\tEROFS_FEATURE_COMPAT_PLAIN_XATTR_PFX) != 0 ||
\t\t    sbi->packed_nid == 0)
\t\t\treturn (EOPNOTSUPP);
\t}
\tsbi->blkszbits = dsb->blkszbits;
\tsbi->block_size = 1u << sbi->blkszbits;
"""
expected_read = replace_once(
    expected_read,
    dead_exception,
    "\tif (unsupported != 0)\n\t\treturn (EOPNOTSUPP);\n",
    "dead fragments exception",
)
old_checksum_site = (
    "\terror = erofs_superblock_csum_verify(sbi, dsb);\n"
    "\tif (error != 0)\n"
    "\t\treturn (error);\n"
)
if expected_read.count(old_checksum_site) != 2:
    raise SystemExit("checksum trust-order transform did not produce two sites")
old_checksum_offset = expected_read.rfind(old_checksum_site)
expected_read = (
    expected_read[:old_checksum_offset]
    + expected_read[old_checksum_offset + len(old_checksum_site) :]
)
if new_read != expected_read:
    raise SystemExit("erofs_read_superblock differs from exact B21 transforms")
expected_super = expected_super.replace(old_read, expected_read, 1)
if current["super.c"] != expected_super:
    raise SystemExit("super.c changed outside exact B21 transforms")

expected_xattr = replace_once(
    base["xattr.c"],
    "\tif ((sbi->feature_incompat & EROFS_FEATURE_INCOMPAT_XATTR_PREFIXES) ==\n"
    "\t\t0 ||\n"
    "\t    sbi->xattr_prefix_count == 0)\n",
    "\tif (!erofs_sb_has_xattr_prefixes(sbi) || sbi->xattr_prefix_count == 0)\n",
    "xattr-prefix helper",
)
expected_xattr = replace_once(
    expected_xattr,
    "\tif ((sbi->feature_compat & EROFS_FEATURE_COMPAT_PLAIN_XATTR_PFX) == 0) {\n",
    "\tif (!erofs_sb_has_plain_xattr_pfx(sbi)) {\n",
    "plain-prefix helper",
)
if current["xattr.c"] != expected_xattr:
    raise SystemExit("xattr.c changed outside exact B21 helper conversions")

read_super = new_read
checksum_call = read_super.index("erofs_superblock_csum_verify(sbi, dsb)")
before_checksum = read_super[:checksum_call]
for protected in (
    "dirblkbits",
    "feature_incompat",
    "packed_nid",
    "extra_devices",
    "sb_extslots",
    "meta_blkaddr",
    "xattr_blkaddr",
    "xattr_prefix_start",
    "xattr_prefix_count",
    "blocks_root",
):
    if protected in before_checksum:
        raise SystemExit(f"protected field used before checksum: {protected}")
require_order(
    read_super,
    [
        "dsb->magic",
        "dsb->blkszbits < 9",
        "sbi->feature_compat = le32toh(dsb->feature_compat)",
        "erofs_superblock_csum_verify(sbi, dsb)",
        "dsb->dirblkbits",
        "sbi->feature_incompat = le32toh(dsb->feature_incompat)",
        "unsupported = sbi->feature_incompat",
        "sbi->sb_size = 128 + dsb->sb_extslots",
        "sbi->xattr_prefix_start = le32toh(dsb->xattr_prefix_start)",
        "erofs_validate_device_size",
        "erofs_load_generation_seed",
        "z_erofs_parse_cfgs",
    ],
    "superblock trust order",
)

mountfs = extract_function(current["super.c"], "erofs_mountfs")
require_order(
    mountfs,
    [
        "erofs_read_superblock",
        "erofs_scan_devices",
        "erofs_init_packed_inode",
        "erofs_init_metabox_inode",
        "erofs_read_inode(sbi, sbi->root_nid",
        "erofs_xattr_prefixes_init",
        "mp->mnt_data = sbi",
    ],
    "FreeBSD mount publication",
)
if "fail:\n\terofs_sb_free(sbi);\n\treturn (error);" not in mountfs:
    raise SystemExit("mount failure no longer funnels through erofs_sb_free")
sb_free = extract_function(current["super.c"], "erofs_sb_free")
require_order(
    sb_free,
    [
        "z_erofs_extent_cache_fini",
        "erofs_xattr_prefixes_cleanup",
        "erofs_drop_internal_inodes",
        "erofs_free_dev_context",
        "erofs_release_device_info(&sbi->dif0)",
        "free(sbi, M_EROFS)",
    ],
    "mount cleanup",
)
release = extract_function(current["super.c"], "erofs_release_device_info")
require_order(
    release,
    ["g_topology_lock", "g_vfs_close", "g_topology_unlock", "vrele", "dev_rel"],
    "GEOM release",
)
if any("return (-E" in current[name] for name in current):
    raise SystemExit("negative errno entered the FreeBSD B21 write set")

result = {
    "baseline": baseline,
    "checksum_before_protected_fields": True,
    "dead_fragments_exception_removed": True,
    "feature_helpers": True,
    "geom_release_preserved": True,
    "internal_h_unchanged": True,
    "mount_cleanup_preserved": True,
    "mount_publication_preserved": True,
    "positive_errno_preserved": True,
    "status": "PASS",
}
output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="ascii")
print("B21 source: exact trust-order and feature-helper transforms present")
PY
then
	:
else
	pre15_dut_fail 'B21 source contract failed'
fi
fi

if python3 - "$artifacts/B21-fixture-index.json" \
	"$artifacts/oracle-first.json" "$artifacts/B21-matrix.tsv" <<'PY'
import json
from pathlib import Path
import sys

index = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
oracle = json.loads(Path(sys.argv[2]).read_text(encoding="ascii"))
if index["case_count"] != 13 or index["legal_count"] != 1 or index["damaged_count"] != 12:
    raise SystemExit("B21 fixture cardinality changed")
if oracle["status"] != "PASS" or oracle["passed_count"] != 13:
    raise SystemExit("B21 oracle matrix is incomplete")
lines = ["id\tauthenticated\terrno\treject\tsha256"]
for fixture, result in zip(index["cases"], oracle["results"], strict=True):
    lines.append(
        f"{fixture['id']}\t{str(fixture['authenticated']).lower()}\t"
        f"{result['actual_errno']}\t{result['actual_reject']}\t{fixture['sha256']}"
    )
Path(sys.argv[3]).write_text("\n".join(lines) + "\n", encoding="ascii")
print(
    f"B21 fixtures: {index['case_count']} "
    f"({index['legal_count']} legal, {index['damaged_count']} damaged)"
)
PY
then
	:
else
	pre15_runner_fail 'B21 fixture matrix summary failed'
fi

if test "${PRE15_MODE:-host}" != qemu; then
	printf '%s\n' \
		'B21 super gate: host PASS' \
		'TC172 QEMU runtime NOT_RUN in host mode'
	exit 0
fi

for tool in awk clang file nm scp tar timeout; do
	command -v "$tool" >/dev/null 2>&1 || \
		pre15_infra_blocked "missing B21 QEMU tool: $tool"
done
: "${PRE15_QEMU_CONTROL_PATH:?QEMU control path is required}"
: "${PRE15_QEMU_SSH_KEY:?QEMU SSH key is required}"
: "${PRE15_QEMU_SSH_PORT:?QEMU SSH port is required}"
: "${PRE15_QEMU_SSH_USER:?QEMU SSH user is required}"

fixture_archive=$PRE15_CASE_TMP/B21-fixtures.tar.gz
module=$PRE15_CASE_TMP/B21-erofs-zstdio0.ko
if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
    "$PRE15_FREEBSD_SRC" "$module" "$PRE15_CASE_TMP/kld-work" 0 \
    >"$artifacts/B21-kld-build.stdout" 2>"$artifacts/B21-kld-build.stderr"; then
	pre15_dut_fail 'B21 cross-target zstdio0 KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/B21-kld-file.txt"
sha256sum "$module" >"$artifacts/B21-kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/B21-kld-nm-u.txt"
tar -C "$first" -czf "$fixture_archive" .

pre15_scp()
{
	timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" scp -O -q \
		-o BatchMode=yes -o StrictHostKeyChecking=no \
		-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
		-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
		-i "$PRE15_QEMU_SSH_KEY" -P "$PRE15_QEMU_SSH_PORT" \
		"$1" "$PRE15_QEMU_SSH_USER@127.0.0.1:$2"
}

if ! pre15_scp "$fixture_archive" /root/B21-fixtures.tar.gz || \
    ! pre15_scp "$qemu_probe" /root/B21-qemu-probe.c || \
    ! pre15_scp "$module" /root/B21-erofs-zstdio0.ko; then
	pre15_infra_blocked 'could not transfer B21 module, probe, or fixtures'
fi
if ! pre15_guest_ssh_bounded \
    'rm -rf /root/B21-fixtures && mkdir /root/B21-fixtures && tar -xzf /root/B21-fixtures.tar.gz -C /root/B21-fixtures && cc -O2 -Wall -Wextra -Werror -std=c17 -o /root/B21-qemu-probe /root/B21-qemu-probe.c'; then
	pre15_infra_blocked 'could not prepare B21 guest fixtures/probe'
fi
if pre15_guest_ssh_bounded kldstat -q -m erofs >/dev/null 2>&1; then
	pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
fi
if ! pre15_guest_ssh_bounded kldload /root/B21-erofs-zstdio0.ko \
    >"$artifacts/B21-kldload.stdout" 2>"$artifacts/B21-kldload.stderr"; then
	if grep -q 'module already loaded or in kernel' \
	    "$artifacts/B21-kldload.stderr"; then
		pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
	fi
	pre15_dut_fail 'B21 exact-source zstdio0 KLD failed to load'
fi
pre15_own_guest_kld erofs 'B21 exact-source KLD'
pre15_guest_ssh_bounded dmesg >"$artifacts/B21-dmesg-before.txt"
pre15_guest_ssh_bounded mkdir -p /mnt/pre15-b21

b21_unown()
{
	kind=$1
	value=$2
	tmp=$PRE15_CASE_TMP/ownership.$$
	awk -F '\t' -v kind="$kind" -v value="$value" \
	    '!($1 == kind && $2 == value)' "$PRE15_OWNERSHIP_FILE" >"$tmp"
	mv "$tmp" "$PRE15_OWNERSHIP_FILE"
}

printf 'id\texpected_errno\tactual_errno\treject\n' \
    >"$artifacts/B21-qemu-matrix.tsv"
while IFS="$(printf '\t')" read -r id authenticated expected_errno reject fixture_hash; do
	test "$id" != id || continue
	b21_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
	    -f "/root/B21-fixtures/$id.erofs") || \
		pre15_dut_fail "$id md attach failed"
	case "$b21_md" in
	md[0-9]*) ;;
	*) pre15_runner_fail "unexpected B21 md unit: $b21_md" ;;
	esac
	pre15_own_guest_md "$b21_md" "$id md"
	result=$(pre15_guest_ssh_bounded /root/B21-qemu-probe \
	    "/dev/$b21_md" /mnt/pre15-b21) || \
		pre15_infra_blocked "$id guest mount probe failed"
	actual_errno=$(printf '%s\n' "$result" | sed -n 's/.* errno=\([0-9][0-9]*\).*/\1/p')
	test -n "$actual_errno" || pre15_runner_fail "$id mount probe did not report errno"
	if test "$actual_errno" = 0; then
		pre15_own_guest_mount /mnt/pre15-b21 "$id mount"
	fi
	printf '%s\t%s\t%s\t%s\n' "$id" "$expected_errno" \
	    "$actual_errno" "$reject" >>"$artifacts/B21-qemu-matrix.tsv"
	if test "$actual_errno" != "$expected_errno"; then
		pre15_dut_fail "$id expected errno $expected_errno, received $actual_errno"
	fi
	if test "$actual_errno" = 0; then
		pre15_guest_ssh_bounded find /mnt/pre15-b21 -mindepth 1 -maxdepth 1 \
		    -print >"$artifacts/B21-legal-readdir.txt"
		pre15_guest_ssh_bounded umount /mnt/pre15-b21 || \
			pre15_dut_fail "$id unmount failed"
		b21_unown guest-mount /mnt/pre15-b21
	fi
	pre15_guest_ssh_bounded mdconfig -d -u "$b21_md" || \
		pre15_dut_fail "$id md detach failed"
	b21_unown guest-md "$b21_md"
done <"$artifacts/B21-matrix.tsv"

pre15_guest_ssh_bounded dmesg >"$artifacts/B21-dmesg-after.txt"
diff -u "$artifacts/B21-dmesg-before.txt" "$artifacts/B21-dmesg-after.txt" \
    >"$artifacts/B21-dmesg.diff" || true
sed -n '/^+++ /d; /^+/s/^+//p' "$artifacts/B21-dmesg.diff" \
    >"$artifacts/B21-dmesg-added.txt"
if grep -Eqi 'panic:|lock order reversal|witness.*warning|use-after-free' \
    "$artifacts/B21-dmesg-added.txt"; then
	pre15_dut_fail 'TC172 produced panic, WITNESS, or UAF evidence'
fi
printf '%s\n' \
	'TC172-super-trust B21 QEMU PASS' \
	'13/13 legal/damaged images returned exact mount errno with checksum trust order preserved'
