Files
erofs-freebsd-out-tree/tests/pre15/cases/B13-time.sh
T
2026-08-18 09:20:44 +02:00

605 lines
21 KiB
Bash
Executable File

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