#!/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=08a567554881f19bcec933a92a46d18cfb73d22e
artifacts=$PRE15_RUN_DIR/artifacts
compact_fixture=$PRE15_DUT/tests/pre15/fixtures/B13-compact-time.json
extended_fixture=$PRE15_DUT/tests/pre15/fixtures/B13-extended-time.json
inode=$PRE15_DUT/src/inode.c
internal=$PRE15_DUT/src/internal.h
super=$PRE15_DUT/src/super.c
ondisk=$PRE15_DUT/src/erofs_fs.h
vnops=$PRE15_DUT/src/erofs_vnops.c

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

pre15_record_fixture b13-compact "$compact_fixture"
pre15_record_fixture b13-extended "$extended_fixture"
pre15_record_fixture b13-inode "$inode"
pre15_record_fixture b13-internal "$internal"
pre15_record_fixture b13-super "$super"
pre15_record_fixture b13-ondisk "$ondisk"
pre15_record_fixture b13-vnops "$vnops"
pre15_record_fixture b13-case "$PRE15_DUT/tests/pre15/cases/B13-time.sh"

mkdir -p "$artifacts"
pre15_target_reached

if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$compact_fixture" \
	"$extended_fixture" "$artifacts" <<'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]
compact_path = Path(sys.argv[4])
extended_path = Path(sys.argv[5])
artifacts = Path(sys.argv[6])
src = dut / "src"
eintegrity = 97
int64_min = -(1 << 63)
int64_max = (1 << 63) - 1
int32_min = -(1 << 31)
int32_max = (1 << 31) - 1


def committed(path: str) -> str:
    completed = subprocess.run(
        ["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/src/{path}"],
        check=False,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    if completed.returncode != 0:
        raise SystemExit(f"cannot read B13 baseline {path}: {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())
    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 load_fixture(path: Path, expected_encoding: set[str]) -> dict:
    fixture = json.loads(path.read_text(encoding="ascii"))
    if fixture.get("schema") != 1 or fixture.get("candidate") != "P15-018":
        raise SystemExit(f"invalid B13 fixture identity: {path.name}")
    if set(fixture.get("encoding", {})) != expected_encoding:
        raise SystemExit(f"invalid B13 fixture encoding keys: {path.name}")
    if not isinstance(fixture.get("cases"), list) or not fixture["cases"]:
        raise SystemExit(f"empty B13 fixture: {path.name}")
    return fixture


def decode_le(raw: str, width: int, *, signed: bool) -> int:
    if not re.fullmatch(r"[0-9a-f]+", raw) or len(raw) != width * 2:
        raise SystemExit(f"invalid {width}-byte lowercase hex value: {raw!r}")
    return int.from_bytes(bytes.fromhex(raw), byteorder="little", signed=signed)


def status_for_range(value: int, minimum: int, maximum: int) -> str:
    return "PASS" if minimum <= value <= maximum else "EINTEGRITY"


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

expected_internal = replace_once(
    base["internal.h"], "\tuint64_t epoch;", "\tint64_t epoch;", "signed epoch field"
)
expected_internal = replace_once(
    expected_internal, "\tuint64_t mtime;", "\ttime_t mtime;", "time_t inode field"
)
if current["internal.h"] != expected_internal:
    raise SystemExit("internal.h differs from the two declared B13 type changes")

old_set_timestamp = """static int
erofs_set_timestamp(struct erofs_inode *vi, uint64_t seconds,
    uint32_t nanoseconds)
{
\tif (nanoseconds >= 1000000000 || seconds > (uint64_t)INT64_MAX)
\t\treturn (EINTEGRITY);
\tvi->mtime = seconds;
\tvi->mtime_nsec = nanoseconds;
\treturn (0);
}
"""
new_set_timestamp = """static int
erofs_set_timestamp(struct erofs_inode *vi, int64_t seconds,
    uint32_t nanoseconds)
{
\ttime_t mtime;

\tif (nanoseconds >= 1000000000 ||
\t    __builtin_add_overflow(seconds, 0, &mtime))
\t\treturn (EINTEGRITY);
\tvi->mtime = mtime;
\tvi->mtime_nsec = nanoseconds;
\treturn (0);
}
"""
expected_inode = replace_once(
    base["inode.c"], old_set_timestamp, new_set_timestamp, "timestamp conversion"
)
expected_inode = replace_once(
    expected_inode,
    "\tuint64_t addrmask, mtime;",
    "\tuint64_t addrmask;\n\tint64_t mtime;",
    "signed decoded mtime",
)
expected_inode = replace_once(
    expected_inode,
    "\t\t    (uint64_t)le32toh(dic->i_mtime), &mtime)",
    "\t\t    (int64_t)le32toh(dic->i_mtime), &mtime)",
    "compact signed checked add",
)
expected_inode = replace_once(
    expected_inode,
    "\t\terror = erofs_set_timestamp(vi, le64toh(die->i_mtime),\n"
    "\t\t    le32toh(die->i_mtime_nsec));",
    "\t\terror = erofs_set_timestamp(vi,\n"
    "\t\t    (int64_t)le64toh(die->i_mtime),\n"
    "\t\t    le32toh(die->i_mtime_nsec));",
    "extended signed decode",
)
if current["inode.c"] != expected_inode:
    raise SystemExit("inode.c differs from the four declared B13 transforms")

expected_super = replace_once(
    base["super.c"],
    "\tsbi->epoch = le64toh(dsb->epoch);",
    "\tsbi->epoch = (int64_t)le64toh(dsb->epoch);",
    "super signed epoch decode",
)
if current["super.c"] != expected_super:
    raise SystemExit("super.c differs from the exact signed epoch transform")
if current["erofs_fs.h"] != base["erofs_fs.h"]:
    raise SystemExit("B13 changed ondisk endian types, widths, or layout")
if current["erofs_vnops.c"] != base["erofs_vnops.c"]:
    raise SystemExit("B13 changed FreeBSD VOP/timespec publication code")

ondisk_required = (
    "\t__le64 epoch;",
    "\t__le32 i_mtime;",
    "\t__le64 i_mtime;",
    "\t__le32 i_mtime_nsec;",
)
for declaration in ondisk_required:
    if current["erofs_fs.h"].count(declaration) != 1:
        raise SystemExit(f"ondisk timestamp declaration changed: {declaration}")

timestamp_helper = extract_function(current["inode.c"], "erofs_set_timestamp")
read_inode = extract_function(current["inode.c"], "erofs_read_inode")
if timestamp_helper.count("__builtin_add_overflow(seconds, 0, &mtime)") != 1:
    raise SystemExit("time_t checked conversion is absent or duplicated")
compact_pattern = re.compile(
    r"__builtin_add_overflow\(sbi->epoch,\s*"
    r"\(int64_t\)le32toh\(dic->i_mtime\), &mtime\)"
)
if len(compact_pattern.findall(read_inode)) != 1:
    raise SystemExit("compact signed checked addition is absent or duplicated")
if read_inode.count("(int64_t)le64toh(die->i_mtime)") != 1:
    raise SystemExit("extended signed seconds decode is absent or duplicated")
if "(uint64_t)le32toh(dic->i_mtime)" in read_inode:
    raise SystemExit("unsigned compact timestamp arithmetic remains")

changed_sources = current["internal.h"] + current["inode.c"] + current["super.c"]
if "ckd_add" in changed_sources or "stdckdint" in changed_sources:
    raise SystemExit("unsupported checked-arithmetic API entered B13")
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", changed_sources):
    raise SystemExit("negative errno entered the FreeBSD timestamp path")

overflow_lines = []
for path in ("inode.c", "super.c"):
    for line_number, line in enumerate(current[path].splitlines(), start=1):
        if "__builtin_" in line and "overflow" in line:
            overflow_lines.append(f"{path}:{line_number}:{line.strip()}")
(artifacts / "B13-overflow-scan.txt").write_text(
    "\n".join(overflow_lines) + "\n", encoding="ascii"
)

compact = load_fixture(
    compact_path, {"epoch", "fixed_nsec", "mtime_delta"}
)
extended = load_fixture(extended_path, {"mtime", "mtime_nsec"})
seen_ids: set[str] = set()
decoded_compact = []
decoded_extended = []

for case in compact["cases"]:
    case_id = case.get("id")
    if not isinstance(case_id, str) or case_id in seen_ids:
        raise SystemExit(f"invalid or duplicate B13 case id: {case_id!r}")
    seen_ids.add(case_id)
    epoch = decode_le(case["epoch_le"], 8, signed=True)
    delta = decode_le(case["mtime_delta_le"], 4, signed=False)
    nanoseconds = decode_le(case["fixed_nsec_le"], 4, signed=False)
    mathematical = epoch + delta
    add_status = status_for_range(mathematical, int64_min, int64_max)
    if add_status != case["expected_add"]:
        raise SystemExit(f"compact add oracle mismatch: {case_id}")
    if add_status == "PASS":
        if mathematical != case["expected_seconds"]:
            raise SystemExit(f"compact seconds oracle mismatch: {case_id}")
        if status_for_range(mathematical, int64_min, int64_max) != case["expected_time64"]:
            raise SystemExit(f"compact time64 oracle mismatch: {case_id}")
        if status_for_range(mathematical, int32_min, int32_max) != case["expected_time32"]:
            raise SystemExit(f"compact time32 oracle mismatch: {case_id}")
        nsec_status = "PASS" if nanoseconds < 1_000_000_000 else "EINTEGRITY"
        if nsec_status != case["expected_nsec"]:
            raise SystemExit(f"compact nsec oracle mismatch: {case_id}")
    elif any(case[key] != "NOT_REACHED" for key in (
        "expected_time32", "expected_time64", "expected_nsec"
    )) or case["expected_seconds"] is not None:
        raise SystemExit(f"compact overflow must stop before conversion: {case_id}")
    decoded_compact.append(
        {
            "id": case_id,
            "epoch": epoch,
            "delta": delta,
            "nanoseconds": nanoseconds,
            "mathematical_seconds": mathematical,
            "add_status": add_status,
        }
    )

for case in extended["cases"]:
    case_id = case.get("id")
    if not isinstance(case_id, str) or case_id in seen_ids:
        raise SystemExit(f"invalid or duplicate B13 case id: {case_id!r}")
    seen_ids.add(case_id)
    seconds = decode_le(case["mtime_le"], 8, signed=True)
    nanoseconds = decode_le(case["mtime_nsec_le"], 4, signed=False)
    if seconds != case["expected_seconds"]:
        raise SystemExit(f"extended signed decode mismatch: {case_id}")
    if status_for_range(seconds, int64_min, int64_max) != case["expected_time64"]:
        raise SystemExit(f"extended time64 oracle mismatch: {case_id}")
    if status_for_range(seconds, int32_min, int32_max) != case["expected_time32"]:
        raise SystemExit(f"extended time32 oracle mismatch: {case_id}")
    nsec_status = "PASS" if nanoseconds < 1_000_000_000 else "EINTEGRITY"
    if nsec_status != case["expected_nsec"]:
        raise SystemExit(f"extended nsec oracle mismatch: {case_id}")
    decoded_extended.append(
        {"id": case_id, "seconds": seconds, "nanoseconds": nanoseconds}
    )

fixture_result = {
    "status": "PASS",
    "compact": decoded_compact,
    "extended": decoded_extended,
}
(artifacts / "B13-decoded-fixtures.json").write_text(
    json.dumps(fixture_result, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
    encoding="ascii",
)


def c_i64(value: int) -> str:
    if value == int64_min:
        return "INT64_MIN"
    if value == int64_max:
        return "INT64_MAX"
    if value < 0:
        return f"(-INT64_C({-value}))"
    return f"INT64_C({value})"


def c_status(value: str) -> str:
    if value == "PASS":
        return "0"
    if value == "EINTEGRITY":
        return "EINTEGRITY"
    raise SystemExit(f"cannot emit non-terminal status: {value}")


program = f'''#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <time.h>

#define EINTEGRITY {eintegrity}

struct erofs_inode {{
\ttime_t mtime;
\tuint32_t mtime_nsec;
}};

{timestamp_helper}

static int
checked_compact(int64_t epoch, uint32_t delta, int64_t *seconds)
{{
\tif (__builtin_add_overflow(epoch, (int64_t)delta, seconds))
\t\treturn (EINTEGRITY);
\treturn (0);
}}

static int
checked_time32(int64_t seconds, int32_t *result)
{{
\treturn (__builtin_add_overflow(seconds, 0, result) ? EINTEGRITY : 0);
}}

static int
checked_time64(int64_t seconds, int64_t *result)
{{
\treturn (__builtin_add_overflow(seconds, 0, result) ? EINTEGRITY : 0);
}}

static int
check_timestamp(const char *id, int64_t seconds, uint32_t nanoseconds,
    int expected_time32, int expected_time64, int expected_nsec)
{{
\tstruct erofs_inode inode = {{ 0 }};
\tint32_t time32;
\tint64_t time64;
\tint error, expected;

\tif (checked_time32(seconds, &time32) != expected_time32 ||
\t    checked_time64(seconds, &time64) != expected_time64) {{
\t\tfprintf(stderr, "%s: representability\\n", id);
\t\treturn (1);
\t}}
\texpected = expected_nsec != 0 ? expected_nsec :
\t    (sizeof(time_t) == sizeof(int32_t) ? expected_time32 : expected_time64);
\terror = erofs_set_timestamp(&inode, seconds, nanoseconds);
\tif (error != expected) {{
\t\tfprintf(stderr, "%s: timestamp errno %d != %d\\n", id, error, expected);
\t\treturn (1);
\t}}
\tif (error == 0 && ((int64_t)inode.mtime != seconds ||
\t    inode.mtime_nsec != nanoseconds)) {{
\t\tfprintf(stderr, "%s: timestamp value\\n", id);
\t\treturn (1);
\t}}
\treturn (0);
}}

int
main(void)
{{
\tint64_t seconds;
\tint error;

\tif (sizeof(time_t) != sizeof(int32_t) && sizeof(time_t) != sizeof(int64_t))
\t\treturn (2);
'''
for case, decoded in zip(compact["cases"], decoded_compact, strict=True):
    epoch = decoded["epoch"]
    delta = decoded["delta"]
    expected_add = c_status(case["expected_add"])
    program += f'''\terror = checked_compact({c_i64(epoch)}, UINT32_C({delta}), &seconds);
\tif (error != {expected_add}) {{
\t\tfprintf(stderr, "{case['id']}: compact add errno\\n");
\t\treturn (1);
\t}}
'''
    if case["expected_add"] == "PASS":
        program += f'''\tif (seconds != {c_i64(case['expected_seconds'])} ||
\t    check_timestamp("{case['id']}", seconds, UINT32_C({decoded['nanoseconds']}),
\t    {c_status(case['expected_time32'])}, {c_status(case['expected_time64'])},
\t    {c_status(case['expected_nsec'])}) != 0)
\t\treturn (1);
'''
for case, decoded in zip(extended["cases"], decoded_extended, strict=True):
    program += f'''\tif (check_timestamp("{case['id']}", {c_i64(decoded['seconds'])},
\t    UINT32_C({decoded['nanoseconds']}), {c_status(case['expected_time32'])},
\t    {c_status(case['expected_time64'])}, {c_status(case['expected_nsec'])}) != 0)
\t\treturn (1);
'''
program += f'''\tprintf("B13 boundary PASS compact={len(compact['cases'])} extended={len(extended['cases'])} time_t=%zu\\n",
\t    sizeof(time_t) * 8);
\treturn (0);
}}
'''

program_path = artifacts / "B13-time-boundary.c"
binary_path = artifacts / "B13-time-boundary"
program_path.write_text(program, encoding="ascii")
compiled = subprocess.run(
    [
        "cc", "-std=c11", "-O2", "-Wall", "-Wextra", "-Werror",
        str(program_path), "-o", str(binary_path),
    ],
    check=False,
    text=True,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
)
(artifacts / "B13-time-boundary.compile.stdout").write_text(
    compiled.stdout, encoding="utf-8"
)
(artifacts / "B13-time-boundary.compile.stderr").write_text(
    compiled.stderr, encoding="utf-8"
)
if compiled.returncode != 0:
    raise SystemExit("B13 boundary extractor compilation failed")
executed = subprocess.run(
    [str(binary_path)],
    check=False,
    text=True,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
)
(artifacts / "B13-time-boundary.stdout").write_text(
    executed.stdout, encoding="utf-8"
)
(artifacts / "B13-time-boundary.stderr").write_text(
    executed.stderr, encoding="utf-8"
)
if executed.returncode != 0:
    raise SystemExit("B13 boundary extractor execution failed")

program32 = program.replace("#include <time.h>\n", "#define time_t int32_t\n", 1)
program32_path = artifacts / "B13-time-boundary32.c"
binary32_path = artifacts / "B13-time-boundary32"
program32_path.write_text(program32, encoding="ascii")
compiled32 = subprocess.run(
    [
        "cc", "-std=c11", "-O2", "-Wall", "-Wextra", "-Werror",
        str(program32_path), "-o", str(binary32_path),
    ],
    check=False,
    text=True,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
)
(artifacts / "B13-time-boundary32.compile.stdout").write_text(
    compiled32.stdout, encoding="utf-8"
)
(artifacts / "B13-time-boundary32.compile.stderr").write_text(
    compiled32.stderr, encoding="utf-8"
)
if compiled32.returncode != 0:
    raise SystemExit("B13 32-bit time_t extractor compilation failed")
executed32 = subprocess.run(
    [str(binary32_path)],
    check=False,
    text=True,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
)
(artifacts / "B13-time-boundary32.stdout").write_text(
    executed32.stdout, encoding="utf-8"
)
(artifacts / "B13-time-boundary32.stderr").write_text(
    executed32.stderr, encoding="utf-8"
)
if executed32.returncode != 0:
    raise SystemExit("B13 32-bit time_t extractor execution failed")

result = {
    "status": "PASS",
    "final_id": "P15-018",
    "baseline": baseline,
    "compact_cases": len(compact["cases"]),
    "extended_cases": len(extended["cases"]),
    "signed_add_overflows": sum(
        case["expected_add"] == "EINTEGRITY" for case in compact["cases"]
    ),
    "negative_seconds_passes": sum(
        case["expected_seconds"] is not None and case["expected_seconds"] < 0
        for case in compact["cases"] + extended["cases"]
    ),
    "time32_lower_rejections": sum(
        case["expected_seconds"] is not None
        and case["expected_seconds"] < int32_min
        and case["expected_time32"] == "EINTEGRITY"
        for case in compact["cases"] + extended["cases"]
    ),
    "time32_upper_rejections": sum(
        case["expected_seconds"] is not None
        and case["expected_seconds"] > int32_max
        and case["expected_time32"] == "EINTEGRITY"
        for case in compact["cases"] + extended["cases"]
    ),
    "nanosecond_rejections": sum(
        case["expected_nsec"] == "EINTEGRITY"
        for case in compact["cases"] + extended["cases"]
    ),
    "ondisk_header_byte_identical": True,
    "ondisk_seconds_width": 64,
    "ondisk_compact_delta_width": 32,
    "time_t_checked_conversion": True,
    "extracted_helper_time_widths": [32, 64],
    "unsupported_ckd_api": False,
    "positive_errno_preserved": True,
    "qemu": "NOT_RUN",
    "full_feature_suite": "NOT_RUN",
}
(artifacts / "B13-result.json").write_text(
    json.dumps(result, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
    encoding="ascii",
)
print(
    f"B13 PASS compact={len(compact['cases'])} extended={len(extended['cases'])} "
    f"signed-overflow={result['signed_add_overflows']}"
)
PY
then
	:
else
	pre15_dut_fail 'B13 signed timestamp source or boundary check failed'
fi

sha256sum "$artifacts/B13-overflow-scan.txt" \
	"$artifacts/B13-decoded-fixtures.json" \
	"$artifacts/B13-time-boundary.c" "$artifacts/B13-time-boundary" \
	"$artifacts/B13-time-boundary32.c" "$artifacts/B13-time-boundary32" \
	"$artifacts/B13-result.json" > "$artifacts/SHA256SUMS"
printf 'B13 PASS signed timestamps and checked time_t boundaries\n'
