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

fixture_dir=$PRE15_DUT/tests/pre15/fixtures
spec=$fixture_dir/B28-partial.json
generator=$fixture_dir/B28-partial-fixtures.py
probe=$fixture_dir/B28-partial-probe.c
kld_builder=$fixture_dir/B28-build-kld.sh
gate_script=$PRE15_DUT/tests/pre15/gates/P15-086.sh
gate_input=$PRE15_DUT/tests/pre15/gates/P15-086-input.json
gate_result=$PRE15_ROOT/planning/pre15/evidence/20260815T154915Z-G04-G05-P15-086/gate-output/result.json
gate_capabilities=$PRE15_ROOT/planning/pre15/evidence/20260815T154915Z-G04-G05-P15-086/gate-output/authorized-capabilities.json
artifacts=$PRE15_RUN_DIR/artifacts
first=$PRE15_CASE_TMP/fixtures-first
second=$PRE15_CASE_TMP/fixtures-second
action=${1:-runtime}

case "$action" in
runtime|k2) ;;
*) pre15_fail_usage "unknown B28 action: $action" ;;
esac

if test "$action" = k2; then
	for tool in awk clang cmp diff file nm sha256sum; do
		command -v "$tool" >/dev/null 2>&1 || \
			pre15_infra_blocked "missing B28 K2 tool: $tool"
	done
	pre15_record_fixture b28-case "$PRE15_DUT/tests/pre15/cases/B28-partial.sh"
	pre15_record_fixture b28-kld-builder "$kld_builder"
	for source in compress.h decompressor.c decompressor_lz4.c \
	    decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c; do
		pre15_record_fixture "b28-$source" "$PRE15_DUT/src/$source"
	done
	mkdir -p "$artifacts"
	for config in 0 1; do
		module=$PRE15_CASE_TMP/B28-zstdio$config.ko
		if ! timeout -k 10 600 /bin/sh "$kld_builder" "$PRE15_DUT" \
		    "$PRE15_FREEBSD_SRC" "$module" \
		    "$PRE15_CASE_TMP/kld-work-$config" "$config" \
		    >"$artifacts/B28-build-zstdio$config.stdout" \
		    2>"$artifacts/B28-build-zstdio$config.stderr"; then
			pre15_dut_fail "B28 zstdio$config cross-target KLD build failed"
		fi
		file "$module" >"$artifacts/B28-zstdio$config-file.txt"
		sha256sum "$module" >"$artifacts/B28-zstdio$config-sha256.txt"
		nm -g "$module" | LC_ALL=C sort \
		    >"$artifacts/B28-zstdio$config-nm-global.txt"
		nm -u "$module" | LC_ALL=C sort \
		    >"$artifacts/B28-zstdio$config-nm-u.txt"
	done
	awk '$1 != "U" { print $NF }' "$artifacts/B28-zstdio0-nm-global.txt" \
	    >"$PRE15_CASE_TMP/B28-zstdio0-global-names.txt"
	awk '$1 != "U" { print $NF }' "$artifacts/B28-zstdio1-nm-global.txt" \
	    >"$PRE15_CASE_TMP/B28-zstdio1-global-names.txt"
	if ! diff -u "$PRE15_CASE_TMP/B28-zstdio0-global-names.txt" \
	    "$PRE15_CASE_TMP/B28-zstdio1-global-names.txt" \
	    >"$artifacts/B28-config-nm-global.diff"; then
		pre15_dut_fail 'B28 K2 global symbols differ by ZSTDIO configuration'
	fi
	if ! grep -q ' z_erofs_decompress_supports_subextent$' \
	    "$artifacts/B28-zstdio0-nm-global.txt"; then
		pre15_dut_fail 'B28 K2 partial capability symbol is absent'
	fi
	pre15_target_reached
	printf '%s\n' \
		'B28 targeted FreeBSD 15 K2 PASS' \
		'zstdio0/zstdio1 link, defined-global parity, optional Zstd ABI, and partial capability symbol PASS' \
		'KLD and object files removed by owned case cleanup'
	exit 0
fi

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

pre15_record_fixture b28-case "$PRE15_DUT/tests/pre15/cases/B28-partial.sh"
pre15_record_fixture b28-spec "$spec"
pre15_record_fixture b28-generator "$generator"
pre15_record_fixture b28-probe "$probe"
pre15_record_fixture b28-kld-builder "$kld_builder"
pre15_record_fixture b28-gate "$gate_script"
pre15_record_fixture b28-gate-input "$gate_input"
pre15_record_fixture b28-gate-result "$gate_result"
pre15_record_fixture b28-gate-capabilities "$gate_capabilities"
for source in compress.h decompressor.c decompressor_lz4.c \
    decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c; do
	pre15_record_fixture "b28-$source" "$PRE15_DUT/src/$source"
done
mkdir -p "$artifacts"

if ! timeout -k 5 120 python3 -B "$generator" --spec "$spec" \
    --output "$first" >"$artifacts/generate-first.stdout" \
    2>"$artifacts/generate-first.stderr"; then
	pre15_runner_fail 'B28 first real EROFS fixture generation failed'
fi
if ! timeout -k 5 120 python3 -B "$generator" --spec "$spec" \
    --output "$second" >"$artifacts/generate-second.stdout" \
    2>"$artifacts/generate-second.stderr"; then
	pre15_runner_fail 'B28 second real EROFS fixture generation failed'
fi
if ! diff -ru "$first" "$second" >"$artifacts/fixture-repeat.diff"; then
	pre15_runner_fail 'B28 real EROFS fixtures are not byte reproducible'
fi
cp "$first/fixture-index.json" "$artifacts/B28-fixture-index.json"

if test "${PRE15_QEMU_TARGET_ONLY:-0}" = 1; then
	pre15_target_reached
else
if ! python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$spec" "$gate_result" \
    "$gate_capabilities" "$artifacts/B28-source-audit.json" \
    "$PRE15_CASE_TMP/decision-harness.c" <<'PY'
from __future__ import annotations

import hashlib
import json
from pathlib import Path
import re
import subprocess
import sys


root = Path(sys.argv[1])
dut = Path(sys.argv[2])
spec_path = Path(sys.argv[3])
gate_result_path = Path(sys.argv[4])
gate_capabilities_path = Path(sys.argv[5])
report_path = Path(sys.argv[6])
harness_path = Path(sys.argv[7])
spec = json.loads(spec_path.read_text(encoding="ascii"))
baseline = spec["baseline"]
implementation = "7a0d7e4a4047ac6d6130a3be5cb677cb94979fe7"
runtime_base = "d09924362eb29819bd393e1e86602eb38c7608c6"


def fail(message: str) -> None:
    raise SystemExit(message)


def sha256_path(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def committed(commit: str, name: str) -> str:
    completed = subprocess.run(
        ["git", "-C", str(root), "show", f"{commit}:repo-pre-15/src/{name}"],
        check=False,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
    )
    if completed.returncode != 0:
        fail(f"cannot read B28 source {commit}:{name}: {completed.stderr.strip()}")
    return completed.stdout


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


if (
    spec.get("schema") != 1
    or spec.get("batch") != "B28"
    or spec.get("candidate") != "P15-086"
    or spec.get("test") != "TC176-stream-runtime"
):
    fail("B28 fixture identity changed")
for name, commit in (
    ("baseline", baseline),
    ("implementation", implementation),
    ("runtime base", runtime_base),
):
    if subprocess.check_output(
        ["git", "-C", str(root), "rev-parse", commit], text=True
    ).strip() != commit:
        fail(f"B28 {name} identity changed")
gate = json.loads(gate_result_path.read_text(encoding="ascii"))
capabilities = json.loads(gate_capabilities_path.read_text(encoding="ascii"))
if (
    gate.get("status") != "GO"
    or gate.get("g04") != "GO"
    or gate.get("g05") != "GO"
    or gate.get("b28") != "AUTHORIZED"
    or gate.get("resolved_base") != "6bf5724619be70f805bbe7d1ba77dd70cdced69f"
):
    fail("frozen P15-086 gate no longer authorizes B28")
if capabilities != {
    "full_fallback": ["zstd"],
    "partial": ["deflate", "lz4", "lzma"],
    "persistent_state_added": False,
    "schema": 1,
}:
    fail("P15-086 per-codec authorization changed")
for name, expected in (
    ("gate script", spec["gate"]["script_sha256"]),
    ("gate input", spec["gate"]["input_sha256"]),
    ("gate result", spec["gate"]["result_sha256"]),
):
    path = {
        "gate script": dut / "tests/pre15/gates/P15-086.sh",
        "gate input": dut / "tests/pre15/gates/P15-086-input.json",
        "gate result": gate_result_path,
    }[name]
    if sha256_path(path) != expected:
        fail(f"{name} hash changed")

source_names = (
    "compress.h",
    "decompressor.c",
    "decompressor_lz4.c",
    "decompressor_lzma.c",
    "decompressor_deflate.c",
    "decompressor_zstd.c",
    "zdata.c",
)
baseline_sources = {name: committed(baseline, name) for name in source_names}
implemented_sources = {name: committed(implementation, name) for name in source_names}
sources = {
    name: (dut / "src" / name).read_text(encoding="utf-8")
    for name in source_names
}
if implemented_sources["decompressor_lz4.c"] != baseline_sources["decompressor_lz4.c"]:
    fail("B28 changed the already-capable LZ4 backend implementation")
if "bool supports_subextent;" not in implemented_sources["compress.h"]:
    fail("B28 descriptor capability is absent")
if implemented_sources["compress.h"].count("z_erofs_decompress_supports_subextent") != 1:
    fail("B28 capability prototype count changed")
if "bool supports_subextent;" not in sources["compress.h"]:
    fail("current tree lost the B28 descriptor capability")
if sources["compress.h"].count("z_erofs_decompress_supports_subextent") != 1:
    fail("current B28 capability prototype count changed")

def read_descriptor_decisions(source_set: dict[str, str]) -> dict[str, bool]:
    decisions = {}
    for codec in ("lzma", "deflate", "zstd"):
        value = re.search(
            rf"const struct z_erofs_decompressor z_erofs_{codec}_decomp = \{{.*?\.supports_subextent = ([01]),",
            source_set[f"decompressor_{codec}.c"],
            re.DOTALL,
        )
        if value is None:
            fail(f"missing {codec} descriptor capability")
        decisions[codec] = value.group(1) == "1"
    lz4 = re.search(
        r"static const struct z_erofs_decompressor z_erofs_lz4_decomp = \{.*?\.supports_subextent = ([01]),",
        source_set["decompressor.c"],
        re.DOTALL,
    )
    if lz4 is None:
        fail("missing LZ4 descriptor capability")
    decisions["lz4"] = lz4.group(1) == "1"
    return decisions


implemented_decisions = read_descriptor_decisions(implemented_sources)
if implemented_decisions != {
    "lz4": True, "lzma": True, "deflate": True, "zstd": False
}:
    fail(f"B28 implementation decisions differ from G04/G05: {implemented_decisions}")
descriptor_decisions = read_descriptor_decisions(sources)
if descriptor_decisions != {
    "lz4": True, "lzma": True, "deflate": True, "zstd": False
}:
    fail(f"current partial descriptor decisions changed: {descriptor_decisions}")
expected_zstd = committed(runtime_base, "decompressor_zstd.c")
if function(sources["decompressor_zstd.c"], "z_erofs_zstd_finish") != \
    function(expected_zstd, "z_erofs_zstd_finish"):
    fail("current Zstd completion policy differs from the accepted transform")

unchanged_zdata = (
    "z_erofs_extent_cache_match",
    "z_erofs_extent_cache_copy",
    "z_erofs_extent_cache_publish",
    "z_erofs_extent_cache_init",
    "z_erofs_extent_cache_fini",
    "z_erofs_extent_cache_eligible",
    "z_erofs_read_data",
    "z_erofs_read_uio",
)
baseline_zdata = baseline_sources["zdata.c"]
for name in unchanged_zdata:
    if function(implemented_sources["zdata.c"], name) != function(baseline_zdata, name):
        fail(f"B28 changed unrelated zdata function {name}")
implemented_decode_length = function(implemented_sources["zdata.c"], "z_erofs_decode_length")
implemented_read_extent = function(implemented_sources["zdata.c"], "z_erofs_read_extent")
implemented_do_read = function(implemented_sources["zdata.c"], "z_erofs_do_read")
decode_length = function(sources["zdata.c"], "z_erofs_decode_length")
decode_extent = function(sources["zdata.c"], "z_erofs_decode_extent")
do_read = function(sources["zdata.c"], "z_erofs_do_read")
if decode_length != implemented_decode_length:
    fail("current tree changed the B28 decode-length transform")
if decode_extent[decode_extent.index("{") :] != implemented_read_extent[
    implemented_read_extent.index("{") :
]:
    fail("current tree changed B28 provider or decoded-buffer ownership")
for anchor in (
    "z_erofs_decompress_supports_subextent(map)",
    "*partial = (map->m_flags & EROFS_MAP_PARTIAL_REF) != 0;",
    "*decoded_len = end;",
    "*partial = true;",
):
    if anchor not in implemented_decode_length:
        fail(f"B28 decode-length anchor missing: {anchor}")
for anchor in (
    "erofs_read_metadata(",
    "erofs_read_physical(",
    "erofs_put_metabuf(&buf);",
    "erofs_brelse(compressed);",
    "free(decoded, M_EROFS);",
    "*bufp = decoded;",
):
    if implemented_read_extent.count(anchor) != function(baseline_zdata, "z_erofs_read_extent").count(anchor):
        fail(f"B28 changed provider or cleanup count: {anchor}")
if not (
    implemented_do_read.index("z_erofs_extent_cache_copy")
    < implemented_do_read.index("z_erofs_decode_length")
    < implemented_do_read.index("z_erofs_read_extent")
    < implemented_do_read.index("cache_eligible && !partial")
    < implemented_do_read.index("z_erofs_extent_cache_publish")
):
    fail("B28 cache/decode/publication order changed")
if "partial, &decoded" not in implemented_do_read:
    fail("B28 does not pass the selected partial mode to extent decode")
if not (
    do_read.index("z_erofs_decode_length")
    < do_read.index("cache_eligible = !partial")
    < do_read.index("z_erofs_decode_extent")
):
    fail("current tree no longer excludes partial decodes from cache admission")
if "partial, &decoded" not in do_read:
    fail("current tree does not pass B28 partial mode to extent decode")

changed = set(
    subprocess.check_output(
        [
            "git",
            "-C",
            str(root),
            "diff-tree",
            "--no-commit-id",
            "--name-only",
            "-r",
            implementation,
        ],
        text=True,
    ).splitlines()
)
expected_write_set = {
    "repo-pre-15/src/compress.h",
    "repo-pre-15/src/decompressor.c",
    "repo-pre-15/src/decompressor_deflate.c",
    "repo-pre-15/src/decompressor_lzma.c",
    "repo-pre-15/src/decompressor_zstd.c",
    "repo-pre-15/src/zdata.c",
    "repo-pre-15/tests/pre15/cases/B28-partial.sh",
    "repo-pre-15/tests/pre15/fixtures/B28-build-kld.sh",
    "repo-pre-15/tests/pre15/fixtures/B28-partial-fixtures.py",
    "repo-pre-15/tests/pre15/fixtures/B28-partial-probe.c",
    "repo-pre-15/tests/pre15/fixtures/B28-partial.json",
}
if changed != expected_write_set:
    fail(f"B28 exact write set differs: {sorted(changed ^ expected_write_set)}")
if any(re.search(r"return\s*\(\s*-E[A-Z0-9_]+", source) for source in sources.values()):
    fail("negative Linux errno entered B28")

harness = r'''#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#define EINTEGRITY 97
#define EOVERFLOW 84
#define EROFS_MAP_PARTIAL_REF 0x20
typedef uint64_t erofs_off_t;
struct erofs_map_blocks {
    uint64_t m_llen;
    unsigned int m_flags;
    unsigned int m_algorithmformat;
};
static bool z_erofs_decompress_supports_subextent(const struct erofs_map_blocks *map)
{
    return map->m_algorithmformat <= 2;
}
''' + decode_length + r'''
static int failures;
static void check(const char *name, struct erofs_map_blocks map, uint64_t off,
    size_t want, int expected_error, size_t expected_length, bool expected_partial)
{
    size_t length = 0;
    bool partial = false;
    int error = z_erofs_decode_length(&map, off, want, &length, &partial);
    if (error != expected_error || (!error &&
        (length != expected_length || partial != expected_partial))) {
        fprintf(stderr, "%s error=%d length=%zu partial=%d\n", name,
            error, length, partial);
        failures++;
    }
}
int main(void)
{
    check("lz4 prefix", (struct erofs_map_blocks){1038906, 0, 0}, 0,
        4096, 0, 4096, true);
    check("lzma middle", (struct erofs_map_blocks){151552, 0, 1}, 8192,
        4096, 0, 12288, true);
    check("deflate cross", (struct erofs_map_blocks){22878, 0, 2}, 3584,
        4096, 0, 7680, true);
    check("zstd prefix", (struct erofs_map_blocks){151552, 0, 3}, 0,
        4096, 0, 151552, false);
    check("lzma tail", (struct erofs_map_blocks){151552, 0, 1}, 147456,
        4096, 0, 151552, false);
    check("zstd partial ref", (struct erofs_map_blocks){151552,
        EROFS_MAP_PARTIAL_REF, 3}, 0, 4096, 0, 4096, true);
    check("shifted fallback", (struct erofs_map_blocks){65536, 0, 4}, 0,
        4096, 0, 65536, false);
    check("bad range", (struct erofs_map_blocks){4096, 0, 0}, 4090,
        16, EINTEGRITY, 0, false);
    return failures != 0;
}
'''
harness_path.write_text(harness, encoding="ascii")
report = {
    "baseline": baseline,
    "batch": "B28",
    "cache_hit_before_decode": True,
    "candidate": "P15-086",
    "descriptor_decisions": descriptor_decisions,
    "frozen_gate_full_fallback": ["zstd"],
    "current_full_fallback": ["shifted", "interlaced", "unknown", "zstd"],
    "current_partial": ["deflate", "lz4", "lzma"],
    "gate": "GO",
    "implementation": implementation,
    "partial_cache_publication": False,
    "partial_reference_preserved": True,
    "persistent_state_added": False,
    "positive_errno": True,
    "provider_and_cleanup_unchanged": True,
    "runtime_base": runtime_base,
    "status": "PASS",
    "test": "TC176-stream-runtime",
    "write_set": sorted(changed),
}
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
print(json.dumps(report, indent=2, sort_keys=True))
PY
then
	pre15_runner_fail 'B28 gate/source/write-set audit failed'
fi

if ! cc -std=c11 -Wall -Wextra -Werror "$PRE15_CASE_TMP/decision-harness.c" \
    -o "$PRE15_CASE_TMP/decision-harness" \
    >"$artifacts/decision-harness-build.stdout" \
    2>"$artifacts/decision-harness-build.stderr" || \
    ! "$PRE15_CASE_TMP/decision-harness" \
    >"$artifacts/decision-harness.stdout" \
    2>"$artifacts/decision-harness.stderr"; then
	pre15_dut_fail 'B28 extracted decode-length harness failed'
fi
cp "$gate_result" "$artifacts/P15-086-result.json"
cp "$gate_capabilities" "$artifacts/P15-086-capabilities.json"
sha256sum "$artifacts/P15-086-result.json" \
    "$artifacts/P15-086-capabilities.json" \
    "$artifacts/B28-fixture-index.json" \
    "$artifacts/B28-source-audit.json" >"$artifacts/SHA256SUMS"
fi
pre15_target_reached

if test "${PRE15_MODE:-host}" != qemu; then
	printf '%s\n' \
		'TC176 B28 host partial-subextent subset PASS' \
		'B28 LZ4/LZMA/Deflate partial policy and Zstd full fallback, cache exclusion, arithmetic, real fixtures, and cleanup PASS' \
		'QEMU runtime NOT_RUN in host mode' \
		'Full feature suite NOT_RUN'
	exit 0
fi

for tool in awk clang scp; do
	command -v "$tool" >/dev/null 2>&1 || \
		pre15_infra_blocked "missing B28 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/B28-fixtures.tar.gz
module=$PRE15_CASE_TMP/B28-erofs-zstdio1.ko
if ! /bin/sh "$kld_builder" "$PRE15_DUT" "$PRE15_FREEBSD_SRC" "$module" \
    "$PRE15_CASE_TMP/kld-work" 1 >"$artifacts/B28-kld-build.stdout" \
    2>"$artifacts/B28-kld-build.stderr"; then
	pre15_dut_fail 'B28 cross-target zstdio1 KLD build failed'
fi
pre15_record_module "$module"
file "$module" >"$artifacts/B28-kld-file.txt"
sha256sum "$module" >"$artifacts/B28-kld-sha256.txt"
nm -u "$module" | LC_ALL=C sort >"$artifacts/B28-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"
}

pre15_guest_ssh_bounded()
{
	timeout -k 5 "${PRE15_GUEST_COMMAND_TIMEOUT:-60}" ssh -n -q \
		-o BatchMode=yes -o StrictHostKeyChecking=no \
		-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
		-o "ControlPath=$PRE15_QEMU_CONTROL_PATH" \
		-p "$PRE15_QEMU_SSH_PORT" \
		"$PRE15_QEMU_SSH_USER@127.0.0.1" "$@"
}

if ! pre15_scp "$fixture_archive" /root/B28-fixtures.tar.gz || \
    ! pre15_scp "$probe" /root/B28-partial-probe.c || \
    ! pre15_scp "$module" /root/B28-erofs-zstdio1.ko; then
	pre15_infra_blocked 'could not transfer B28 module, probe, or fixtures'
fi
if ! pre15_guest_ssh_bounded \
    'rm -rf /root/B28-fixtures && mkdir /root/B28-fixtures && tar -xzf /root/B28-fixtures.tar.gz -C /root/B28-fixtures && cc -O2 -Wall -Wextra -Werror -o /root/B28-partial-probe /root/B28-partial-probe.c'; then
	pre15_infra_blocked 'could not prepare B28 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/B28-erofs-zstdio1.ko \
    >"$artifacts/B28-kldload.stdout" 2>"$artifacts/B28-kldload.stderr"; then
	if grep -q 'module already loaded or in kernel' \
	    "$artifacts/B28-kldload.stderr"; then
		pre15_infra_blocked 'guest kernel already owns the erofs.1 interface'
	fi
	if grep -Eq 'link_elf|symbol|not defined' "$artifacts/B28-kldload.stderr"; then
		pre15_infra_blocked 'guest kernel lacks the planned ZSTDIO provider ABI'
	fi
	pre15_dut_fail 'B28 exact-source zstdio1 KLD failed to load'
fi
pre15_own_guest_kld B28-erofs-zstdio1.ko 'B28 exact-source KLD'

attach_mount()
{
	image=$1
	label=$2
	B28_MD=$(pre15_guest_ssh_bounded mdconfig -a -t vnode \
		-f "/root/B28-fixtures/images/$image") || \
		pre15_dut_fail "$label md attach failed"
	case "$B28_MD" in
	md[0-9]*) ;;
	*) pre15_runner_fail "unexpected B28 md unit: $B28_MD" ;;
	esac
	pre15_own_guest_md "$B28_MD" "$label md"
	B28_MOUNT=/mnt/pre15-b28-$label
	pre15_guest_ssh_bounded mkdir -p "$B28_MOUNT"
	pre15_guest_ssh_bounded mount -t erofs -o ro "/dev/$B28_MD" "$B28_MOUNT" || \
		pre15_dut_fail "$label mount failed"
	pre15_own_guest_mount "$B28_MOUNT" "$label mount"
}

valid_assertions=0
corrupt_range_assertions=0
full_corruption_assertions=0
while IFS='|' read -r codec source logical_offset logical_length decision; do
	reference=/root/B28-fixtures/sources/$source/payload.bin
	attach_mount "$codec-valid.erofs" "$codec-valid"
	for range in '0 4096' '3584 4096' '8192 4096'; do
		set -- $range
		offset=$((logical_offset + $1))
		pre15_guest_ssh_bounded /root/B28-partial-probe range \
		    "$B28_MOUNT/payload.bin" "$reference" "$offset" "$2" || \
			pre15_dut_fail "$codec valid subextent mismatch at $offset"
		valid_assertions=$((valid_assertions + 1))
	done
	tail_offset=$((logical_offset + logical_length - 4096))
	pre15_guest_ssh_bounded /root/B28-partial-probe range \
	    "$B28_MOUNT/payload.bin" "$reference" "$tail_offset" 4096 || \
		pre15_dut_fail "$codec valid tail fallback mismatch"
	valid_assertions=$((valid_assertions + 1))

	attach_mount "$codec-corrupt.erofs" "$codec-corrupt"
	if test "$decision" = GO; then
		pre15_guest_ssh_bounded /root/B28-partial-probe range \
		    "$B28_MOUNT/payload.bin" "$reference" "$logical_offset" 4096 || \
			pre15_dut_fail "$codec range-before-corruption partial decode failed"
	else
		pre15_guest_ssh_bounded /root/B28-partial-probe range-errno \
		    "$B28_MOUNT/payload.bin" "$logical_offset" 4096 97 || \
			pre15_dut_fail "$codec did not use exact full fallback"
	fi
	corrupt_range_assertions=$((corrupt_range_assertions + 1))
	pre15_guest_ssh_bounded /root/B28-partial-probe full-errno \
	    "$B28_MOUNT/payload.bin" 97 || \
		pre15_dut_fail "$codec full corruption did not return EINTEGRITY"
	full_corruption_assertions=$((full_corruption_assertions + 1))
done <<'EOF'
deflate|stream|114326|22878|GO
lz4|lz4|0|1038906|GO
lzma|stream|0|151552|GO
zstd|stream|0|151552|FULL_FALLBACK
EOF
if test "$valid_assertions" -ne 16 || \
    test "$corrupt_range_assertions" -ne 4 || \
    test "$full_corruption_assertions" -ne 4; then
	pre15_runner_fail "B28 assertion count mismatch: valid=$valid_assertions corrupt-range=$corrupt_range_assertions full-corruption=$full_corruption_assertions"
fi

pre15_guest_ssh_bounded dmesg >"$artifacts/B28-dmesg.txt"
printf '%s\n' \
	'TC176 B28 QEMU partial-subextent subset PASS' \
	'valid-ranges=16 partial-before-corruption=4 full-corruption=4 errno=97' \
	'Full feature suite NOT_RUN'
