#!/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=083acc65f842c409c5a2e089e50060d0062b677c
fixture_dir=$PRE15_DUT/tests/pre15/fixtures
spec=$fixture_dir/B23-explicit-spec.json
generator=$fixture_dir/B23-explicit-generate.py
oracle=$fixture_dir/B23-explicit-oracle.py
probe=$fixture_dir/B23-explicit-probe.c
kld_builder=$fixture_dir/B23-build-kld.sh
zmap=$PRE15_DUT/src/zmap.c
linux_zmap=$PRE15_ROOT/src-linux/zmap.c
artifacts=$PRE15_RUN_DIR/artifacts
first=$PRE15_CASE_TMP/first
second=$PRE15_CASE_TMP/second

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

pre15_record_fixture b23-spec "$spec"
pre15_record_fixture b23-generator "$generator"
pre15_record_fixture b23-oracle "$oracle"
pre15_record_fixture b23-probe "$probe"
pre15_record_fixture b23-kld-builder "$kld_builder"
pre15_record_fixture b23-case "$PRE15_DUT/tests/pre15/cases/B23-explicit-table.sh"
pre15_record_fixture b23-zmap "$zmap"
pre15_record_fixture b23-linux-zmap "$linux_zmap"
mkdir -p "$artifacts"

if ! python3 -B "$generator" --spec "$spec" --output "$first" \
    >"$artifacts/B23-generate-first.stdout" \
    2>"$artifacts/B23-generate-first.stderr"; then
	pre15_runner_fail 'B23 first fixture generation failed'
fi
if ! python3 -B "$generator" --spec "$spec" --output "$second" \
    >"$artifacts/B23-generate-second.stdout" \
    2>"$artifacts/B23-generate-second.stderr"; then
	pre15_runner_fail 'B23 repeat fixture generation failed'
fi
if ! diff -u "$first/SHA256SUMS" "$second/SHA256SUMS" \
    >"$artifacts/B23-repeat-sums.diff" || \
    ! diff -u "$first/fixture-manifest.json" "$second/fixture-manifest.json" \
    >"$artifacts/B23-repeat-manifest.diff"; then
	pre15_runner_fail 'B23 fixture generation is not byte deterministic'
fi
if ! python3 -B "$oracle" --spec "$spec" --fixtures "$first" \
    --report "$artifacts/B23-oracle-first.json" \
    >"$artifacts/B23-oracle-first.stdout" \
    2>"$artifacts/B23-oracle-first.stderr"; then
	pre15_runner_fail 'B23 independent fixture oracle failed'
fi
if ! python3 -B "$oracle" --spec "$spec" --fixtures "$second" \
    --report "$artifacts/B23-oracle-second.json" \
    >"$artifacts/B23-oracle-second.stdout" \
    2>"$artifacts/B23-oracle-second.stderr"; then
	pre15_runner_fail 'B23 repeat fixture oracle failed'
fi
cp "$first/SHA256SUMS" "$artifacts/B23-fixture-SHA256SUMS"
cp "$first/fixture-manifest.json" "$artifacts/B23-fixture-manifest.json"
pre15_record_fixture b23-generated-sums "$artifacts/B23-fixture-SHA256SUMS"

if ! cc -std=c11 -Wall -Wextra -Werror -c "$probe" \
    -o "$PRE15_CASE_TMP/B23-explicit-probe.o" \
    >"$artifacts/B23-probe-compile.stdout" \
    2>"$artifacts/B23-probe-compile.stderr"; then
	pre15_runner_fail 'B23 guest probe does not compile on the host'
fi

if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$spec" \
    "$artifacts/B23-source-audit.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]
spec_path = Path(sys.argv[4])
report_path = Path(sys.argv[5])


def committed(path: str) -> str:
    completed = subprocess.run(
        ["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
        check=False,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    if completed.returncode != 0:
        raise SystemExit(f"cannot read B23 baseline {path}: {completed.stderr}")
    return completed.stdout


def function(source: str, name: str) -> str:
    match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
    if match is None:
        raise SystemExit(f"missing function: {name}")
    start = source.rfind("\n", 0, source.rfind("\n", 0, match.start())) + 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}")


spec = json.loads(spec_path.read_text(encoding="ascii"))
current = (dut / "src/zmap.c").read_text(encoding="utf-8")
before = committed("src/zmap.c")
validator = function(current, "z_erofs_validate_extent_table")
old_validator = function(before, "z_erofs_validate_extent_table")
if validator == old_validator:
    raise SystemExit("B23 validator did not change")
if function(current, "z_erofs_read_extent") != function(before, "z_erofs_read_extent"):
    raise SystemExit("B23 changed the local record decoder")
for name in ("z_erofs_map_blocks_ext", "z_erofs_fill_inode"):
    if function(current, name) != function(before, name):
        raise SystemExit(f"B23 changed {name}")
if "#define Z_EROFS_EXTENT_VALIDATE_CHUNK_SIZE (64 * 1024)" not in current:
    raise SystemExit("B23 fixed validation chunk bound is missing")
required = (
    "z_erofs_read_extent(sbi, vi, record_pos, recsz, &ext)",
    "while (scan_pos < table_end)",
    "Z_EROFS_EXTENT_VALIDATE_CHUNK_SIZE",
    "erofs_read_metadata(sbi, vi->nid, scan_pos, chunk_len, &buf)",
    "memcpy(&ext, (const char *)buf.data + offset, recsz)",
    "z_erofs_extent_lstart(&ext, recsz)",
    "erofs_put_metabuf(&buf)",
    "return (index == vi->z_extents ? 0 : EINTEGRITY)",
)
if not all(marker in validator for marker in required):
    raise SystemExit("B23 validator is missing a bounded-scan contract marker")
if validator.count("z_erofs_read_extent(") != 1:
    raise SystemExit("B23 short tail probe count changed")
if validator.count("erofs_read_metadata(") != 1:
    raise SystemExit("B23 long-table reader is not chunk-scoped")
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", validator):
    raise SystemExit("B23 introduced Linux negative errno")

allowed = {
    "repo-pre-15/src/zmap.c",
    "repo-pre-15/tests/pre15/cases/B23-explicit-table.sh",
    "repo-pre-15/tests/pre15/fixtures/B23-build-kld.sh",
    "repo-pre-15/tests/pre15/fixtures/B23-explicit-generate.py",
    "repo-pre-15/tests/pre15/fixtures/B23-explicit-oracle.py",
    "repo-pre-15/tests/pre15/fixtures/B23-explicit-probe.c",
    "repo-pre-15/tests/pre15/fixtures/B23-explicit-spec.json",
}
changed = set(
    subprocess.run(
        ["git", "-C", str(root), "diff", "--name-only", baseline, "--", "repo-pre-15"],
        check=True,
        text=True,
        stdout=subprocess.PIPE,
    ).stdout.splitlines()
)
changed.update(
    subprocess.run(
        [
            "git", "-C", str(root), "ls-files", "--others",
            "--exclude-standard", "--", "repo-pre-15",
        ],
        check=True,
        text=True,
        stdout=subprocess.PIPE,
    ).stdout.splitlines()
)
if changed != allowed:
    raise SystemExit(f"B23 write set differs: {sorted(changed ^ allowed)}")
for path in (
    "src/erofs_fs.h",
    "src/data.c",
    "src/decompressor.c",
    "src/decompressor_lz4.c",
    "src/decompressor_lzma.c",
    "src/decompressor_deflate.c",
    "src/decompressor_zstd.c",
    "src/internal.h",
):
    if (dut / path).read_text(encoding="utf-8") != committed(path):
        raise SystemExit(f"B23 changed an excluded provider/codec/ABI file: {path}")

linux = (root / "src-linux/zmap.c").read_text(encoding="utf-8")
for marker in (
    "recsz <= offsetof(struct z_erofs_extent, pstart_hi)",
    "le64_to_cpu(*(__le64 *)ext)",
    "le32_to_cpu(ext->pstart_lo)",
    "le32_to_cpu(ext->pstart_hi) << 32",
    "le32_to_cpu(ext->lstart_hi) << 32",
    "erofs_inode_in_metabox(inode)",
):
    if marker not in linux:
        raise SystemExit(f"Linux explicit-table anchor changed: {marker}")

report = {
    "baseline": baseline,
    "batch": "B23",
    "candidates": spec["candidates"],
    "chunk_size": spec["chunk_size"],
    "decompressor_files_unchanged": True,
    "freebsd_positive_errno": True,
    "linux_record_decode_audited": True,
    "ondisk_header_unchanged": True,
    "primary_metabox_reader_unchanged": True,
    "status": "PASS",
    "test": spec["test"],
    "write_set": sorted(changed),
}
report_path.write_text(
    json.dumps(report, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
    encoding="ascii",
)
print("B23 source/write-set/Linux-FreeBSD audit PASS")
PY
then
	:
else
	pre15_dut_fail 'B23 source, write-set, or cross-tree audit failed'
fi

sha256sum "$artifacts/B23-fixture-SHA256SUMS" \
    "$artifacts/B23-fixture-manifest.json" \
    "$artifacts/B23-oracle-first.json" \
    "$artifacts/B23-oracle-second.json" \
    "$artifacts/B23-source-audit.json" \
    >"$artifacts/SHA256SUMS"
pre15_target_reached

if test "${PRE15_MODE:-host}" != qemu; then
	printf '%s\n' \
		'TC171 host real-fixture, independent oracle, and bounded source audit PASS' \
		'QEMU exact-source mapped/backing runtime NOT_RUN in host mode' \
		'Full feature suite NOT_RUN'
	exit 0
fi

for tool in awk clang file nm scp tar; do
	command -v "$tool" >/dev/null 2>&1 || \
		pre15_infra_blocked "missing B23 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/B23-fixtures.tar.gz
module=$PRE15_CASE_TMP/B23-erofs.ko
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
    "$PRE15_CASE_TMP/kld-work" >"$artifacts/B23-kld-build.stdout" \
    2>"$artifacts/B23-kld-build.stderr"; then
	pre15_dut_fail 'B23 cross-target zstdio0 KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/B23-kld-file.txt"
sha256sum "$module" >"$artifacts/B23-kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/B23-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/B23-fixtures.tar.gz || \
    ! pre15_scp "$probe" /root/B23-explicit-probe.c || \
    ! pre15_scp "$module" /root/B23-erofs.ko; then
	pre15_infra_blocked 'could not transfer B23 module, probe, or fixtures'
fi
if ! pre15_guest_ssh_bounded \
    'rm -rf /root/pre15-b23-fixtures && mkdir /root/pre15-b23-fixtures && tar -xzf /root/B23-fixtures.tar.gz -C /root/pre15-b23-fixtures'; then
	pre15_infra_blocked 'could not prepare B23 guest fixtures'
fi
if pre15_guest_ssh_bounded kldstat -n erofs >/dev/null 2>&1; then
	pre15_infra_blocked 'FreeBSD guest already has an EROFS module loaded'
fi
if ! pre15_guest_ssh_bounded kldload /root/B23-erofs.ko \
    >"$artifacts/B23-kldload.stdout" 2>"$artifacts/B23-kldload.stderr"; then
	if grep -q 'module already loaded or in kernel' "$artifacts/B23-kldload.stderr"; then
		pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
	fi
	pre15_dut_fail 'B23 exact-source KLD failed to load'
fi
pre15_own_guest_kld erofs 'B23 exact-source KLD'
if ! pre15_guest_ssh_bounded cc -std=c11 -Wall -Wextra -Werror \
    -o /root/B23-explicit-probe /root/B23-explicit-probe.c; then
	pre15_infra_blocked 'could not compile the B23 guest probe'
fi

python3 -B - "$first/fixture-manifest.json" >"$PRE15_CASE_TMP/B23-qemu-cases.tsv" <<'PY'
import json
from pathlib import Path
import sys

manifest = json.loads(Path(sys.argv[1]).read_text(encoding="ascii"))
for name, case in sorted(manifest["cases"].items()):
    offsets = ",".join(str(value) for value in case["probe_offsets"])
    print(
        name,
        case["image"],
        case["expected_errno"],
        case["file_size"],
        offsets,
        case["target_marker"],
        sep="\t",
    )
PY

pre15_attach_md()
{
	pre15_md=$(pre15_guest_ssh_bounded mdconfig -a -t vnode -f "$1") || \
		pre15_dut_fail "could not attach B23 provider: $1"
	case "$pre15_md" in
	md[0-9]*) ;;
	*) pre15_runner_fail "unexpected mdconfig output: $pre15_md" ;;
	esac
	pre15_md=${pre15_md#md}
	pre15_own_guest_md "$pre15_md" "$2"
	printf '%s\n' "$pre15_md"
}

while IFS="$(printf '\t')" read -r case_id image_name expected_errno \
    file_size offsets target_marker; do
	mountpoint=/mnt/pre15-b23-$case_id
	unit=$(pre15_attach_md "/root/pre15-b23-fixtures/$image_name" \
	    "B23 $case_id primary")
	pre15_guest_ssh_bounded mkdir -p "$mountpoint"
	if ! pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/md$unit" \
	    "$mountpoint"; then
		pre15_dut_fail "B23 $case_id mount failed before $target_marker"
	fi
	pre15_own_guest_mount "$mountpoint" "B23 $case_id mount"
	if test "$expected_errno" -eq 0; then
		old_ifs=$IFS
		IFS=,
		set -- $offsets
		IFS=$old_ifs
		if ! pre15_guest_ssh_bounded /root/B23-explicit-probe pass \
		    "$mountpoint/target.bin" "$file_size" "$@"; then
			pre15_dut_fail "B23 mapped positive failed at $target_marker"
		fi
	else
		if ! pre15_guest_ssh_bounded /root/B23-explicit-probe errno \
		    "$mountpoint/target.bin" "$expected_errno"; then
			pre15_dut_fail "B23 corruption result failed at $target_marker"
		fi
	fi
done <"$PRE15_CASE_TMP/B23-qemu-cases.tsv"

pre15_guest_ssh_bounded dmesg >"$artifacts/B23-dmesg.txt"
if grep -Eq 'panic:|Fatal trap|lock order reversal|KDB: stack backtrace' \
    "$artifacts/B23-dmesg.txt"; then
	pre15_dut_fail 'B23 runtime produced a kernel diagnostic'
fi
printf '%s\n' \
	'TC171 QEMU explicit primary/metabox mapped and corruption runtime PASS' \
	'All errno values are positive FreeBSD ABI values; full feature suite NOT_RUN'
