]> FriiDump Source - friidump.git/blob - validation/friidump-v0.5.3.16-pf1-candidate21-linux-live-validation.sh
Document exact-release Linux hardware validation
[friidump.git] / validation / friidump-v0.5.3.16-pf1-candidate21-linux-live-validation.sh
1 #!/usr/bin/env bash
2 set -Eeuo pipefail
3
4 phase="${1:-build}"
5 device="${2:-/dev/sr0}"
6 firmware_note="${3:-${FRIIDUMP_FIRMWARE_MODIFIED_NOTE:-}}"
7
8 source_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
9 build_root="$source_root/build-candidate21-linux"
10 expected_version="0.5.3.16-pf1-candidate21"
11 expected_commit="$(
12     python3 - "$source_root/CANDIDATE.json" <<'PYEXPECTED'
13 import json
14 import re
15 import sys
16 from pathlib import Path
17
18 try:
19     value = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))["candidate_implementation"]["commit"]
20 except (OSError, KeyError, TypeError, ValueError):
21     value = ""
22
23 print(value.lower() if re.fullmatch(r"[0-9a-fA-F]{40}", value) else "")
24 PYEXPECTED
25 )"
26
27 if [[ ! "$expected_commit" =~ ^[0-9a-f]{40}$ ]]; then
28     echo "Unable to resolve candidate21 implementation commit from CANDIDATE.json." >&2
29     exit 1
30 fi
31
32 require_command() {
33     local command_name="$1"
34     local package_hint="$2"
35
36     if ! command -v "$command_name" >/dev/null 2>&1; then
37         echo "Missing required command: $command_name" >&2
38         echo "Ubuntu package hint: sudo apt-get install -y $package_hint" >&2
39         exit 1
40     fi
41 }
42
43 require_build_dependencies() {
44     require_command cmake cmake
45     require_command cc build-essential
46     require_command python3 python3
47
48     if ! python3 -c 'import jsonschema' >/dev/null 2>&1; then
49         echo "Missing Python module: jsonschema" >&2
50         echo "Ubuntu package hint: sudo apt-get install -y python3-jsonschema" >&2
51         exit 1
52     fi
53 }
54
55 resolve_commit() {
56     local git_root git_commit
57
58     git_root="$(git -C "$source_root" rev-parse --show-toplevel 2>/dev/null || true)"
59     if [[ -n "$git_root" && "$(cd -- "$git_root" && pwd -P)" == "$(cd -- "$source_root" && pwd -P)" ]]; then
60         git_commit="$(git -C "$source_root" rev-parse HEAD 2>/dev/null || true)"
61         if [[ ! "$git_commit" =~ ^[0-9a-fA-F]{40}$ ]]; then
62             echo "Unable to resolve HEAD from the source-root Git repository." >&2
63             exit 1
64         fi
65         if [[ "${git_commit,,}" != "$expected_commit" ]]; then
66             echo "Candidate implementation commit mismatch." >&2
67             echo "Expected: $expected_commit" >&2
68             echo "Actual:   ${git_commit,,}" >&2
69             exit 1
70         fi
71     fi
72
73     printf '%s\n' "$expected_commit"
74 }
75
76 validate_report_directory() {
77     local report_root="$1"
78
79     python3 "$source_root/tests/validate_native_reports.py" \
80         --fixtures "$report_root" \
81         --schema "$source_root/tests/contracts/friidump-test-result.v1.schema.json" \
82         --allow-arbitrary
83 }
84
85 normalize_future_source_timestamps() {
86     python3 - "$source_root" <<'PYTIMESTAMPS'
87 import os
88 import sys
89 import time
90 from pathlib import Path
91
92 root = Path(sys.argv[1])
93 now = time.time()
94 normalized = 0
95
96 for path in root.rglob("*"):
97     if not path.is_file():
98         continue
99     stat = path.stat()
100     if stat.st_mtime > now + 1.0:
101         os.utime(path, (stat.st_atime, now))
102         normalized += 1
103
104 print(f"Future source timestamp preflight: PASS (normalized={normalized})")
105 PYTIMESTAMPS
106 }
107
108 build_phase() {
109     local commit fixtures
110
111     require_build_dependencies
112     commit="$(resolve_commit)"
113     normalize_future_source_timestamps
114
115     rm -rf -- "$build_root"
116
117     cmake -S "$source_root" -B "$build_root" \
118         -DBUILD_STATIC_BINARY=ON \
119         -DBUILD_NATIVE_REPORT_TESTS=ON \
120         -DFRIIDUMP_BUILD_COMMIT="$commit"
121
122     cmake --build "$build_root" --parallel
123
124     "$build_root/src/friidump-xbox-portable-metadata-test"
125     python3 "$source_root/tests/test_linux_xbox_portable_contract.py"
126     python3 "$source_root/tests/test_seed_diagnostics_contract.py"
127     "$build_root/src/friidump-linux-rawio-test"
128     python3 "$source_root/tests/test_linux_rawio_contract.py"
129     python3 "$source_root/tests/test_xbox_dvd_structure_contract.py" --source-root "$source_root"
130
131     fixtures="$(mktemp -d)"
132     trap 'rm -rf -- "$fixtures"' RETURN
133
134     "$build_root/src/friidump-report-fixtures" "$fixtures"
135     validate_report_directory "$fixtures"
136
137     test -f "$build_root/src/redump_dat/Nintendo - GameCube.dat"
138     test -f "$build_root/src/redump_dat/Nintendo - Wii.dat"
139     test -f "$build_root/src/redump_dat/Microsoft - Xbox.dat"
140
141     "$build_root/src/friidump-redump-dat-resolver-test" \
142         - \
143         "$build_root/src" \
144         "Nintendo - GameCube.dat" \
145         "$build_root/src/redump_dat/Nintendo - GameCube.dat" \
146         1
147
148     "$build_root/src/friidump-redump-dat-verify-test" \
149         "$build_root/src/redump_dat/Nintendo - GameCube.dat"
150
151     resolver_tmp="$(mktemp -d)"
152     mkdir -p "$resolver_tmp/prefix/bin" \
153              "$resolver_tmp/prefix/share/friidump/redump_dat" \
154              "$resolver_tmp/env-dat" \
155              "$resolver_tmp/explicit-dat"
156     printf 'installed\n' > "$resolver_tmp/prefix/share/friidump/redump_dat/Test.dat"
157     printf 'environment\n' > "$resolver_tmp/env-dat/Test.dat"
158     printf 'explicit\n' > "$resolver_tmp/explicit-dat/Test.dat"
159
160     "$build_root/src/friidump-redump-dat-resolver-test" \
161         - \
162         "$resolver_tmp/prefix/bin" \
163         Test.dat \
164         "$resolver_tmp/prefix/bin/../share/friidump/redump_dat/Test.dat" \
165         1
166
167     FRIIDUMP_REDUMP_DAT_DIR="$resolver_tmp/env-dat" \
168         "$build_root/src/friidump-redump-dat-resolver-test" \
169             - \
170             "$resolver_tmp/prefix/bin" \
171             Test.dat \
172             "$resolver_tmp/env-dat/Test.dat" \
173             1
174
175     FRIIDUMP_REDUMP_DAT_DIR="$resolver_tmp/env-dat" \
176         "$build_root/src/friidump-redump-dat-resolver-test" \
177             "$resolver_tmp/explicit-dat" \
178             "$resolver_tmp/prefix/bin" \
179             Test.dat \
180             "$resolver_tmp/explicit-dat/Test.dat" \
181             1
182
183
184     rm -rf -- "$resolver_tmp"
185
186     set +e
187     help_output="$("$build_root/src/friidump" --help 2>&1)"
188     help_status=$?
189     set -e
190
191     if test "$help_status" -ne 1; then
192         echo "Unexpected FriiDump --help exit status: $help_status (expected 1)." >&2
193         exit 1
194     fi
195
196     grep -F "FriiDump $expected_version" <<<"$help_output"
197     grep -F "FRIIDUMP_REDUMP_DAT_DIR" <<<"$help_output"
198     grep -F "Linux raw-I/O requirement" <<<"$help_output"
199
200     echo
201     echo "WARNING: this Linux build does not yet carry CAP_SYS_RAWIO."
202     echo "Vendor-command hardware operations will be refused until the exact"
203     echo "validated executable receives cap_sys_rawio=ep. Do not run FriiDump"
204     echo "itself as root. Rebuilding or replacing the executable clears the capability."
205     echo "Install and verify with:"
206     echo "  bash $source_root/validation/friidump-v0.5.3.16-pf1-candidate21-linux-live-validation.sh capability"
207     echo
208     echo "FRIIDUMP CANDIDATE21 LINUX BUILD: PASS"
209 }
210
211 capability_phase() {
212     require_command getcap libcap2-bin
213     require_command setcap libcap2-bin
214     require_command sudo sudo
215     ensure_build
216
217     bash "$source_root/validation/friidump-linux-rawio-capability.sh"         install         "$build_root/src/friidump"
218
219     bash "$source_root/validation/friidump-linux-rawio-capability.sh"         status         "$build_root/src/friidump"
220
221     echo "FRIIDUMP CANDIDATE21 LINUX CAPABILITY: PASS"
222 }
223
224 ensure_build() {
225     if ! test -x "$build_root/src/friidump"; then
226         build_phase
227     fi
228 }
229
230 device_model() {
231     local block_name model_path
232
233     block_name="$(basename -- "$device")"
234     model_path="/sys/class/block/$block_name/device/model"
235
236     if test -r "$model_path"; then
237         tr -d '\000' < "$model_path" | xargs
238     else
239         printf '%s\n' ""
240     fi
241 }
242
243 no_media_phase() {
244     local output_root report_path output_path exit_code
245
246     require_build_dependencies
247     ensure_build
248
249     output_root="$source_root/_native-report-linux-validation/$(date -u +%Y%m%dT%H%M%SZ)-no-media"
250     mkdir -p "$output_root"
251
252     echo "Remove media from $device, close the tray, wait for the drive to settle, then press Enter."
253     read -r
254
255     output_path="$output_root/no-media-placeholder.iso"
256     report_path="$output_root/no-media-placeholder.iso.friidump.json"
257
258     set +e
259     (
260         cd "$output_root"
261         "$build_root/src/friidump" \
262             -d "$device" \
263             -i "no-media-placeholder.iso"
264     )
265     exit_code=$?
266     set -e
267
268     test -f "$report_path" || {
269         echo "FAIL: expected native report was not created: $report_path" >&2
270         exit 1
271     }
272
273     validate_report_directory "$output_root"
274
275     python3 - "$report_path" "$device" "$expected_version" "$expected_commit" <<'PYREPORT'
276 import json
277 import sys
278 from pathlib import Path
279
280 report_path = Path(sys.argv[1])
281 expected_device = sys.argv[2]
282 expected_version = sys.argv[3]
283 expected_commit = sys.argv[4]
284
285 report = json.loads(report_path.read_text(encoding="utf-8"))
286
287 checks = {
288     "schema": report.get("schema") == "friidump-test-result.v1",
289     "version": report.get("generator", {}).get("version") == expected_version,
290     "commit": report.get("generator", {}).get("build_commit") == expected_commit,
291     "device": report.get("drive", {}).get("device_path") == expected_device,
292     "stock_firmware": report.get("drive", {}).get("firmware_modified") is False,
293     "no_modification_note": report.get("drive", {}).get("modification_note") is None,
294     "test_type": report.get("run", {}).get("test_type") == "diagnostic_no_media",
295     "run_result": report.get("run", {}).get("result") == "not_applicable",
296     "unknown_platform": report.get("media", {}).get("platform") == "unknown",
297     "seed_not_attempted": report.get("seed", {}).get("attempted") is False,
298     "dump_not_attempted": report.get("dump", {}).get("attempted") is False,
299     "no_output_path": report.get("dump", {}).get("output_path") is None,
300     "no_hashes": all(value is None for value in report.get("hashes", {}).values()),
301     "no_artifacts": report.get("artifacts") == [],
302 }
303
304 failed = [name for name, ok in checks.items() if not ok]
305 for name, ok in checks.items():
306     print(f"{'PASS' if ok else 'FAIL'}: {name}")
307
308 if failed:
309     raise SystemExit("No-media report contract failed: " + ", ".join(failed))
310 PYREPORT
311
312     test ! -e "$output_path" || {
313         echo "FAIL: no-media validation unexpectedly created an image: $output_path" >&2
314         exit 1
315     }
316
317     echo "FriiDump exit code: $exit_code (nonzero is expected for no media)"
318     echo "Report: $report_path"
319     echo "FRIIDUMP CANDIDATE21 LINUX NO-MEDIA: PASS"
320 }
321
322 xbox_cancel_phase() {
323     local model output_root
324
325     require_build_dependencies
326     ensure_build
327
328     model="$(device_model)"
329     if [[ "$model" != *GDR8050L* && "$model" != *GDR-8050L* ]]; then
330         echo "Refusing Xbox cancellation validation on this device." >&2
331         echo "Expected a GDR-8050L inquiry identity; found: ${model:-unknown}" >&2
332         exit 1
333     fi
334
335     if [[ -z "$firmware_note" ]]; then
336         echo "Xbox cancellation validation requires an explicit modified-firmware note." >&2
337         echo "Pass it as argument 3 or set FRIIDUMP_FIRMWARE_MODIFIED_NOTE." >&2
338         exit 1
339     fi
340
341     output_root="$source_root/_native-report-linux-validation/$(date -u +%Y%m%dT%H%M%SZ)-xbox-cancel"
342     mkdir -p "$output_root"
343
344     echo "Insert the Xbox disc in $device. Press Ctrl+C once after XISO progress begins."
345
346     (
347         cd "$output_root"
348         "$build_root/src/friidump" \
349             -d "$device" \
350             -T 4 \
351             -X "Red Faction II [TQ00501A].xiso" \
352             --firmware-modified "$firmware_note"
353     ) || true
354
355     echo "Inspect the generated partial XISO, .friidump.json, and log under: $output_root"
356 }
357
358 identify_phase() {
359     local block_name
360
361     block_name="$(basename -- "$device")"
362
363     echo "Device: $device"
364     for field in vendor model rev; do
365         printf '%-8s ' "$field:"
366         cat "/sys/class/block/$block_name/device/$field" 2>/dev/null || echo "unavailable"
367     done
368
369     if command -v sg_inq >/dev/null 2>&1; then
370         echo
371         sg_inq "$device"
372     else
373         echo
374         echo "Optional detailed inquiry: sudo apt-get install -y sg3-utils"
375     fi
376
377     echo
378     stat -c 'Access: %A  Owner: %U  Group: %G' "$device"
379     id
380
381     if test -x "$build_root/src/friidump"; then
382         echo
383         bash "$source_root/validation/friidump-linux-rawio-capability.sh"             status             "$build_root/src/friidump" || true
384     fi
385 }
386
387 case "$phase" in
388     build) build_phase ;;
389     capability) capability_phase ;;
390     identify) identify_phase ;;
391     no-media) no_media_phase ;;
392     xbox-cancel) xbox_cancel_phase ;;
393     *)
394         echo "Usage: $0 {build|capability|identify|no-media|xbox-cancel} [/dev/sr0] [firmware-note]" >&2
395         exit 2
396         ;;
397 esac