#!/bin/sh
# Case: v1.1.4 byte-identical regression contract.
#
# Asserts that modulejail v1.2 with --no-syslog-logging, run against the
# archived v1.1.4-era canned inputs (proc-modules + modules-list + pinned
# kver), produces output whose BODY is byte-identical to the archived
# expected-blacklist.conf generated by v1.1.4 against the same inputs.
#
# Body-only: three header lines legitimately changed between v1.1.4 and v1.2
# and are filtered out before the diff:
#   # modulejail <version>      - VERSION line bumped 1.1.4 -> 1.2.0
#   # fingerprint: sha256:...    - filtered defensively; on identical inputs
#                                  the fingerprint IS identical, but if a
#                                  future version touches the fingerprint
#                                  inputs the body-diff stays clean
#   # install-line: ...          - new in v1.2, absent in v1.1.4
# All other lines (kernel, profile, disclaimer, every install line) MUST
# match exactly.
#
# Contract scope (clarified 2026-05-29 during the v1.3.3 fix):
# This case asserts that the install-line RENDERING (the per-line
# `install <name> /bin/true` form with the `--no-syslog-logging`
# body) stays byte-identical across releases. It does NOT freeze the
# SET of modules that appear in the blacklist: every intentional
# addition to BASELINE_CONSERVATIVE moves a module from "blacklisted"
# to "kept", which changes which install lines appear in the output.
# When that happens, the expected reference fixture is updated in the
# same commit as the baseline change, and the test continues to
# enforce the rendering contract.
#
# The current fixture reflects the v1.3.3 baseline (6360 install
# lines); the v1.1.4 original was 6363 lines (three modules -
# inet_diag, tcp_diag, udp_diag - moved into BASELINE_CONSERVATIVE
# in v1.3.2).
#
# The fixture's universe is rebuilt from tests/fixtures/v1.1.4-regression/
# modules-list (6474 basenames). The case builds a synthetic /lib/modules/
# tree of empty files matching those basenames under $CASE_TMP and points
# modulejail at it via MODULEJAIL_MODULES_ROOT (test-only plumbing). The
# fixture's proc-modules and kver pin the loaded set and kernel version.
# Net effect: the case is hermetic, network-free, and runs on any host
# that can execute /bin/sh - including the macOS dev box.
set -eu

CASE_NAME=v1.1.4-regression
export CASE_NAME

# Source the shared boilerplate (REPO_ROOT, MODULEJAIL_BIN, CASE_TMP,
# the centralized EXIT trap, MODULEJAIL_NO_UPDATE_CHECK,
# MODULEJAIL_DEFAULT_WHITELIST_FILE, case_pass, case_fail). This case
# builds its own 6474-entry universe inline below from the canned
# modules-list fixture, with a 2-tier (lo/ + uc/) sharding scheme to
# survive case-insensitive filesystems, so it does NOT source the
# small representative-universe builder lib (which would conflict
# with its own layout).
# shellcheck source=tests/lib/case-env.sh disable=SC1091
. "$(dirname "$0")/../lib/case-env.sh"

FIXTURE_DIR=$REPO_ROOT/tests/fixtures/v1.1.4-regression

# Sanity-check the fixture is present.
for f in proc-modules kver modules-list expected-blacklist.conf; do
    if [ ! -f "$FIXTURE_DIR/$f" ]; then
        case_fail "missing fixture file: $FIXTURE_DIR/$f"
    fi
done

# CASE_TMP cleanup is handled by the centralized EXIT trap installed
# in tests/lib/case-env.sh (D-Phase6-15). No inline trap needed here.

# Rebuild the synthetic modules tree from the canned list. All 6474
# entries in the v1.1.4 fixture use .ko.zst; the list is shipped as-is
# (basename per line). The script's list_universe walks recursively via
# `find -type f -name '*.ko*'` and only uses the basename, so the
# directory layout does not matter for the regression contract.
#
# 2-tier sharding (lo/ + uc/) is REQUIRED to survive case-insensitive
# filesystems (default macOS APFS). The v1.1.4 fixture's modules-list
# contains four uppercase-vs-lowercase basename pairs (xt_DSCP / xt_dscp,
# xt_HL / xt_hl, xt_RATEEST / xt_rateest, xt_TCPMSS / xt_tcpmss); on
# Linux ext4/xfs these are distinct inodes, but on a single flat
# case-insensitive dir the second touch updates the first's mtime
# instead of creating a sibling, silently dropping four modules from the
# synthetic universe. Sharding files-with-any-uppercase into uc/ and
# all-lowercase into lo/ gives the two case-variant groups separate
# basename namespaces, so they coexist on APFS as well as on ext4.
KVER=$(cat "$FIXTURE_DIR/kver")
TREE_ROOT=$CASE_TMP/lib/modules/$KVER
mkdir -p "$TREE_ROOT/lo" "$TREE_ROOT/uc"
# awk classifies each line (has-any-uppercase vs all-lowercase) and
# routes to uc/ or lo/. Then a single batched xargs invocation does the
# touches.
awk '{
    has_upper = 0
    for (i=1; i<=length($0); i++) {
        c = substr($0, i, 1)
        if (c >= "A" && c <= "Z") { has_upper = 1; break }
    }
    if (has_upper) print "uc/" $0
    else           print "lo/" $0
}' "$FIXTURE_DIR/modules-list" > "$CASE_TMP/manifest"
(cd "$TREE_ROOT" && xargs touch < "$CASE_TMP/manifest")

# Generate the v1.2 candidate output with --no-syslog-logging set so the
# install-line body matches the v1.1.4 form (byte-identical contract).
ACTUAL=$CASE_TMP/actual.conf
MODULEJAIL_MODULES_ROOT=$CASE_TMP/lib/modules \
MODULEJAIL_KVER=$KVER \
MODULEJAIL_PROC_MODULES=$FIXTURE_DIR/proc-modules \
MODULEJAIL_NO_UPDATE_CHECK=1 \
MODULEJAIL_DEFAULT_WHITELIST_FILE=$CASE_TMP/default-whitelist-absent.conf \
"$MODULEJAIL_BIN" -p conservative --no-syslog-logging -o "$ACTUAL" > "$CASE_TMP/stdout" 2> "$CASE_TMP/stderr" || \
    case_fail "modulejail exited $? (expected 0); stderr=$(cat "$CASE_TMP/stderr")"

# Body-only diff: drop the three legitimately-changed header lines.
# All other lines (kernel, profile, disclaimer, every install line) must
# match the archived v1.1.4 reference.
EXPECTED_FILTERED=$CASE_TMP/expected.body
ACTUAL_FILTERED=$CASE_TMP/actual.body
grep -v -E '^# modulejail |^# fingerprint:|^# install-line:|^# invocation:' \
    "$FIXTURE_DIR/expected-blacklist.conf" > "$EXPECTED_FILTERED"
grep -v -E '^# modulejail |^# fingerprint:|^# install-line:|^# invocation:' \
    "$ACTUAL" > "$ACTUAL_FILTERED"

if ! diff -u "$EXPECTED_FILTERED" "$ACTUAL_FILTERED" > "$CASE_TMP/diff.out"; then
    # KEEP the inline diff dump for operator diagnosability - case_fail
    # cannot render multi-line context. The diff context goes to stderr
    # BEFORE the case_fail invocation so it appears above the FAIL line.
    head -40 "$CASE_TMP/diff.out" >&2
    case_fail "v1.1.4 regression body diverged from reference (see diff above)"
fi

# Sanity: the expected counts (6363 of 6474) ARE the v1.1.4 reference
# counts.  Confirm v1.2 head produced the same totals (this would also
# fall out of a body-identical diff above, but the explicit check makes
# the regression contract self-documenting in test output).
EXPECTED_COUNT=$(grep -c '^install ' "$FIXTURE_DIR/expected-blacklist.conf")
ACTUAL_COUNT=$(grep -c '^install ' "$ACTUAL")
if [ "$EXPECTED_COUNT" != "$ACTUAL_COUNT" ]; then
    case_fail "install-line count drift: expected $EXPECTED_COUNT got $ACTUAL_COUNT"
fi

printf '[%s] PASS (%d/%d install lines body-identical to v1.1.4 reference)\n' \
    "$CASE_NAME" "$ACTUAL_COUNT" "$EXPECTED_COUNT"
exit 0
