From: jtryba Date: Fri, 24 Jul 2026 23:27:40 +0000 (-0500) Subject: FriiDump 0.5.3.16 candidate: add native reports and Linux Xbox support X-Git-Tag: v0.5.3.16-pf1~1 X-Git-Url: https://git.jtryba.com/browse/friidump.git/commitdiff_plain/29417fff9ebac65b1cf60ea8951c687696302c55?ds=sidebyside;hp=de0c1a3eccb6f04bbfd23436b592fe7fc501fcf2 FriiDump 0.5.3.16 candidate: add native reports and Linux Xbox support --- diff --git a/CMakeLists.txt b/CMakeLists.txt index b985f1b..635300d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,17 @@ cmake_minimum_required(VERSION 2.8) # to the root binary directory of the project as ${HELLO_BINARY_DIR}. project (FriiDump) +if (UNIX AND NOT WIN32) + message (WARNING + "Linux vendor-command paths require CAP_SYS_RAWIO. " + "A normal build does not install this capability. " + "After validating the exact executable, run the candidate validation " + "script's capability phase or validation/friidump-linux-rawio-capability.sh install. " + "Do not run the entire FriiDump process as root; rebuilding or replacing " + "the executable clears file capabilities." + ) +endif () + if (MSVC) # msvc2005 deprecated warnings add_definitions (-D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE) @@ -67,6 +78,26 @@ option ( OFF ) +option ( + BUILD_NATIVE_REPORT_TESTS + "Build the native-report fixture generator" + OFF +) + +set ( + FRIIDUMP_BUILD_COMMIT + "" + CACHE STRING + "Full 40-character Git commit embedded in native compatibility reports" +) + +if (FRIIDUMP_BUILD_COMMIT) + string (LENGTH "${FRIIDUMP_BUILD_COMMIT}" FRIIDUMP_BUILD_COMMIT_LENGTH) + if (NOT FRIIDUMP_BUILD_COMMIT_LENGTH EQUAL 40 OR FRIIDUMP_BUILD_COMMIT MATCHES "[^0-9A-Fa-f]") + message (FATAL_ERROR "FRIIDUMP_BUILD_COMMIT must be exactly 40 hexadecimal characters") + endif () +endif () + if (BUILD_STATIC_BINARY) set (libmultihash_type STATIC) set (libfriidump_type STATIC) diff --git a/README b/README index a481527..a983057 100644 --- a/README +++ b/README @@ -7,6 +7,16 @@ FF 08 01 vendor lock-state path, redump-style metadata output, optional XISO output, and a convenient forced-DVD mode (`-D` / `--dvd`). See docs/XBOX.md for Xbox-specific details. +FriiDump 0.5.3.16-pf1 also creates native compatibility evidence reports +by default for normal dump invocations using the `friidump-test-result.v1` +contract. The default report is written beside the final FriiDump log and +derives its filename from that log. `--report-json` overrides the pathname, +`--report-dir` overrides the destination directory, and +`--firmware-modified ` records modified firmware; omission assumes +stock firmware. Reports are atomic, preserve exact live drive/firmware +identity, and distinguish complete, partial, failed, cancelled, diagnostic, +and resumed runs. See `docs/NATIVE_REPORTS.md`. + FriiDump is a program that lets you dump Nintendo Wii and GameCube disc from your computer, without using original Nintendo hardware. It basically performs the same functions as the famous "RawDump" program, but with a big difference, @@ -176,6 +186,11 @@ or a command-prompt under Windows. The basic usage is as follows: friidump -d -a +A native compatibility report is created automatically for the same run. +To place it in a different directory: + +friidump -d -a --report-dir reports + For a plain DVD-ROM dump from any readable drive, use `-D` / `--dvd` or the numeric equivalent `-T 3`: diff --git a/README.md b/README.md index 9853ea5..27c81d7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,46 @@ # FriiDump — Project Frankenstein Branch +## Candidate21 Linux Xbox DMI boundary correction + +Candidate20 proved the complete Linux GDR-8050L Xbox command and data path: +direct SG_IO challenge delivery, both game-view transitions, XDVDFS metadata, +drive-captured game lead-in, partial GAME-XDVDFS output, controlled +cancellation, hashes, native reporting, and STOP UNIT. + +Candidate21 preserves that exact Windows-sequence control flow and SG_IO +handshake. It corrects one remaining MMC boundary: standard READ DVD STRUCTURE +uses CDB byte 7 for the Format field. Candidate20 incorrectly wrote the format +to byte 11 (Control), so DMI format `0x04` was not captured and the media ID +remained empty. + +Candidate21 also adds persistent PFI/DMI capture diagnostics and a source +contract that compares the portable request against the proven copied Windows +`GetMediaID()` request. + +The generic Xbox path still contains no RecoveryKick, generic media-auth kick, +or synthetic LBA-zero recovery sequence. + +Linux vendor-command work continues to require a normal-user process with +`cap_sys_rawio=ep` on the exact validated executable. Rebuilding or replacing +the executable clears that capability. + +See [`docs/LINUX.md`](docs/LINUX.md) before Linux hardware use. + +## Candidate11 Linux Xbox parity baseline + +Candidate11 begins the Linux parity campaign without replacing the validated +Windows/MSVC GDR-8050L path. The Windows copied-reference implementation remains +unchanged for challenge drives. Linux now uses the existing portable MMC/Xbox +dump path with a shared XDFS/XBE metadata reader, XBE-derived default filenames, +exact native-report result handoff, partial/full hashes, and consistent Xbox +title, media ID, and region reporting. + +Candidate11 subsequently completed real Linux GameCube validation on a +GCC-4244 B101, including an exact full-dump hash match. Candidate12 retains that +transport and dump behavior while correcting resource discovery and evidence +wording. The development-only XGD1 layout and raw-ID probes remain separate from +ordinary Linux ISO/XISO user paths. + FriiDump is a command-line optical-disc dumping tool for Nintendo GameCube, Nintendo Wii, standard DVD-ROM, and selected Original Xbox/XGD workflows. This repository preserves the Project Frankenstein development branch based on @@ -12,6 +53,7 @@ The branch extends the original FriiDump code with: - redump-style Original Xbox/XGD1 reconstruction and metadata; - optional game-partition XISO output; - media preflight, profile reporting, and STOP UNIT cleanup; +- optional atomic `friidump-test-result.v1` compatibility evidence reports; - validated Windows/MSVC and Linux/CMake build paths. The original detailed user documentation remains in [`README`](README). @@ -74,9 +116,16 @@ cmake --build build ./build/src/friidump --help ``` -The canonical repository baseline was locally compiled with GCC using this path. The -build completed successfully. One inherited warning remains in -`libfriidump/rs.c` concerning a right-shift count; it is recorded as technical +The build prints a warning that Linux vendor-command hardware paths require a +separate, explicit `CAP_SYS_RAWIO` installation step. After validating the +binary: + +```bash +bash ./validation/friidump-linux-rawio-capability.sh install ./build/src/friidump +``` + +Run FriiDump as the normal user, not with `sudo`. One inherited warning remains +in `libfriidump/rs.c` concerning a right-shift count; it is recorded as technical debt and was not silently changed during repository import. ## Common commands @@ -111,8 +160,16 @@ Original Xbox/XGD game-partition XISO: friidump -d -T 4 -X xbox.xiso ``` -Consult [`README`](README), [`docs/options`](docs/options), and -[`docs/XBOX.md`](docs/XBOX.md) before hardware use. +Native compatibility reports are created automatically in the working +directory using the log filename. To override the destination directory: + +```text +friidump -d -8 -s -i game.iso --report-dir reports +``` + +Consult [`README`](README), [`docs/options`](docs/options), +[`docs/XBOX.md`](docs/XBOX.md), and +[`docs/NATIVE_REPORTS.md`](docs/NATIVE_REPORTS.md) before hardware use. ## Safety and evidence rules @@ -152,3 +209,12 @@ CMakeLists.txt portable build entry point FriiDump is distributed under the GNU General Public License, version 2 or later. See [`COPYING`](COPYING). Existing source-file copyright notices and authors are preserved. + +## Candidate21 Xbox Linux scope + +Candidate21 preserves Candidate20's successful SG_IO Xbox handshake and +Windows state-machine order. The only functional correction is the standard +READ DVD STRUCTURE Format byte used for PFI/DMI capture. The mode-select-6 +sticky-descrambling command remains evidence-bearing and non-authoritative: +final READ CAPACITY plus XDVDFS readability determine whether game view was +established. diff --git a/build_msvc32.cmd b/build_msvc32.cmd index 0d69211..e2a3d1d 100644 --- a/build_msvc32.cmd +++ b/build_msvc32.cmd @@ -16,17 +16,40 @@ if not errorlevel 1 ( exit /b 1 ) - if not exist "build-msvc32" mkdir "build-msvc32" -call "%~dp0.vscode\msvc32.cmd" cl @"%~dp0msvc32_friidump.rsp" +set "FRIIDUMP_BUILD_COMMIT_EFFECTIVE=%FRIIDUMP_BUILD_COMMIT%" +if not defined FRIIDUMP_BUILD_COMMIT_EFFECTIVE if exist "%~dp0CANDIDATE.json" ( + for /f "usebackq delims=" %%G in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "$d = Get-Content -Raw -LiteralPath '%~dp0CANDIDATE.json' | ConvertFrom-Json; $d.candidate_implementation.commit"`) do set "FRIIDUMP_BUILD_COMMIT_EFFECTIVE=%%G" +) +if not defined FRIIDUMP_BUILD_COMMIT_EFFECTIVE ( + for /f "usebackq delims=" %%G in (`git -C "%~dp0" rev-parse HEAD 2^>nul`) do set "FRIIDUMP_BUILD_COMMIT_EFFECTIVE=%%G" +) + +if defined FRIIDUMP_BUILD_COMMIT_EFFECTIVE ( + powershell -NoProfile -ExecutionPolicy Bypass -Command "if ($env:FRIIDUMP_BUILD_COMMIT_EFFECTIVE -notmatch '^[0-9a-fA-F]{40}$') { Write-Error 'FRIIDUMP_BUILD_COMMIT must be exactly 40 hexadecimal characters.'; exit 1 }" + if errorlevel 1 exit /b %ERRORLEVEL% + echo [INFO] Native-report build commit: %FRIIDUMP_BUILD_COMMIT_EFFECTIVE% +) else ( + echo [INFO] Native-report build commit: not embedded ^(set FRIIDUMP_BUILD_COMMIT to a full 40-character commit^) +) + +set "EFFECTIVE_RSP=%~dp0build-msvc32\friidump-effective.rsp" +copy /y "%~dp0msvc32_friidump.rsp" "%EFFECTIVE_RSP%" >nul +if errorlevel 1 exit /b %ERRORLEVEL% +if defined FRIIDUMP_BUILD_COMMIT_EFFECTIVE >>"%EFFECTIVE_RSP%" echo /DFRIIDUMP_BUILD_COMMIT=\"%FRIIDUMP_BUILD_COMMIT_EFFECTIVE%\" + +call "%~dp0.vscode\msvc32.cmd" cl @"%EFFECTIVE_RSP%" /link ole32.lib +if errorlevel 1 exit /b %ERRORLEVEL% + +call "%~dp0build_msvc32_xbox_portable_tests.cmd" if errorlevel 1 exit /b %ERRORLEVEL% set "HELP_LOG=%TEMP%\friidump-help-%RANDOM%-%RANDOM%.txt" "%~dp0friidump.exe" --help > "%HELP_LOG%" 2>&1 type "%HELP_LOG%" -findstr /c:"FriiDump " "%HELP_LOG%" >nul +findstr /c:"FriiDump 0.5.3.16-pf1-candidate21" "%HELP_LOG%" >nul if errorlevel 1 ( echo [ERROR] FriiDump help/version smoke test failed. del /q "%HELP_LOG%" >nul 2>nul @@ -34,22 +57,36 @@ if errorlevel 1 ( exit /b 1 ) -findstr /c:"Available command line options:" "%HELP_LOG%" >nul -if errorlevel 1 ( - echo [ERROR] FriiDump command-line help smoke test failed. - del /q "%HELP_LOG%" >nul 2>nul - if exist "friidump.log" del /q "friidump.log" - exit /b 1 +for %%T in ("--report-json" "--report-dir" "--firmware-modified") do ( + findstr /l /c:"%%~T" "%HELP_LOG%" >nul + if errorlevel 1 ( + echo [ERROR] Native-report option %%~T is missing from help output. + del /q "%HELP_LOG%" >nul 2>nul + if exist "friidump.log" del /q "friidump.log" + exit /b 1 + ) +) + +for %%T in ("reports are created by default" "beside the final log" "may be combined with --report-json" "omission assumes stock") do ( + findstr /l /c:"%%~T" "%HELP_LOG%" >nul + if errorlevel 1 ( + echo [ERROR] Native-report default/override contract is missing from help output: %%~T + del /q "%HELP_LOG%" >nul 2>nul + if exist "friidump.log" del /q "friidump.log" + exit /b 1 + ) ) del /q "%HELP_LOG%" >nul 2>nul if exist "friidump.log" del /q "friidump.log" -powershell -NoProfile -ExecutionPolicy Bypass -Command "Compress-Archive -Force -Path friidump.exe,docs,README,AUTHORS,COPYING,config.h,msvc32_friidump.rsp,build_msvc32.cmd,.vscode -DestinationPath friidump_msvc32_build_results.zip" +powershell -NoProfile -ExecutionPolicy Bypass -Command "Compress-Archive -Force -Path friidump.exe,redump_dat,docs,README,README.md,AUTHORS,COPYING,config.h,msvc32_friidump.rsp,build_msvc32.cmd,build_msvc32_native_report_tests.cmd,msvc32_native_report_tests.rsp,build_msvc32_xbox_portable_tests.cmd,msvc32_xbox_portable_tests.rsp,tests,.vscode -DestinationPath friidump_msvc32_build_results.zip" exit /b %ERRORLEVEL% :clean if exist "friidump.exe" del /q "friidump.exe" +if exist "friidump-report-fixtures.exe" del /q "friidump-report-fixtures.exe" +if exist "friidump-xbox-portable-metadata-test.exe" del /q "friidump-xbox-portable-metadata-test.exe" if exist "build-msvc32" rmdir /s /q "build-msvc32" if exist "friidump_msvc32_build_results.zip" del /q "friidump_msvc32_build_results.zip" if exist "friidump.log" del /q "friidump.log" diff --git a/build_msvc32_native_report_tests.cmd b/build_msvc32_native_report_tests.cmd new file mode 100644 index 0000000..e150138 --- /dev/null +++ b/build_msvc32_native_report_tests.cmd @@ -0,0 +1,30 @@ +@echo off +setlocal EnableExtensions +cd /d "%~dp0" + +if not exist "build-msvc32" mkdir "build-msvc32" +if exist "friidump-report-fixtures.exe" del /q "friidump-report-fixtures.exe" +if exist "build-msvc32\report-fixtures" rmdir /s /q "build-msvc32\report-fixtures" +mkdir "build-msvc32\report-fixtures" + +call "%~dp0.vscode\msvc32.cmd" cl @"%~dp0msvc32_native_report_tests.rsp" /link ole32.lib +if errorlevel 1 exit /b %ERRORLEVEL% + +"%~dp0friidump-report-fixtures.exe" "%~dp0build-msvc32\report-fixtures" +if errorlevel 1 exit /b %ERRORLEVEL% + +powershell -NoProfile -ExecutionPolicy Bypass -Command "$files = Get-ChildItem -LiteralPath '%~dp0build-msvc32\report-fixtures' -Recurse -Filter '*.friidump.json'; if ($files.Count -lt 9) { throw 'Expected at least 9 native-report fixtures.' }; $files | ForEach-Object { $null = Get-Content -Raw -LiteralPath $_.FullName | ConvertFrom-Json }; Write-Host ('MSVC native-report fixtures: PASS (' + $files.Count + ')')" +if errorlevel 1 exit /b %ERRORLEVEL% + +where py >nul 2>nul +if not errorlevel 1 ( + py -3 "%~dp0tests\validate_native_reports.py" --fixtures "%~dp0build-msvc32\report-fixtures" --schema "%~dp0tests\contracts\friidump-test-result.v1.schema.json" + if errorlevel 1 ( + echo [ERROR] Strict Python fixture validation failed. Install jsonschema with: py -3 -m pip install jsonschema + exit /b %ERRORLEVEL% + ) +) else ( + echo [WARN] Python launcher not found; strict JSON Schema validation was not run on Windows. +) + +exit /b 0 diff --git a/build_msvc32_xbox_portable_tests.cmd b/build_msvc32_xbox_portable_tests.cmd new file mode 100644 index 0000000..4909b14 --- /dev/null +++ b/build_msvc32_xbox_portable_tests.cmd @@ -0,0 +1,14 @@ +@echo off +setlocal EnableExtensions +cd /d "%~dp0" + +if not exist "build-msvc32" mkdir "build-msvc32" + +call "%~dp0.vscode\msvc32.cmd" cl @"%~dp0msvc32_xbox_portable_tests.rsp" +if errorlevel 1 exit /b %ERRORLEVEL% + +"%~dp0friidump-xbox-portable-metadata-test.exe" +if errorlevel 1 exit /b %ERRORLEVEL% + +echo MSVC XBOX PORTABLE METADATA CONTRACT: PASS +exit /b 0 diff --git a/docs/BUILDING.md b/docs/BUILDING.md index a9fb65a..ebe7926 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -26,13 +26,28 @@ cmake --build build ./build/src/friidump --help ``` -The standalone repository candidate was tested with GCC 14.2 and CMake. The -build succeeded. The inherited `libfriidump/rs.c` shift-count warning remains -open and should be corrected only as a separately reviewed source change. +The Linux configure step prints an explicit warning: a normal build does not +install `CAP_SYS_RAWIO`. This is intentional. Build systems must not silently +invoke `sudo` or elevate their output. + +After validating the exact executable, install only the capability required for +vendor-specific SCSI commands: + +```bash +bash ./validation/friidump-linux-rawio-capability.sh install ./build/src/friidump +``` + +Do not run FriiDump itself with `sudo`. Rebuilding, relinking, copying, or +replacing the executable clears file capabilities; reapply and verify after +every build intended for GC/Wii memory-dump or Xbox vendor-unlock hardware use. +See [`LINUX.md`](LINUX.md) for the complete permission model. + +The inherited `libfriidump/rs.c` shift-count warning remains open and should be +corrected only as a separately reviewed source change. ## External runtime requirements -Dumping requires operating-system access to the selected optical drive. -Privileges and device permissions vary by platform. Avoid broadly running -unrelated tools with elevated privileges; grant only the access required for -the optical device. +Dumping requires operating-system access to the selected optical drive. Linux +vendor-command paths require both device-node access and effective +`CAP_SYS_RAWIO` on the exact validated executable. Grant only the minimum +required authority. diff --git a/docs/CAMPAIGN_FRIIDUMP_HOMEPAGE_AND_RELEASES.md b/docs/CAMPAIGN_FRIIDUMP_HOMEPAGE_AND_RELEASES.md new file mode 100644 index 0000000..771e866 --- /dev/null +++ b/docs/CAMPAIGN_FRIIDUMP_HOMEPAGE_AND_RELEASES.md @@ -0,0 +1,49 @@ +# FriiDump homepage, Linux parity, and release automation campaign + +## Governing order + +1. Candidate11 Linux Xbox/XGD user-path parity. +2. Release automation v1 with exact-tag builders and immutable artifacts. +3. Compatibility site v1.3.0 promoted into the FriiDump project homepage. +4. MinGW cross-build investigation after the MSVC and native-Linux pipeline is + established and verified. + +## Candidate11 + +The validated Windows/MSVC build remains the Windows authority. Linux reuses +FriiDump's portable MMC transport and receives equivalent Xbox metadata, +output, cancellation, hashing, and native-report behavior. Development-only +XGD1 probes are a separate substage. + +## Release automation v1 + +A validated annotated tag creates a small build request. Compilation does not +run inside the Git hook. + +- Linux worker: exact tag, GCC/CMake, x86_64 artifact. +- Windows worker: exact tag, Visual Studio 2019 Build Tools, x86 artifact. +- Both workers run tests and publish signed-by-hash build manifests to staging. +- A publisher verifies all required artifacts and atomically publishes an + immutable release directory plus `latest.json`. +- Failed or incomplete builds remain in staging and never change `latest`. + +## Site v1.3.0 + +The public site becomes the FriiDump homepage with: + +- Windows and Linux download cards; +- source archive, tag, commit, tree, and SHA-256 identity; +- compatibility table and evidence views; +- report submission; +- documentation and release history; +- machine-readable `latest.json` and immutable release manifests. + +The Git repository may remain LAN-only. Public source ZIPs and exact provenance +are sufficient; public Gitweb is optional. + +## MinGW follow-up + +MinGW is the preferred eventual Windows automation path after initial setup and +verification. It must be introduced as a separate toolchain identity, compared +against the validated MSVC behavior, and promoted only after real-drive and +native-report parity is demonstrated. diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index f769f8f..85e1df4 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -2,25 +2,82 @@ ## Authority -FriiDump is active at `/srv/project-frankenstein/repos/friidump.git` under -annotated tag `v0.5.3.5-pf1`. +FriiDump remains governed by the established Project Frankenstein repository +and promoted release history. Candidate11 is a development candidate derived +from candidate10; it is not yet a promoted release. ## Validated paths -- Windows/MSVC 32-bit build helper retained; -- Linux/CMake configure, build, and `--help` smoke validated; -- exact promoted HLDS profiles support selected GC/Wii hardware; -- GDR-8050L and GDR-3120L Xbox/XGD paths are present; -- media/profile preflight, metadata, XISO, and STOP UNIT cleanup are maintained. +- Windows/MSVC 32-bit candidate10 challenge-drive behavior remains the current + live-validated Windows authority. +- Candidate11 compiles cleanly through Linux/CMake as a static binary. +- Native-report fixtures and schema/semantic validation pass. +- A portable XDFS/XBE metadata fixture proves title, media ID, Title ID, raw + GameRegion, formatted region, and UTF-16-to-UTF-8 conversion. +- Linux ISO/XISO code now returns the same structured Xbox result fields used by + native reports, summary output, hashes, and Redump verification. +- Linux GDR-8050L default output names can be derived from XBE/DMI metadata. -## Known limitation +## Hardware-validation boundary -`libfriidump/rs.c` retains an inherited right-shift-count warning. It remains -technical debt and was not silently altered during repository or documentation -work. +Candidate11 has not yet been run against a physical Xbox-capable optical drive +on Linux. Real hardware must prove no-media behavior, unlock state transitions, +metadata, controlled cancellation, exact hashes, STOP UNIT, complete XISO, and +redump-style ISO behavior before Linux support can be described as validated. -## Boundary +The development-only XGD1 layout and raw-ID probes remain Windows-only in this +first candidate11 increment. They are tracked as a separate Linux-port substage +and do not block ordinary user ISO/XISO validation. -Disc images, live logs, compiled output, build directories, firmware, and -result archives remain outside Git. `friidump-handoff.md` is transition history, -not the current specification; use `README.md`, `docs/XBOX.md`, and this file. +## Known inherited warnings + +`libfriidump/rs.c` retains an inherited right-shift-count warning. Additional +warnings visible under an explicit `-Wall -Wextra` audit predate candidate11 or +are non-fatal test-path warnings. No candidate11 portable metadata source error +or warning blocked the build. + +## Campaign + +After Linux user-path validation, work proceeds to exact-tag release automation, +then the FriiDump homepage/compatibility site v1.3.0, then a separately identified +MinGW investigation. + +## Candidate12 correction status + +Candidate12 closes the Linux GCC-4244 B101 full-dump gate and corrects runtime +DAT discovery, Linux lock-status reporting, and the Linux `--help` validation +false failure. The accepted live result is Sonic Mega Collection (US), 712880 +sectors, with CRC32 `01b52739`, MD5 +`85a525df1481d0ad67d8761f832dca12`, SHA-1 +`06eb6d15b4d7f90ec0fed9ce9a77db41358d74ed`, and SHA-256 +`30098da93f5de9ece8da44f8afdfb85cf9bcdc24d77131221e64e3038a529010`. + + +## Candidate16 Linux authorization status + +Candidate13's live diagnostics proved that Candidate12/13 seed failures were +host-side authorization failures: vendor opcode `0xE7` returned `EPERM` before +usable drive sense, while Candidate11's working executable carried +`cap_sys_rawio=ep`. + +Candidate16 makes this boundary explicit. Linux vendor-command paths preflight +effective `CAP_SYS_RAWIO`, refuse whole-process root execution, provide exact +least-privilege installation instructions, record preflight failures in native +reports, and issue STOP UNIT cleanup. The build emits a warning but does not +silently invoke `sudo` or modify file capabilities. + + +## Candidate16 build-hygiene status + +Candidate16 preserves Candidate14 raw-I/O authorization and removes the user-side clock-skew and production long-path warning defects. The Linux validator normalizes only future source timestamps before building, and the raw-I/O preflight uses bounded path construction with a larger native-report note buffer. + + +## Candidate16 Linux Xbox tray-cycle status + +Candidate15's first Linux Xbox smoke used a normal video DVD and proved target +selection plus raw-I/O authorization, but failed immediately at Stage 2 because +the Linux optical door remained locked while FriiDump held the device open. +Candidate16 releases the Linux `CDROM_LOCKDOOR` state before each GDR-8050L +software eject/load cycle, restores the lock after a successful load, and leaves +the door unlocked on failure for manual recovery. A physical Xbox-disc smoke is +required before the Linux Xbox gate can close. diff --git a/docs/ChangeLog b/docs/ChangeLog index f893c78..71f91eb 100644 --- a/docs/ChangeLog +++ b/docs/ChangeLog @@ -1,3 +1,52 @@ +FriiDump 0.5.3.16-pf1-candidate21 +----------------------------------- +- Correct standard READ DVD STRUCTURE Format placement from CDB byte 11 + (Control) to byte 7, matching the copied Windows DMI request. +- Add persistent Xbox PFI/DMI capture diagnostics. +- Add an Xbox READ DVD STRUCTURE CDB source contract. +- Preserve Candidate20 SG_IO handshake and Windows-sequence behavior unchanged. + +FriiDump 0.5.3.16-pf1-candidate20 +- Rejects Candidate17; do not test or promote it. +- Removes generic RecoveryKick behavior and its repeated LBA-zero read cadence. +- Removes automatic media-auth kicks from the generic copied Windows dump path. +- Ports the proven Windows state-aware GDR-8050L preparation sequence into Linux style. +- Uses direct handshake first, one tray-cycle retry only when needed, and capacity as authority. + +2026-07-24 Project Frankenstein Candidate16 + + * Release Linux optical-door locking before GDR-8050L media cycles. + * Restore the door lock after successful eject/load readiness. + * Leave the door unlocked on media-cycle failure for recoverable tray access. + * Add Linux Xbox tray-cycle source/documentation contract coverage. + +2026-07-24 Project Frankenstein Candidate15 + + * Document the two-part Linux access model: optical device-node access plus + CAP_SYS_RAWIO for vendor-specific SCSI commands. + * Add a release-build preflight that checks the effective capability mask + before GC/Wii memory-dump and Xbox vendor-unlock operations. + * Refuse whole-process root execution for Linux vendor-command paths and + direct users to a least-privilege file capability on the exact executable. + * Record missing raw-I/O authority in the native report and issue STOP UNIT + before returning. + * Add explicit CMake/build warnings that normal builds do not install or + preserve file capabilities. + * Add an install/status/remove helper for cap_sys_rawio=ep and a validator + capability phase. + * Document that rebuilding, replacing, copying, or re-extracting an executable + clears the capability. + +2026-07-24 Project Frankenstein Candidate12 + + * Copy bundled Redump DAT resources beside CMake-built executables. + * Install Redump DAT resources under the Unix share directory. + * Add FRIIDUMP_REDUMP_DAT_DIR and installed-share lookup. + * Preserve explicit --redump-dat-dir as an authoritative override. + * Report Linux exclusive optical-device locking as unavailable instead of OK. + * Correct the Linux validation wrapper's intentional --help exit handling. + * Include redump_dat in the MSVC build-results archive. + 2026-07-18 - FriiDump 0.5.3.15 XGD1 live closure and release cleanup * Closed the 0.5.3.14 hardware validation on a modified GDR-8050L revision @@ -640,3 +689,10 @@ Build packaging note: * Strengthened media preflight: TEST UNIT READY is followed by READ CAPACITY(10), requiring a nonzero 2048-byte logical-block medium before vendor seed/cache commands. * Fixed empty-tray false positive observed on HL-DT-ST CDRW/DVD GCC4244/B103 over USB. * Added completed GDR-8163B 0L23 Redump-matching validation result. + +Candidate15 +----------- +* Preserve explicit CAP_SYS_RAWIO build/runtime documentation and preflight. +* Normalize future source timestamps before Linux builds. +* Remove production raw-I/O preflight path-truncation warning signatures. +* Normalize source ZIP member timestamps. diff --git a/docs/FRIIDUMP_NATIVE_REPORT_REQUIREMENTS.md b/docs/FRIIDUMP_NATIVE_REPORT_REQUIREMENTS.md new file mode 100644 index 0000000..4386007 --- /dev/null +++ b/docs/FRIIDUMP_NATIVE_REPORT_REQUIREMENTS.md @@ -0,0 +1,939 @@ +# FriiDump Native Report Requirements for the Compatibility Database + +**Document version:** 1.0 +**Target database baseline:** FriiDump Compatibility Database v1.2.0 +**Audience:** FriiDump developers and maintainers +**Purpose:** Define the machine-readable evidence FriiDump must produce for future compatibility testing. + +--- + +## 1. Objective + +FriiDump must produce a self-contained JSON report for each test execution so that the compatibility database can preserve the exact drive, firmware, media, result, provenance, and measured performance of that run. + +The report is evidence only. It must not claim or directly change a public compatibility status. Public compatibility assessments remain curated separately and may only become report-backed after an administrator accepts and links the report to the exact firmware and platform scope. + +The current interchange schema is: + +```text +friidump-test-result.v1 +``` + +The preferred filename suffix is: + +```text +.friidump.json +``` + +--- + +## 2. Required output behavior + +For database-compatible operation, FriiDump must be able to emit **one report per execution**. Native reporting requires exactly one primary dump output; combined raw and ISO output is rejected so `dump.output_path`, byte count, hashes, and the `dump_output` artifact remain unambiguous. + +A report represents exactly: + +- one FriiDump invocation; +- one physical or emulated drive; +- one live firmware identity; +- one disc or diagnostic media state; +- one final result. + +FriiDump must not place multiple runs inside one JSON document. Batch submission is performed by selecting multiple individual report files. + +Native report generation is enabled by default for normal dump invocations. Without a report option, FriiDump writes the report beside the final FriiDump log and derives the filename from that log filename by replacing the final `.log` suffix with `.friidump.json`. + +The command-line overrides are: + +```text +--report-json Override the complete report pathname. +--report-dir Override only the destination directory. +--firmware-modified Declare modified firmware and record the explanation. +``` + +`--report-json` and `--report-dir` may be combined. When combined, the filename from `--report-json` is retained while `--report-dir` replaces its directory. When `--firmware-modified` is omitted, stock firmware is assumed. FriiDump must not infer modified firmware from the selected drive profile. + +The report should be written automatically when the run reaches a final state, including: + +- successful complete dump; +- partial dump; +- seed-retrieval failure; +- dump failure after successful seed retrieval; +- no-media diagnostic; +- wrong-media diagnostic; +- media-transition diagnostic; +- user cancellation or another interrupted run when enough state exists to describe it honestly. + +An interrupted run must not be reported as successful. + +### Recommended write behavior + +The final report should be written atomically: + +1. Write the complete JSON to a temporary file. +2. Flush and close it. +3. Rename it to the final `.friidump.json` name. + +This prevents an incomplete JSON file from appearing to be a finished report. + +--- + +## 3. Root format identity + +Every report must contain this exact identity: + +```json +{ + "schema": "friidump-test-result.v1", + "format": { + "name": "FriiDump Interchange Format", + "generation": 1, + "serialization": "json" + } +} +``` + +The JSON document must be UTF-8. + +The schema is strict. Unknown root fields and unknown fields inside defined objects are rejected. Compatible future additions require coordinated schema work; FriiDump must not add arbitrary fields to generation 1 reports. + +--- + +## 4. Required root objects + +Every report must contain all of these root fields: + +```text +schema +format +generated_utc +generator +run +drive +media +seed +dump +hashes +reference +profile +notes +artifacts +``` + +Fields that do not apply must still be present with an appropriate `null`, `false`, `not_attempted`, or `not_applicable` value. + +--- + +## 5. Run identity and timestamps + +### 5.1 Run UUID + +`run.run_id` must be a UUID generated once for the execution. + +Requirements: + +- Generate it at or before the start of the run. +- Preserve it throughout the run. +- A retry is a new run and receives a new UUID. +- Re-exporting the exact same stored run must preserve the original UUID. +- Never reuse a UUID for different report content. + +The database uses the run UUID as part of duplicate and conflict detection. Reusing a UUID with changed content creates a review conflict. + +### 5.2 Time fields + +All timestamps must be ISO-8601 UTC timestamps, preferably with a `Z` suffix. + +```text +generated_utc +run.started_utc +run.completed_utc +``` + +For a native FriiDump run: + +- `run.started_utc` should record the authoritative execution start time. +- `run.completed_utc` should record the authoritative final-state time. +- `generated_utc` records when the JSON report was generated. +- `completed_utc` must not precede `started_utc`. +- If a timestamp genuinely cannot be known, use `null`; do not invent one. + +A reconstructed historical report may have unknown run timestamps, but native FriiDump should normally provide both. + +--- + +## 6. Generator identity + +FriiDump must record the exact executable identity: + +```json +"generator": { + "name": "FriiDump", + "version": "0.5.3.15-pf1", + "build_commit": "40-character-git-commit-or-null" +} +``` + +Requirements: + +- `name` must be exactly `FriiDump`. +- `version` must identify the running build. +- `build_commit` should contain the full 40-character Git commit when available. +- Do not substitute a package date, branch name, or abbreviated commit for the full commit field. + +--- + +## 7. Exact drive and firmware identity + +The `drive` object must describe the drive actually used by the run: + +```json +"drive": { + "vendor": "HL-DT-ST", + "model": "DVD-ROM GDR8163B", + "firmware_revision": "0M26", + "interface": "ATAPI", + "device_path": "F:", + "firmware_modified": false, + "modification_note": null +} +``` + +Requirements: + +- Populate vendor, model, and firmware revision from the live device inquiry. +- Preserve the exact meaningful firmware revision. +- Do not substitute a label revision, expected revision, or profile revision for the live inquiry result. +- `firmware_modified` must always be a Boolean. +- When `firmware_modified` is `true`, `modification_note` must explain the relevant modification. +- Stock and modified firmware are separate compatibility identities. +- `device_path` is descriptive and may be `null` when unavailable. + +A report can only be linked to an assessment with the same exact firmware identity and platform. + +--- + +## 8. Media identity + +The `media.platform` value must be one of: + +```text +gamecube +wii +xbox +dvd +unknown +``` + +The media object is: + +```json +"media": { + "platform": "gamecube", + "title": "Sonic Mega Collection", + "region": "USA", + "disc_id": null +} +``` + +Requirements: + +- Use the actual tested platform. +- For unforced GameCube/Wii runs, classify the platform from the unscrambled + sector-0 platform magic after seed retrieval: Wii magic `0x5d1c9ea3` at + byte `0x18`, otherwise GameCube magic `0xc2339f3d` at byte `0x1c`. +- The sector-0 platform magic overrides a conflicting legacy capacity-probe + result. Explicit `-T` and `-S` selections remain authoritative. +- Include the disc title when media is present and it can be determined. +- Include region and disc ID when known. +- Use `null` rather than guessed values. +- Diagnostic no-media runs should normally use `platform: "unknown"` and null media identity fields. + +The database maps GameCube and Wii reports into the shared `gcwii` assessment platform while retaining the original report platform. + +--- + +## 9. Test type and final result + +### 9.1 Allowed test types + +```text +full_dump +seed_only +partial_dump +diagnostic_no_media +diagnostic_wrong_media +media_transition +other +``` + +### 9.2 Allowed run results + +```text +pass +partial +fail +not_applicable +``` + +### 9.3 Required mappings + +| Situation | `run.test_type` | `run.result` | +|---|---|---| +| Complete successful dump | `full_dump` | `pass` | +| Run stopped after producing valid partial progress | `partial_dump` | `partial` | +| Seed-only test succeeds | `seed_only` | `pass` | +| Seed retrieval fails | `seed_only` | `fail` | +| No disc inserted | `diagnostic_no_media` | `not_applicable` | +| Wrong media inserted | `diagnostic_wrong_media` | `not_applicable` | +| Media transition test | `media_transition` | normally `not_applicable` | +| Unclassified test | `other` | honest final result | + +A no-media or wrong-media diagnostic is not evidence that the drive is incompatible. + +--- + +## 10. Seed-stage reporting + +The `seed` object must always be present: + +```json +"seed": { + "attempted": true, + "result": "pass", + "duration_seconds": 6.0 +} +``` + +Allowed seed results: + +```text +pass +partial +fail +not_attempted +not_applicable +``` + +Requirements: + +- `attempted` must reflect whether FriiDump actually attempted seed retrieval. +- `duration_seconds` must measure the seed stage only. +- Use a non-negative numeric duration when measured. +- Use `null` when no defensible measurement exists. +- Do not include seed time in `dump.duration_seconds`. + +--- + +## 11. Dump-stage reporting + +The `dump` object must always be present: + +```json +"dump": { + "attempted": true, + "result": "pass", + "sector_count": 712880, + "byte_count": 1459978240, + "duration_seconds": 7241.31, + "failure_stage": null, + "failure_sector": null, + "output_path": "sonic.iso" +} +``` + +Allowed dump results: + +```text +pass +partial +fail +not_attempted +not_applicable +``` + +### Complete successful dump + +A successful dump must include: + +- `sector_count`; +- `byte_count`; +- `duration_seconds` when measured; +- at least one output hash for a warning-free report; +- `failure_stage: null`; +- `failure_sector: null`. + +### Partial dump + +A partial dump should include: + +- the number of sectors actually completed; +- the number of output or payload bytes actually completed; +- the measured elapsed dump time, when defensible; +- the stage where the run stopped; +- the failure or stopping sector, when known; +- the partial output path, when an output exists. + +Do not report the planned final size as `byte_count` for a partial run. + +### Failed dump + +A failed dump should identify `failure_stage`. Examples include: + +```text +seed_retrieval +raw_sector_read +output_write +finalization +reference_verification +user_cancelled +``` + +Use the most specific stable machine-readable value available. + +--- + +## 12. Throughput and measurement scope + +The database calculates measured throughput from the report: + +```text +MiB/hour = byte_count × 3600 ÷ duration_seconds ÷ 1,048,576 +``` + +FriiDump must not provide an estimated or instantaneous progress rate as the completed-run average. + +For a meaningful calculation: + +- `dump.byte_count` must describe the bytes represented by the measurement. +- `dump.duration_seconds` must describe the time spent producing those same bytes. +- The duration should exclude seed retrieval. +- Post-dump hashing and reference matching should not be included unless the measurement is explicitly defined to include them. +- A zero or unknown duration must be represented as `null`, not as a fabricated small value. + +### 12.1 Required measurement scopes + +The compatibility database recognizes: + +```text +full_optical_payload +assembled_output +partial_progress +unknown +``` + +The generation-1 JSON schema has no dedicated measurement-scope property. FriiDump must therefore place the scope in a controlled note using this exact form: + +```text +Measurement scope: full_optical_payload. +``` + +Other valid forms are: + +```text +Measurement scope: assembled_output. +Measurement scope: partial_progress. +Measurement scope: unknown. +``` + +The importer reads the `measurement scope:` token case-insensitively. + +### 12.2 Scope definitions + +#### `full_optical_payload` + +Use when `byte_count` and `duration_seconds` represent a complete optical payload read. + +Examples: + +- complete GameCube optical dump; +- complete Wii optical dump; +- complete ordinary DVD optical read. + +#### `assembled_output` + +Use when the timed operation creates a final image by combining optical reads with copied, generated, synthetic, padded, or finalized regions. + +This is required for the current Original Xbox assembled-output workflow when the reported bytes and time describe the final assembled image rather than only physical optical reads. + +Assembled-output throughput must not be presented as directly comparable to optical-only throughput. + +#### `partial_progress` + +Use when the run did not complete and the byte/time pair describes only actual progress before failure or cancellation. + +A partial-progress rate is not a completed-dump average. + +#### `unknown` + +Use when no defensible measurement classification exists. + +### 12.3 Native automatic inference + +The database can infer `full_optical_payload` for a native successful GameCube, Wii, or DVD full dump, but FriiDump should still emit an explicit scope note. Explicit scope is mandatory for Xbox and any nonstandard output workflow. + +--- + +## 13. Output hashes + +The `hashes` object must always contain all four keys: + +```json +"hashes": { + "crc32": "01b52739", + "md5": "85a525df1481d0ad67d8761f832dca12", + "sha1": null, + "sha256": null +} +``` + +Requirements: + +- Use lowercase or uppercase hexadecimal; the importer normalizes to lowercase. +- Hashes must describe the exact output represented by `dump.output_path`. +- A successful complete dump should provide at least one hash. +- Prefer recording every hash FriiDump already calculates. +- Do not hash a different file, temporary file, or logical payload while describing the final output file. +- Use `null` when a hash was not calculated. + +Required lengths: + +```text +CRC32 8 hexadecimal characters +MD5 32 hexadecimal characters +SHA-1 40 hexadecimal characters +SHA-256 64 hexadecimal characters +``` + +--- + +## 14. Reference verification + +The `reference` object describes software-side comparison results: + +```json +"reference": { + "provider": "redump", + "result": "match", + "reference_id": null +} +``` + +Allowed results: + +```text +match +mismatch +not_checked +not_applicable +``` + +Requirements: + +- A `match` should identify the reference provider. +- Use `not_checked` when no comparison was performed. +- Use `not_applicable` for diagnostic runs where reference comparison has no meaning. +- Reference match or mismatch is report evidence, not a direct hardware compatibility status. +- FriiDump must not convert a Redump result into a published compatibility assessment. + +--- + +## 15. FriiDump profile identity + +The `profile` object must always be present: + +```json +"profile": { + "support_tier": "known_supported_profile_hardening", + "cdb_offset": "0x5E0", + "gate_address": "0x90024FF7" +} +``` + +Requirements: + +- Record the support tier actually selected by FriiDump. +- Record the selected CDB offset and firmware gate address when applicable. +- Hexadecimal addresses must begin with `0x`. +- Use `null` when the value does not apply or is not known. +- Do not report analyzer candidates as confirmed runtime values unless FriiDump actually selected them. + +--- + +## 16. Notes and evidence provenance + +`notes` is an array of concise factual strings. + +For a native FriiDump-generated report: + +- Do not add `Evidence origin: reconstructed...`. +- Do not add a converter identity. +- Do not claim the report was reconstructed or imported. +- The database classifies a report as `native_report` by default. +- Include the controlled measurement-scope note. +- Include concise information that cannot be represented elsewhere. +- Do not place a compatibility status recommendation in the notes. +- Do not copy large portions of the text log into the notes. + +Example: + +```json +"notes": [ + "Measurement scope: full_optical_payload.", + "Complete optical dump; output hashes calculated after the timed read." +] +``` + +The phrases used by the historical converter are reserved for reconstructed evidence and must not be emitted by native FriiDump: + +```text +Evidence origin: reconstructed ... +Source artifact: ... +Source artifact SHA-256: ... +Converter: ... +``` + +--- + +## 17. Artifact inventory + +`artifacts` is an array. Each entry must contain: + +```text +type +path +bytes +sha256 +``` + +Example: + +```json +"artifacts": [ + { + "type": "log", + "path": "docs/debug/gdr8163b_0m26.log", + "bytes": 48320, + "sha256": "64-hex-character-sha256" + }, + { + "type": "dump_output", + "path": "Sonic Mega Collection.iso", + "bytes": 1459978240, + "sha256": null + } +] +``` + +Requirements: + +- Artifact paths are descriptive metadata. +- Use a stable type such as `log`, `dump_output`, or another documented FriiDump artifact type. +- Record artifact byte size and SHA-256 when FriiDump knows them. +- Use `null` when a path, size, or SHA-256 is unavailable. +- The report importer does not open arbitrary paths from the JSON. +- The report file itself is preserved separately by the database after acceptance. + +--- + +## 18. Native report versus reconstructed historical report + +Future FriiDump output must be native evidence. + +| Property | Native FriiDump report | Reconstructed historical report | +|---|---|---| +| Evidence origin | Inferred as `native_report` | Declared in controlled notes | +| Run timestamps | Normally authoritative | May be unknown | +| Converter identity | Not present | Present | +| Source-log identity | Optional normal artifact | Required reconstruction provenance | +| Measurement scope | Emitted by FriiDump | Recovered from historical evidence | +| Report generation | During or immediately after run | Later conversion from archived logs | + +FriiDump must not imitate reconstruction provenance in new native reports. + +--- + +## 19. Database workflow implications + +A generated report does not automatically change public compatibility data. + +The workflow is: + +1. FriiDump generates one `.friidump.json` report. +2. The report is submitted or imported. +3. The report is validated and fingerprinted. +4. An administrator accepts or rejects it. +5. An accepted report becomes reviewed evidence. +6. An administrator may link it to an assessment with the same exact firmware and platform. +7. A tested compatibility status may be published only when accepted evidence is linked. + +FriiDump therefore reports facts, not final compatibility conclusions. + +--- + +## 20. Duplicate and reproducibility requirements + +The database evaluates duplicate identity using: + +```text +source system + run UUID +exact report-file SHA-256 +normalized report SHA-256 +``` + +FriiDump requirements: + +- Each new execution gets a new UUID. +- The same stored execution must retain its UUID when exported again. +- FriiDump should serialize reports deterministically where practical. +- Do not regenerate the same run with a new `generated_utc` and changed content while preserving the old UUID. +- Do not reuse a report file as a template without replacing its run UUID. +- A resumed run should be clearly treated as either the same persisted execution or a new execution; it must not ambiguously reuse identity. + +--- + +## 21. Filename requirements + +Preferred filename: + +```text +.friidump.json +``` + +Examples: + +```text +Sonic Mega Collection.friidump.json +Red Faction II [TQ00501A].friidump.json +friidump-diagnostic-no-media-.friidump.json +``` + +Requirements: + +- The `.friidump.json` suffix is required by the public submission workflow. +- Sanitize characters invalid on the host filesystem. +- Avoid overwriting an earlier report. +- Use a collision-safe suffix when two runs have the same title. +- The root `schema` field, not the filename alone, determines the format. + +--- + +## 22. Warning-free evidence policy + +A report may be schema-valid while still generating quality warnings. FriiDump should produce warning-free reports whenever the information is available. + +Avoid these warning conditions: + +- modified firmware without `modification_note`; +- media present but no disc title; +- failed dump without `failure_stage`; +- successful dump with no output hash; +- reference match without a reference provider. + +Warnings do not always invalidate a report, but they reduce evidence quality and require additional review. + +--- + +## 23. Minimum acceptance checklist for FriiDump + +A FriiDump implementation is ready for database integration when all of the following pass: + +- [ ] Emits exactly one UTF-8 `.friidump.json` file per finalized run. +- [ ] Uses `friidump-test-result.v1`. +- [ ] Emits every required root object and required child field. +- [ ] Rejects or prevents unknown schema fields. +- [ ] Generates a unique persistent UUID per run. +- [ ] Records authoritative UTC start and completion timestamps. +- [ ] Records exact FriiDump version and full build commit. +- [ ] Records exact live drive and firmware inquiry identity. +- [ ] Separates stock and modified firmware. +- [ ] Provides a modification note for modified firmware. +- [ ] Records platform and media identity without guessing. +- [ ] Uses correct test-type and result mappings. +- [ ] Separates seed duration from dump duration. +- [ ] Records actual sectors, bytes, and elapsed time. +- [ ] Does not report planned final bytes as partial progress. +- [ ] Emits an explicit `Measurement scope:` note. +- [ ] Uses `assembled_output` for the current Xbox assembled-image metric. +- [ ] Provides at least one output hash for successful dumps. +- [ ] Records reference verification separately from compatibility status. +- [ ] Records the selected runtime profile identity. +- [ ] Does not emit reconstruction provenance for native runs. +- [ ] Preserves exact report identity on re-export. +- [ ] Writes the final JSON atomically. +- [ ] Passes schema and semantic validation with zero errors. +- [ ] Produces warning-free successful-run fixtures. +- [ ] Includes automated fixtures for success, partial, seed failure, and diagnostics. + +--- + +## 24. Required test fixtures + +FriiDump development should include at least these report-generation tests: + +1. **Successful GameCube full dump** + - `full_dump` / `pass` + - `full_optical_payload` + - sectors, bytes, duration, hash + - stock firmware + +2. **Successful Xbox assembled output** + - `full_dump` / `pass` + - `assembled_output` + - explicit non-comparable scope + - modified firmware when applicable + +3. **Partial optical dump** + - `partial_dump` / `partial` + - actual progress bytes and sectors + - `partial_progress` + - failure stage and sector + +4. **Seed-retrieval failure** + - `seed_only` / `fail` + - dump `not_attempted` + - no fabricated dump size or throughput + +5. **No-media diagnostic** + - `diagnostic_no_media` / `not_applicable` + - unknown media identity + - no compatibility conclusion + +6. **Modified-firmware identity** + - `firmware_modified: true` + - non-empty modification note + +7. **Duplicate export** + - same stored run exports with the same UUID and equivalent normalized content + +8. **New retry** + - same drive and disc, but a new execution receives a new UUID + +--- + +## 25. Native successful-run template + +```json +{ + "schema": "friidump-test-result.v1", + "format": { + "name": "FriiDump Interchange Format", + "generation": 1, + "serialization": "json" + }, + "generated_utc": "2026-07-21T20:15:00Z", + "generator": { + "name": "FriiDump", + "version": "REPLACE_WITH_VERSION", + "build_commit": "REPLACE_WITH_FULL_40_CHARACTER_COMMIT_OR_NULL" + }, + "run": { + "run_id": "REPLACE_WITH_UUID", + "started_utc": "2026-07-21T18:00:00Z", + "completed_utc": "2026-07-21T20:14:31Z", + "test_type": "full_dump", + "result": "pass" + }, + "drive": { + "vendor": "HL-DT-ST", + "model": "DVD-ROM GDR8163B", + "firmware_revision": "0M26", + "interface": "ATAPI", + "device_path": "F:", + "firmware_modified": false, + "modification_note": null + }, + "media": { + "platform": "gamecube", + "title": "Sonic Mega Collection", + "region": "USA", + "disc_id": null + }, + "seed": { + "attempted": true, + "result": "pass", + "duration_seconds": 6.0 + }, + "dump": { + "attempted": true, + "result": "pass", + "sector_count": 712880, + "byte_count": 1459978240, + "duration_seconds": 7241.31, + "failure_stage": null, + "failure_sector": null, + "output_path": "Sonic Mega Collection.iso" + }, + "hashes": { + "crc32": "01b52739", + "md5": "85a525df1481d0ad67d8761f832dca12", + "sha1": null, + "sha256": null + }, + "reference": { + "provider": "redump", + "result": "match", + "reference_id": null + }, + "profile": { + "support_tier": "known_supported_profile_hardening", + "cdb_offset": "0x5E0", + "gate_address": "0x90024FF7" + }, + "notes": [ + "Measurement scope: full_optical_payload.", + "Complete optical dump; output hashes calculated after the timed read." + ], + "artifacts": [ + { + "type": "log", + "path": "friidump.log", + "bytes": null, + "sha256": null + } + ] +} +``` + +--- + +## 26. Validation target + +During development, reports should be validated against: + +```text +schemas/friidump-test-result.v1.schema.json +``` + +The compatibility database package also provides semantic validation through: + +```text +bin/validate-report.php +``` + +Acceptance target: + +```text +valid=true +errors=0 +warnings=0 +``` + +The native-report implementation should be considered incomplete until its automated fixtures pass both JSON Schema validation and the database semantic validator. + +--- + +## 27. Summary of the new FriiDump obligations + +The primary new obligation is not merely “write a JSON log.” FriiDump must produce durable, exact, reviewable evidence with: + +- stable per-run identity; +- authoritative timestamps; +- exact live firmware identity; +- honest final-state semantics; +- defensible byte and duration measurements; +- explicit measurement scope; +- output hashes; +- clear separation between native evidence, reference verification, and curated compatibility status. + +This report becomes the long-term evidence record behind the public compatibility database. It must therefore favor exactness and honesty over optimistic inference. diff --git a/docs/LINUX.md b/docs/LINUX.md new file mode 100644 index 0000000..513ab2f --- /dev/null +++ b/docs/LINUX.md @@ -0,0 +1,218 @@ +# FriiDump on Linux + +## Candidate21 status + +FriiDump builds natively with GCC/CMake and uses Linux optical-drive packet +commands through the existing `dvd_drive` transport. Candidate21 makes the +Linux privilege boundary explicit and reproducible. + +Candidate21 also corrects the Linux GDR-8050L software tray cycle. Linux may +lock an optical door while the device is open (`CDO_LOCK`). Before each required +Xbox media cycle, FriiDump now releases that lock with `CDROM_LOCKDOOR`, sends +the software eject/load sequence, waits for readiness, and restores the door +lock after a successful load. On a cycle failure it deliberately leaves the +door unlocked so the disc can be recovered without power-cycling the external +drive. + +The Linux workflow uses two separate permissions: + +1. **Device-node access** — the normal user must be able to open `/dev/sr*` and, + where applicable, the corresponding `/dev/sg*`. On Ubuntu this normally + comes from membership in the `cdrom` group. +2. **Vendor-command authorization** — GameCube/Wii memory-dump methods and Xbox + vendor-unlock paths issue commands outside Linux's ordinary unprivileged + SCSI allowlist. Those operations require effective `CAP_SYS_RAWIO`. + +Membership in `cdrom` is necessary but does not replace `CAP_SYS_RAWIO`. + +## Candidate21 DMI and media-ID boundary + +Candidate20 proved the Linux SG_IO handshake and XDVDFS data path but left the +media ID empty because its standard READ DVD STRUCTURE helper wrote Format to +CDB byte 11. Candidate21 uses byte 7, matching the copied Windows +`GetMediaID()` request. Live runs log `[XBOX-DVD-STRUCTURE]` PFI and DMI capture +results. The expected Red Faction II media ID is `TQ00501A`. + +The step-9 sticky-descrambling transport result remains visible. Final READ +CAPACITY and readable XDVDFS data are authoritative for game-view success. + + +## Build + +```bash +cmake -S . -B build \ + -DBUILD_STATIC_BINARY=ON \ + -DBUILD_NATIVE_REPORT_TESTS=ON \ + -DFRIIDUMP_BUILD_COMMIT="$(git rev-parse HEAD)" +cmake --build build --parallel +./build/src/friidump-xbox-portable-metadata-test +./build/src/friidump-linux-rawio-test +./build/src/friidump --help +``` + +Linux configuration and the Candidate21 build validator print a warning that a +normal build does **not** install file capabilities. This is intentional: a +compiler invocation must not silently elevate its output or invoke `sudo`. + +## Install the minimum raw-I/O authority + +Do not run FriiDump itself with `sudo`, and do not install it setuid-root. Grant +only `CAP_SYS_RAWIO` to the exact validated executable: + +```bash +BIN="$(readlink -f ./build/src/friidump)" +sudo setcap cap_sys_rawio=ep "$BIN" +getcap "$BIN" +``` + +Expected output: + +```text +/path/to/friidump cap_sys_rawio=ep +``` + +The maintained helper performs identity checks, applies the capability, verifies +that the executable bytes did not change, and confirms the resulting xattr: + +```bash +bash ./validation/friidump-linux-rawio-capability.sh install \ + ./build/src/friidump +``` + +Status and removal operations are also available: + +```bash +bash ./validation/friidump-linux-rawio-capability.sh status \ + ./build/src/friidump + +bash ./validation/friidump-linux-rawio-capability.sh remove \ + ./build/src/friidump +``` + +The candidate validator exposes the same installation step: + +```bash +bash ./validation/friidump-v0.5.3.16-pf1-candidate21-linux-live-validation.sh \ + capability +``` + +### Capability lifetime + +Linux file capabilities are extended attributes. They are not reliably +preserved by ZIP extraction, ordinary copying, source packaging, or rebuilding. +Replacing or relinking the executable clears the capability. Reapply and verify +`cap_sys_rawio=ep` after every build that will be used for vendor-command +hardware work. + +Never assume that a newer candidate inherited the capability from an older +binary. Candidate11 worked because its exact executable carried +`cap_sys_rawio=ep`; Candidate12 and Candidate13 initially did not. + +## Runtime preflight + +Before a vendor-command path begins seed retrieval or Xbox unlock, Candidate21 +checks the process's effective capability mask from `/proc/self/status`. + +With the least-privilege configuration it prints: + +```text +Linux raw-I/O preflight: PASS (CAP_SYS_RAWIO effective; ...). +``` + +When the capability is absent, FriiDump refuses the vendor operation before +seed retrieval, prints exact `setcap` and `getcap` commands for the running +executable, writes the failure into the native report, and issues STOP UNIT. +This replaces the previous generic transport-level `EPERM` failure. + +Candidate21 also refuses vendor-command work when the entire process is running +as root. Run it as the normal user with the file capability instead. + +Standard readable-DVD operations that do not use vendor memory-dump or unlock +commands do not require `CAP_SYS_RAWIO`. + +## Device access + +Identify the actual optical device dynamically. Do not assume the external +validation drive is `/dev/sr0`: + +```bash +for dev in /sys/class/block/sr*; do + printf '%s: ' "/dev/${dev##*/}" + xargs < "$dev/device/vendor" + xargs < "$dev/device/model" + xargs < "$dev/device/rev" +done +``` + +Inspect permissions for the selected block and SCSI-generic device: + +```bash +id +ls -l /dev/sr1 /dev/sg2 +getcap "$(readlink -f ./build/src/friidump)" +``` + +Close media players and file managers, unmount any mounted filesystem from the +drive, and disable automatic media polling where practical. Linux currently +provides no FriiDump-exclusive optical-volume lock equivalent for this path. + +## Xbox XISO + +```bash +./build/src/friidump \ + -d /dev/sr1 \ + -T 4 \ + -X "Red Faction II [TQ00501A].xiso" \ + --firmware-modified \ + "GDR-8163B cross-flashed with modified GDR-8050L firmware; patched to allow 0xE7 memory dumps" +``` + +Omitting the XISO filename on a GDR-8050L derives `Title[MediaID].xiso` from the +unlocked XBE/DMI view on Linux. + +## Xbox redump-style ISO + +```bash +./build/src/friidump \ + -d /dev/sr1 \ + -T 4 \ + -i "Red Faction II [TQ00501A].iso" \ + --firmware-modified \ + "GDR-8163B cross-flashed with modified GDR-8050L firmware; patched to allow 0xE7 memory dumps" +``` + +## Validation boundary + +A Linux build is not considered ready for vendor-command hardware validation +until all of these are recorded: + +- exact executable SHA-256; +- `getcap` output proving `cap_sys_rawio=ep` on that executable; +- normal-user process identity; +- device-node access through the intended group or ACL; +- runtime raw-I/O preflight PASS; +- hardware result, native report, and STOP UNIT result. + +The `--xgd1-layout-probe` and `--xgd1-raw-id-probe` development probes remain +Windows-only in this candidate. Their Linux transport port is tracked +separately from ordinary ISO/XISO user-path parity. + +## Candidate21 GDR-8050L Windows-sequence parity + +The portable Linux path follows the proven Windows state-aware order: readiness, entry capacity, direct `UnlockDrive()`, `RefreshVolume()`, a 2000 ms settle, readiness and capacity verification, with exactly one tray-cycle retry only if the direct handshake remains in the video view. It then sets maximum speed and proceeds to metadata. The same helper is used after video capture. + +`RecoveryKick`, repeated synthetic LBA-zero reads, and generic automatic media-auth kicks are not part of this path. They belong only to a separate experimental modified-firmware campaign and are currently out of scope. + + +## GDR-8050L Xbox handshake transport + +Candidate21 keeps the proven Windows `UnlockDrive()` command sequence but sends +its Linux form with `SG_IO`, the direct Linux SCSI pass-through interface. This +preserves each 6-, 10-, and 12-byte CDB length, the Windows 120-second command +timeouts, the 10-second sticky-descrambling timeout, transfer direction, sense +data, host status, and driver status. Every handshake command emits an +`[XBOX-SGIO]` record to both the live console and persistent FriiDump log. + +This is transport parity only. It does not add `RecoveryKick`, a generic media +authentication kick, synthetic LBA-zero reads, or any modified-firmware-only +recovery behavior. diff --git a/docs/LOCAL_BUILD_VALIDATION.json b/docs/LOCAL_BUILD_VALIDATION.json deleted file mode 100644 index d5bbbdb..0000000 --- a/docs/LOCAL_BUILD_VALIDATION.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "format": "project-frankenstein-friidump-local-build-report", - "version": 1, - "cmake_configure": "pass", - "cmake_build": "pass", - "binary_help_smoke": "pass", - "binary_version_observed": "0.5.3.5", - "help_exit_code": 1, - "warning_present": true, - "warning_location": "libfriidump/rs.c", - "source_modified_to_suppress_warning": false -} diff --git a/docs/NATIVE_REPORTS.md b/docs/NATIVE_REPORTS.md new file mode 100644 index 0000000..c3e5c04 --- /dev/null +++ b/docs/NATIVE_REPORTS.md @@ -0,0 +1,203 @@ +# Native compatibility reports + +FriiDump 0.5.3.16-pf1 creates one native evidence report by default for each +normal dump invocation accepted by the FriiDump Compatibility Database v1.2.0. + +## Default behavior + +No report option is required: + +```text +friidump -d -8 -s -i game.iso +``` + +FriiDump writes the report beside the final FriiDump log and derives its +name from that log filename: + +```text +game.iso.log +game.iso.friidump.json +``` + +The report is written atomically. A derived report name never overwrites prior +evidence; a collision receives the run UUID as a suffix. + +For partial or cancelled dumps, FriiDump's console summary distinguishes the +expected disc size from completed output and computes observed throughput only +from the completed bytes represented by the report. Resumed runs do not claim +throughput because their output size includes data from an earlier invocation. + +## Path overrides + +Override the complete report pathname: + +```text +friidump -d -8 -s -i game.iso \ + --report-json custom-evidence.json +``` + +Override only the destination directory while retaining the derived filename: + +```text +friidump -d -8 -s -i game.iso \ + --report-dir reports +``` + +The options may be combined. In that form, `--report-json` supplies the +filename and `--report-dir` replaces its directory: + +```text +friidump -d -8 -s -i game.iso \ + --report-json ignored/path/custom-evidence.json \ + --report-dir reports +``` + +The final path is `reports/custom-evidence.json`. + +## Modified firmware + +Firmware is assumed to be stock when `--firmware-modified` is omitted. +Modified or cross-flashed firmware must be declared explicitly with a non-empty +explanation: + +```text +friidump -d -8 -s -i game.iso \ + --firmware-modified "patched to allow 0xe7 mem dumps" +``` + +The flag does not require either report-path option because reporting is enabled +by default. FriiDump does not infer modified firmware from a selected profile. + +## Recorded evidence + +Each report uses `friidump-test-result.v1` and records: + +- one UUID for the invocation; +- authoritative UTC start, completion, and generation times; +- FriiDump version and optional full build commit; +- exact live INQUIRY vendor, product, and firmware revision; +- platform, title, region, and disc ID when known; +- for unforced Nintendo runs, GameCube/Wii platform identity from the + unscrambled sector-0 platform magic rather than capacity probing alone; +- seed-stage and dump-stage outcomes; +- actual output sectors, bytes, and elapsed dump time; +- failure stage and sector for partial or failed runs; +- CRC32, MD5, SHA-1, and SHA-256 when hashing was enabled; +- Redump verification independently of compatibility status; +- selected HLDS support tier, CDB offset, and firmware gate evidence; +- an explicit measurement-scope note. + +FriiDump reports observed facts. It does not publish or change a compatibility +assessment. An administrator must accept and link the report in the database. + +## Measurement scopes + +FriiDump emits one of these controlled notes for attempted dumps: + +```text +Measurement scope: full_optical_payload. +Measurement scope: assembled_output. +Measurement scope: partial_progress. +Measurement scope: unknown. +``` + +`full_optical_payload` is used for successful GameCube, Wii, and ordinary DVD +optical reads. Xbox ISO/XISO output uses `assembled_output` because logical or +synthetic output regions make its rate non-comparable to an optical-only read. +A failed run with actual completed bytes uses `partial_progress`. + +A resumed output uses `unknown`: the file size includes bytes from an earlier +invocation, so FriiDump suppresses throughput inference from the current run's +elapsed time. + +## Cancellation + +When native reporting is enabled, Ctrl+C requests a controlled cancellation. +The active dumper stops at the next cancellation boundary, closes the partial +output, and writes an honest report: + +```text +run.test_type = partial_dump +run.result = partial +dump.result = partial +dump.failure_stage = user_cancelled +Measurement scope: partial_progress. +``` + +When no output bytes were completed, cancellation is recorded as a failed +`other` run rather than fabricated partial progress. Some low-level drive +commands are synchronous; cancellation is observed after the active command +returns. + +## Report restrictions + +Native reporting currently requires one normal device dump invocation and +exactly one primary dump output. Combined raw and ISO output is rejected so the +reported output path, bytes, hashes, and `dump_output` artifact remain +unambiguous. It cannot be combined with: + +- raw-file conversion; +- `--allmethods`; +- stop-only operation; +- HLDS research scans or sweeps; +- XGD1 layout or raw-ID research probes. + +Broad INQUIRY, VPD, GET CONFIGURATION, and MODE SENSE discovery belongs in the +separate optical-drive research utility, not FriiDump. + +## Build identity + +CMake builds may embed the exact source commit: + +```bash +cmake -S . -B build \ + -DFRIIDUMP_BUILD_COMMIT= +``` + +The Windows helper uses `FRIIDUMP_BUILD_COMMIT` when supplied, or derives the +current Git `HEAD` when the source tree contains `.git`: + +```powershell +$env:FRIIDUMP_BUILD_COMMIT = "" +.\build_msvc32.cmd +``` + +When no trustworthy commit is supplied, the JSON field is `null` rather than a +guess. + +## Tests + +Linux/CMake fixture build: + +```bash +cmake -S . -B build-native-report-tests \ + -DBUILD_NATIVE_REPORT_TESTS=ON \ + -DBUILD_STATIC_BINARY=ON +cmake --build build-native-report-tests +mkdir -p build-native-report-tests/report-fixtures +./build-native-report-tests/src/friidump-report-fixtures \ + build-native-report-tests/report-fixtures +python3 tests/validate_native_reports.py \ + --fixtures build-native-report-tests/report-fixtures +``` + +Validate one or more live reports without requiring the built-in fixture names: + +```text +python3 tests/validate_native_reports.py \ + --fixtures \ + --allow-arbitrary +``` + +Windows/MSVC fixture build: + +```powershell +.\build_msvc32_native_report_tests.cmd +``` + +Strict validation requires Python's `jsonschema` package. Database semantic +validation can also be run by passing the v1.2.0 `bin/validate-report.php` path +to `tests/validate_native_reports.py`. + +The complete field and acceptance requirements are in +`docs/FRIIDUMP_NATIVE_REPORT_REQUIREMENTS.md`. diff --git a/docs/options b/docs/options index b106e85..d0acedd 100644 --- a/docs/options +++ b/docs/options @@ -1,4 +1,5 @@ -FriiDump 0.5.3.15 - Copyright (C) 2007 Arep +[FRIIDUMP] Log file: friidump.log +FriiDump 0.5.3.16-pf1-candidate21 - Copyright (C) 2007 Arep This software comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions; see COPYING for details. @@ -90,13 +91,26 @@ Available command line options: --scan-dump-prefix Optional raw 0xE7 window dump prefix for --hlds-e7-scan --hlds-profile-report Write selected HLDS profile/evidence JSON --redump-dat-dir Directory containing canonical Redump DAT files - (default: executable-relative redump_dat, then current directory) + (default: bundled executable data, installed share data, then current directory) + Environment override: FRIIDUMP_REDUMP_DAT_DIR --redump-report Write atomic Redump evidence JSON --no-redump-verify Disable automatic post-dump DAT verification --xgd1-layout-probe Read-only locked/unlocked XGD1 boundary probe; writes atomic JSON and does not create an ISO --xgd1-raw-id-probe Modified-firmware cache-flushed, block-aligned raw-ID probe; maps logical LBAs to decoded physical sector IDs + Native .friidump.json reports are created by default + beside the final log using its filename + --report-json Override the complete native report pathname + --report-dir Override only the report destination directory + (may be combined with --report-json) + --firmware-modified Mark firmware modified; omission assumes stock -A, --allmethods Try all known command/method combinations until one works. Reopens the drive for each command so command-specific vendor handlers are rebound. + +Linux raw-I/O requirement: + GC/Wii memory-dump methods and Xbox vendor-unlock paths require effective + CAP_SYS_RAWIO. Apply cap_sys_rawio=ep to the exact validated executable; + do not run FriiDump as root. Rebuilds and replacements clear capabilities. + See docs/LINUX.md and validation/friidump-linux-rawio-capability.sh. diff --git a/docs/reports/FRIIDUMP_CANDIDATE12_LINUX_DAT_AND_LOCK_CORRECTIONS.md b/docs/reports/FRIIDUMP_CANDIDATE12_LINUX_DAT_AND_LOCK_CORRECTIONS.md new file mode 100644 index 0000000..5b0202a --- /dev/null +++ b/docs/reports/FRIIDUMP_CANDIDATE12_LINUX_DAT_AND_LOCK_CORRECTIONS.md @@ -0,0 +1,22 @@ +# FriiDump Candidate12 Linux DAT and lock corrections + +## Accepted live boundary + +Candidate11 completed a full Linux dump of Sonic Mega Collection (US) on an +HL-DT-ST GCC-4244 B101 at `/dev/sr1`. The output matched the established exact +reference hashes. Built-in Redump verification did not run because the CMake +build placed no `redump_dat` resources beside the executable. + +## Candidate12 corrections + +1. CMake copies the bundled DAT resources to `build.../src/redump_dat`. +2. Unix installation places them under `share/friidump/redump_dat`. +3. Runtime lookup order is explicit CLI directory, environment override, + executable-bundled data, installed-share data, then current-directory data. +4. Linux lock status is no longer reported as successful when the transport has + no exclusive-lock implementation. +5. Linux validation captures `--help` output while accepting FriiDump's + intentional exit status 1. + +These corrections do not change HLDS E7 command transport, seed retrieval, dump +geometry, hashing, cancellation, STOP UNIT, or Xbox authentication logic. diff --git a/docs/reports/FRIIDUMP_CANDIDATE13_SEED_DIAGNOSTICS.md b/docs/reports/FRIIDUMP_CANDIDATE13_SEED_DIAGNOSTICS.md new file mode 100644 index 0000000..df20232 --- /dev/null +++ b/docs/reports/FRIIDUMP_CANDIDATE13_SEED_DIAGNOSTICS.md @@ -0,0 +1,48 @@ +# FriiDump Candidate13 Seed Diagnostics + +Candidate13 is a diagnostic successor to Candidate12. It does not intentionally +change the HLDS Method 8 cache/memdump schedule. It makes failures observable in +normal release builds and guarantees cleanup after seed failure. + +## Added evidence + +During the initial twenty Nintendo seed blocks, FriiDump now writes +`[SEED-DIAG]` records to both the console and the ordinary FriiDump log. Records +identify: + +- seed-block number and sector range; +- normal retry or split-recovery profile; +- prefetch/cache-reset, streaming read, header memdump, EDC memdump, or + EDC/unscramble-validation stage; +- transport result; +- operating-system error; +- SCSI status and sense key/ASC/ASCQ; +- the final command descriptor block when a transport command was involved. + +The final classified failure is copied into the native report's `notes` array. +This preserves schema generation 1 while making the failure machine-readable +without requiring a DEBUG or VERBOSE build. + +## Cleanup change + +When Nintendo disc initialization or seed retrieval fails, FriiDump now issues +STOP UNIT and records whether it succeeded. Candidate12 and earlier attempted +STOP UNIT only after a dump had begun. + +## Preserved behavior + +Candidate13 retains Candidate12's: + +- Redump DAT resolver and packaged DAT files; +- truthful Linux exclusive-lock reporting; +- native report contracts; +- Xbox copied-dumper behavior; +- Method 8 cache geometry and retry/split-recovery behavior. + +## Validation boundary + +Static and Linux build validation can prove that all diagnostic stages are +present, transport evidence is captured on Linux and Windows, native reports +remain valid, and existing Redump/Xbox contracts remain intact. One live +GCC-4244 B101 run is required to identify the actual intermittent seed-failure +boundary. diff --git a/docs/reports/FRIIDUMP_CANDIDATE14_LINUX_RAWIO_CAPABILITY.md b/docs/reports/FRIIDUMP_CANDIDATE14_LINUX_RAWIO_CAPABILITY.md new file mode 100644 index 0000000..d19f56e --- /dev/null +++ b/docs/reports/FRIIDUMP_CANDIDATE14_LINUX_RAWIO_CAPABILITY.md @@ -0,0 +1,63 @@ +# FriiDump Candidate14 Linux Raw-I/O Capability Contract + +Candidate14 closes the Linux authorization ambiguity exposed by the Candidate13 +GCC-4244 B101 diagnostic. + +## Evidence that required the correction + +Candidate11 completed an exact Linux GameCube dump while its executable carried: + +```text +cap_sys_rawio=ep +``` + +Candidate12 and Candidate13 executables had no file capability. Candidate13's +instrumented live run showed that ordinary `READ(10)` commands reached the +GCC-4244, while every HLDS `0xE7` memory-read command failed host-side with: + +```text +transport_result=-1 +errno=1 (EPERM) +scsi_status=0xFF +sense=00/00/00 +``` + +The difference was authorization, not media state, drive identity, seed +geometry, or Candidate12's DAT/lock-reporting corrections. + +## Candidate14 behavior + +Candidate14: + +- checks effective `CAP_SYS_RAWIO` before known vendor-command paths; +- refuses the operation before seed retrieval or Xbox unlock when authority is + absent; +- prints exact `setcap` and `getcap` commands for the running executable; +- records the preflight failure in the native report; +- issues STOP UNIT after a failed preflight; +- refuses vendor-command execution when the entire process is root; +- accepts a normal-user process whose exact executable has + `cap_sys_rawio=ep`; +- emits an explicit Linux configure/build warning; +- provides a maintained capability install/status/remove helper; +- documents that rebuilds and file replacement clear capabilities. + +## Security boundary + +The supported configuration is not an unprivileged process. It is a +least-privilege process with one elevated Linux capability attached to one +validated executable. + +Do not: + +- run the whole program with `sudo`; +- install FriiDump setuid-root; +- grant `CAP_SYS_ADMIN` or broader capabilities; +- assume a copied or rebuilt executable retained its xattr; +- apply a capability before verifying the exact executable being tested. + +## Build boundary + +The build itself never invokes `sudo` and never applies a capability silently. +It prints a warning and leaves the binary unprivileged. The explicit capability +phase is a separate installation action requiring user authorization. diff --git a/docs/reports/FRIIDUMP_CANDIDATE15_LINUX_BUILD_HYGIENE.md b/docs/reports/FRIIDUMP_CANDIDATE15_LINUX_BUILD_HYGIENE.md new file mode 100644 index 0000000..f66a6a6 --- /dev/null +++ b/docs/reports/FRIIDUMP_CANDIDATE15_LINUX_BUILD_HYGIENE.md @@ -0,0 +1,33 @@ +# FriiDump Candidate15 Linux Build Hygiene + +## Purpose + +Candidate15 preserves Candidate14's explicit Linux `CAP_SYS_RAWIO` contract and +least-privilege authorization workflow while correcting two issues exposed by +the first user-side Candidate14 build. + +## Corrections + +1. The Linux validator now clamps only source files whose modification times are + in the future before CMake configuration. This prevents archive time-zone + metadata from causing GNU Make `Clock skew detected` warnings. +2. The production raw-I/O preflight no longer uses a potentially truncating + `snprintf` fallback for the executable path. +3. The native-report authorization note buffer was enlarged so the complete + supported Linux path length fits without `-Wformat-truncation` diagnostics. +4. Source-package ZIP member timestamps are normalized to a stable past date. + +## Preserved authorization behavior + +- A normal Linux build never silently runs `sudo` or installs capabilities. +- Vendor-command paths require effective `CAP_SYS_RAWIO`. +- FriiDump refuses whole-process root execution for those paths. +- The explicit capability phase applies only `cap_sys_rawio=ep` to the exact + validated executable and verifies it with `getcap`. +- Rebuilding or replacing the executable clears the capability. + +## Acceptance boundary + +Candidate15 requires clean Linux and Windows builds, explicit capability +installation on the exact Linux executable, and one short GCC-4244 B101 live +smoke proving the runtime preflight and seed path. diff --git a/docs/reports/FRIIDUMP_CANDIDATE16_LINUX_XBOX_TRAY_CYCLE.md b/docs/reports/FRIIDUMP_CANDIDATE16_LINUX_XBOX_TRAY_CYCLE.md new file mode 100644 index 0000000..1ea277b --- /dev/null +++ b/docs/reports/FRIIDUMP_CANDIDATE16_LINUX_XBOX_TRAY_CYCLE.md @@ -0,0 +1,38 @@ +# FriiDump Candidate16 Linux Xbox Tray-Cycle Correction + +## Trigger + +The first Candidate15 Linux Xbox smoke selected the intended USB GDR-8050L +identity and passed the `CAP_SYS_RAWIO` preflight, but the inserted media was a +normal video DVD. The challenge handshake therefore could not establish an Xbox +game view. Stage 2 then failed immediately on the required software eject, and +the tray remained locked after FriiDump exited. + +## Root cause + +Linux optical devices may use the generic CD-ROM layer's `CDO_LOCK` behavior +while a device file is open. Candidate15 sent raw START STOP UNIT eject/load +commands without first releasing that door lock. The initial eject therefore +failed before the media cycle could begin. + +## Correction + +Candidate16 uses the documented `CDROM_LOCKDOOR` ioctl on Linux: + +1. release the optical door lock before software eject; +2. send START STOP UNIT eject; +3. wait three seconds; +4. send START STOP UNIT load; +5. poll readiness and settle; +6. restore the door lock only after a successful load. + +On eject, load, or readiness failure, FriiDump leaves the door unlocked and +prints an always-visible recovery diagnostic. + +## Acceptance boundary + +Candidate16 requires clean Linux and Windows builds, explicit +`cap_sys_rawio=ep` installation on the exact Linux executable, and a physical +GDR-8163B/GDR-8050L Linux Xbox smoke using an Original Xbox retail disc. The +smoke must prove both required media cycles, metadata, unlocked game-data reads, +controlled cancellation, native reporting, and STOP UNIT. diff --git a/docs/reports/FRIIDUMP_CANDIDATE18_WINDOWS_SEQUENCE_PARITY.md b/docs/reports/FRIIDUMP_CANDIDATE18_WINDOWS_SEQUENCE_PARITY.md new file mode 100644 index 0000000..7982bb1 --- /dev/null +++ b/docs/reports/FRIIDUMP_CANDIDATE18_WINDOWS_SEQUENCE_PARITY.md @@ -0,0 +1,17 @@ +# FriiDump Candidate18: Windows Sequence Parity + +Candidate17 is rejected and must not be tested or promoted. It incorrectly generalized `RecoveryKick` from an experimental modified GDR-8050L 0012 firmware path. + +Candidate18 returns to Candidate16 and ports the proven Windows state-aware sequence into the portable Linux path: + +1. `EnsureDriveReady(30000)`; +2. entry `READ CAPACITY`; +3. if already in game view, skip redundant handshake and tray cycle; +4. otherwise run `UnlockDrive()` directly; +5. `RefreshVolume()`, 2000 ms settle, readiness, and capacity observation; +6. only when still in video view, perform one tray cycle and repeat the handshake plus refresh/readiness/capacity sequence; +7. fail if the game view is still not established; +8. request maximum speed; +9. acquire the volume lock where supported and probe metadata. + +The same state-aware helper is used after visible-video capture to re-establish the game view. Generic `RecoveryKick`, repeated LBA-zero reads, and automatic media-auth kicks are removed from both portable and copied Windows dump paths. Experimental modified-firmware recovery remains out of scope. diff --git a/docs/reports/FRIIDUMP_CANDIDATE19_MSVC_ARITY_CORRECTION.md b/docs/reports/FRIIDUMP_CANDIDATE19_MSVC_ARITY_CORRECTION.md new file mode 100644 index 0000000..1859a73 --- /dev/null +++ b/docs/reports/FRIIDUMP_CANDIDATE19_MSVC_ARITY_CORRECTION.md @@ -0,0 +1,19 @@ +# FriiDump Candidate19: MSVC Xbox Helper Arity Correction + +Candidate18 is withdrawn and must not be tested or promoted. Its copied Windows +Xbox helper `DumpSectorRangeWithRetry` was reduced from eight parameters to +seven when generic `RecoveryKick` behavior was removed, but three callers still +passed the removed `authRecovery` argument. Visual Studio emitted C4020 (`too +many actual parameters`) at the GAME-XISO lead-in, GAME-XISO, and XISO calls. + +Candidate19 preserves Candidate18's intended Windows-sequence parity and makes +only the required API-boundary correction: + +- remove the stale eighth argument from all three callers; +- verify every `DumpSectorRangeWithRetry` call has exactly seven arguments; +- add `/we4020` to the production MSVC response file so this warning is fatal. + +No `RecoveryKick`, synthetic LBA-zero cadence, or generic media-authentication +kick is restored. The Linux GDR-8050L behavior remains the Candidate18 design: +direct state-aware handshake first, one tray-cycle retry only when capacity +still proves video view, and capacity/XDVDFS as authority. diff --git a/docs/reports/FRIIDUMP_CANDIDATE20_LINUX_XBOX_SGIO_PARITY.md b/docs/reports/FRIIDUMP_CANDIDATE20_LINUX_XBOX_SGIO_PARITY.md new file mode 100644 index 0000000..d121fa0 --- /dev/null +++ b/docs/reports/FRIIDUMP_CANDIDATE20_LINUX_XBOX_SGIO_PARITY.md @@ -0,0 +1,30 @@ +# FriiDump Candidate20: Linux Xbox SG_IO Transport Parity + +Candidate19 reproduced the Windows state-machine order, but its Linux +`UnlockDrive()` implementation still sent the challenge sequence through the +legacy `CDROM_SEND_PACKET` wrapper. That wrapper does not carry an explicit CDB +length and applied FriiDump's generic 10-second timeout to every command. + +The physical Candidate19 run remained at the 6,992-sector DVD-video view after +both the direct handshake and the one permitted tray-cycle retry. Because the +mode-select transport statuses were not recorded, Candidate19 could not prove +that the partition-unlock and sticky-descrambling commands reached the USB +target with Windows-equivalent boundaries. + +Candidate20 keeps the exact Candidate19 state-machine order and replaces only +the Linux GDR-8050L handshake transport with `SG_IO`: + +- explicit 10-byte READ CAPACITY; +- explicit 10-byte MODE SENSE and MODE SELECT; +- explicit 12-byte challenge-table commands; +- explicit 6-byte sticky-descrambling MODE SELECT; +- 120-second Windows-equivalent timeout for steps 1-8 and 10; +- 10-second timeout for sticky descrambling; +- data-in/data-out direction preserved; +- SCSI status, host status, driver status, sense, residual, timeout, CDB, and + transfer length logged for every step. + +The tray-cycle messages now use the persistent Xbox reference logger. + +Candidate20 does not contain or invoke `RecoveryKick`, generic media-auth kicks, +or synthetic LBA-zero recovery reads. diff --git a/docs/reports/FRIIDUMP_CANDIDATE21_XBOX_DMI_CDB_BOUNDARY.md b/docs/reports/FRIIDUMP_CANDIDATE21_XBOX_DMI_CDB_BOUNDARY.md new file mode 100644 index 0000000..8278615 --- /dev/null +++ b/docs/reports/FRIIDUMP_CANDIDATE21_XBOX_DMI_CDB_BOUNDARY.md @@ -0,0 +1,78 @@ +# FriiDump Candidate21 Xbox DMI CDB boundary correction + +## Candidate20 live evidence + +Candidate20 successfully established the Linux GDR-8050L Xbox game view twice +using direct SG_IO and the proven Windows state-machine order. + +Observed on the USB `HL-DT-ST DVD-ROM GDR8050L 0012` target: + +- entry video-view capacity: `6992` sectors; +- first final game-view capacity: `3431264` sectors; +- second final game-view capacity: `3431264` sectors; +- XBE title: `Red Faction® II`; +- XBE GameRegion: `0x00000001` (`North America`); +- game lead-in marker reached; +- GAME-XDVDFS output growth confirmed; +- controlled partial bytes: `482115584`; +- partial SHA-256: + `5ffb1a61e0bfa6109ecaeb3bad23f0e17e27ccfbb311c489fe03e6dbbb687a89`; +- STOP UNIT: PASS. + +Evidence SHA-256: + +`f82ceb50ca7e41d3a9c962557b2dadb47276c767da756721bb9e4337572742d2` + +## Remaining failure + +The media ID was empty even though the known disc identity is `TQ00501A`. + +The portable helper built READ DVD STRUCTURE as: + +```c +mmc.cmd[11] = format; +``` + +Byte 11 is the Control field. Standard READ DVD STRUCTURE places Format in CDB +byte 7. The copied Windows `GetMediaID()` request already uses: + +```c +sptd.Cdb[7] = 0x04; +``` + +Therefore Candidate20's DMI request did not match the proven Windows request. + +## Candidate21 correction + +Candidate21 changes the portable command to: + +```c +mmc.cmd[6] = layer; +mmc.cmd[7] = format; +mmc.cmd[8] = allocation_length_msb; +mmc.cmd[9] = allocation_length_lsb; +mmc.cmdlen = 12; +``` + +CDB byte 11 remains zero. + +It also emits persistent capture evidence: + +```text +[XBOX-DVD-STRUCTURE] format=0x00 name=PFI cdb_format_byte=7 result=PASS +[XBOX-DVD-STRUCTURE] format=0x04 name=DMI cdb_format_byte=7 result=PASS +``` + +## Sticky descrambling interpretation + +Candidate20's step 9 MODE SELECT(6) returned `05/81/00` on both handshakes. +This remains visible as a transport failure. It is not accepted as proof of +success. Instead, the authoritative proof is the subsequent 3,431,264-sector +READ CAPACITY together with readable XDVDFS data. No command is removed, +reordered, hidden, or retried through RecoveryKick. + +## Scope + +Candidate21 changes neither the successful SG_IO challenge sequence nor the +Windows state-machine order. It adds no RecoveryKick, generic media-auth kick, +or synthetic LBA-zero recovery reads. diff --git a/docs/reports/FRIIDUMP_NATIVE_COMPATIBILITY_REPORTING_0.5.3.16.md b/docs/reports/FRIIDUMP_NATIVE_COMPATIBILITY_REPORTING_0.5.3.16.md new file mode 100644 index 0000000..5f86355 --- /dev/null +++ b/docs/reports/FRIIDUMP_NATIVE_COMPATIBILITY_REPORTING_0.5.3.16.md @@ -0,0 +1,121 @@ +# FriiDump 0.5.3.16-pf1 native compatibility reporting + +## Status + +This is a source candidate for Windows and physical-drive validation. It is not +a promoted release and does not replace `v0.5.3.15-pf1`. + +## Scope + +The change adds native `friidump-test-result.v1` generation to normal dump +invocations. It deliberately excludes broad drive-research discovery; that +work remains assigned to a separate future utility. + +## User-visible behavior + +Native reports are created by default for normal dump invocations. The default +location is beside the final FriiDump log and the filename is derived from +that log filename. + +```text +--report-json Override the complete report pathname +--report-dir Override only the destination directory +--firmware-modified Declare modified firmware; omission assumes stock +``` + +`--report-json` and `--report-dir` may be combined; the directory override is +applied to the filename supplied by `--report-json`. Derived-name collisions +receive the run UUID, while an explicit pathname is never overwritten. Native +reporting requires exactly one primary dump output; combined raw and ISO output +is rejected to keep output identity unambiguous. FriiDump does not infer +modified firmware from the selected drive profile. + +## Implementation boundaries + +- `src/native_report.c/.h` owns report state, validation, JSON escaping, UUIDs, + path resolution, and atomic publication. +- `src/friidump.c` maps live FriiDump state into the report. +- `libfriidump/disc.c/.h` exposes exact live INQUIRY identity already held by + the drive layer. +- `libfriidump/dumper.c/.h` exposes resume state and controlled-cancellation + status without moving report serialization into the library. +- Existing dumping, Redump verification, and HLDS profile code remain the + authorities for their observed results. + +## Evidence behavior + +The report records native evidence only. It contains no reconstruction +converter identity and does not claim a public compatibility status. + +Complete, partial, failed, cancelled, seed-failure, no-media, stock-firmware, +and modified-firmware states are represented without inventing missing values. + +Output size is measured from the actual output file. The report includes one +`dump_output` artifact whose path, byte count, and SHA-256 are cross-checked +against the primary dump fields. Resumed outputs are marked with measurement +scope `unknown` because file bytes may predate the current invocation, and +streaming hashes are suppressed because they cover only appended bytes. Xbox +logical/reconstructed output uses `assembled_output`; normal successful +GC/Wii/DVD output uses `full_optical_payload`. + +## Atomicity and identity + +- One version-4 UUID is generated per invocation. +- The report is UTF-8 JSON with non-ASCII input safely escaped. +- Temporary output is flushed and committed before publication. +- POSIX uses hard-link publication to enforce no-overwrite atomically. +- Windows uses `MoveFileExA` without replace-existing semantics. +- Stale per-run temporary files are removed only when they match the same UUID. + +## Cancellation + +When reporting is enabled, SIGINT/Ctrl+C becomes a controlled cancellation +request. FriiDump exits the active dump loop at the next boundary, finalizes the +partial stream, closes it, and records `user_cancelled`. Research, conversion, all-methods, and stop-only operations remain outside native reporting. + +## Build integration + +- CMake builds `native_report.c` and links `ole32` on Windows for UUIDs. +- `FRIIDUMP_BUILD_COMMIT` accepts only a full 40-character hexadecimal commit. +- The maintained MSVC response file includes the report module and `ole32.lib`. +- `build_msvc32.cmd` embeds an explicit commit or derives Git `HEAD`. +- `build_msvc32_native_report_tests.cmd` builds and executes the report fixture + generator on Windows. + +## Automated fixtures + +The source includes fixtures for: + +- successful GameCube optical dump; +- successful Xbox assembled output; +- partial read/write failure; +- controlled user cancellation; +- seed failure; +- no-media diagnostic; +- modified firmware; +- unique retry UUID and no-overwrite policy; +- recursive report-directory creation; +- rejection of invalid UUID, modified firmware without a note, successful + dumps without sector/byte counts, incoherent attempted-state results, missing + output artifacts, and mismatched artifact byte counts; +- preservation of valid UTF-8 text in JSON output. + +The fixtures are validated against the exact Compatibility Database v1.2.0 JSON +Schema. The database PHP semantic validator is used during package validation. + +## Remaining promotion gates + +Before a release can be promoted: + +1. Build with the maintained Visual Studio 2019 32-bit toolchain. +2. Run the MSVC fixture tests. +3. Validate no-media reporting on a real drive. +4. Validate a complete GameCube or Wii report. +5. Validate a controlled partial cancellation. +6. Validate Xbox assembled-output reporting on the applicable hardware path. + The copied GDR-8050L reference dumper remains a special Windows-only path; + complete output reporting must be validated there, and Ctrl+C cancellation + must not be claimed for that path until its copied loops receive an explicit + cancellation callback. +7. Submit generated reports to the live v1.2.0 database and confirm acceptance. +8. Review exact report contents before assigning a release tag. diff --git a/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE10_XBOX_PARTIAL_FINALIZATION.md b/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE10_XBOX_PARTIAL_FINALIZATION.md new file mode 100644 index 0000000..56f0de4 --- /dev/null +++ b/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE10_XBOX_PARTIAL_FINALIZATION.md @@ -0,0 +1,49 @@ +# FriiDump candidate10 Xbox partial-finalization correction + +Candidate9 proved controlled cancellation and XBE-derived region handoff on +the copied GDR-8050L XISO path. Its native report was structurally correct, +but the human-readable summary still used the pre-XBE placeholders +`XB`/`Xbox DVD`, displayed the source game-view geometry as though it were +the expected XISO output geometry, and left hashes null for a cleanly closed +partial output. + +Candidate10 corrects those remaining boundaries. + +## Identity + +- Version: `0.5.3.16-pf1-candidate10` +- Implementation identity: `b03b227673ee00aec3c29d34038b3c86ee2d8b9a` +- Tree identity: `856bf3d1cb5e892f9968be940757715fbc890b32` +- Logical parent: candidate9 + +## Summary metadata + +When the copied Xbox result is available, the validation summary now uses: + +- XBE title; +- media ID; +- formatted XBE GameRegion; +- source game-view sectors; +- expected logical output sectors. + +For the validated Red Faction II XISO path this becomes: + +```text +Game/Media ID.....: TQ00501A +Title.............: Red Faction® II +Region............: North America +Source sectors....: 3431264 +Expected output...: 1913920 sectors +``` + +## Partial output hashing + +A controlled Xbox cancellation now: + +1. flushes and commits the partial output; +2. closes the file; +3. recalculates CRC32, MD5, SHA-1, and SHA-256 over the exact partial file; +4. returns the hashes through `xbox_ref_dump_result`; +5. serializes them into both the report hash object and the dump artifact. + +The schema remains `friidump-test-result.v1`. diff --git a/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE5_CORRECTIONS.md b/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE5_CORRECTIONS.md new file mode 100644 index 0000000..cb1da68 --- /dev/null +++ b/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE5_CORRECTIONS.md @@ -0,0 +1,65 @@ +# FriiDump native-report candidate5 corrections + +## Evidence basis + +Candidate4 completed its Windows MSVC build, all 12 native-report fixtures, +JSON Schema validation, semantic validation, and help/options smoke. A live +stock-firmware `HL-DT-ST CDRW/DVD GCC4244 B101` run then retrieved a disc seed +successfully and was cancelled after 561 sectors. + +The atomic `friidump-test-result.v1` report was valid and correctly recorded: + +- `partial_dump` / `partial`; +- `failure_stage: user_cancelled`; +- 561 completed sectors; +- 1,148,928 completed bytes; +- 1.169 seconds; +- hashes and artifact identity for the exact partial output; +- `Measurement scope: partial_progress`. + +Two user-facing defects remained: + +1. The default report used the log-derived filename but discarded the final + log directory, so an absolute output/log path still produced the report in + FriiDump's process working directory. +2. The HLDS console summary divided the full expected disc size by the partial + elapsed time, producing a meaningless throughput value and labeling the + expected disc sector count as if it were completed output. + +## Candidate5 behavior + +Without a path override, the report is now created beside the final FriiDump +log. A basename-only log still places the report in the process working +directory. + +```text +D:\captures\test.iso.log +D:\captures\test.iso.friidump.json +``` + +The existing override rules remain unchanged: + +```text +--report-json Override the complete report pathname +--report-dir Override only the destination directory +--firmware-modified Mark firmware modified; omission assumes stock +``` + +For partial or cancelled HLDS runs, the console summary now separates: + +- expected disc sectors; +- completed output sectors; +- completed output bytes; +- cancellation sector versus a failed 16-sector read range; +- throughput calculated only from completed bytes and the measured duration. + +For the observed 561-sector result, the correct measured payload is +1.095703125 MiB and the corresponding average at 1.169 seconds is about +3374.28 MiB/h. Resumed runs continue to suppress throughput because their +output byte count includes data from an earlier invocation. + +## Unchanged contract + +The `friidump-test-result.v1` JSON contract is unchanged. The candidate4 live +report already represented the partial output correctly; candidate5 corrects +report placement and the human-readable console summary. diff --git a/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE6_PLATFORM_CLASSIFICATION.md b/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE6_PLATFORM_CLASSIFICATION.md new file mode 100644 index 0000000..0755bd1 --- /dev/null +++ b/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE6_PLATFORM_CLASSIFICATION.md @@ -0,0 +1,54 @@ +# FriiDump native-report candidate6 platform classification + +## Live evidence + +Candidate5 was tested with a stock `HL-DT-ST CDRW/DVD GCC4244` firmware +`B101` and a retail copy of *Super Smash Bros. Melee*. + +The partial ISO begins with disc ID `GALE01`. Its first 32 bytes contain: + +- Wii magic at byte `0x18`: `0x00000000` +- GameCube magic at byte `0x1c`: `0xc2339f3d` + +Candidate5 nevertheless reported `Wii_DL`, 4,155,840 expected sectors, +`Contains update: Yes`, and native-report platform `wii`. The legacy +capacity classifier had accepted an out-of-range dummy read instead of +receiving the expected LBA-out-of-range sense response. + +Candidate5's other live corrections passed: + +- the native report was written beside the retargeted log; +- cancellation was represented as partial output; +- completed sectors and bytes matched the file; +- throughput used completed output bytes and measured dump duration. + +## Candidate6 correction + +For automatic GameCube/Wii detection, FriiDump now reclassifies the disc +after unscrambling sector 0: + +1. `0x5d1c9ea3` at byte `0x18` means Wii. +2. Otherwise, `0xc2339f3d` at byte `0x1c` means GameCube. +3. Otherwise, the earlier classifier result is retained. + +A GameCube header sets: + +- disc type `GameCube`; +- expected sector count `712880`, unless `-S` was explicitly supplied; +- native-report platform `gamecube`; +- no Wii update-partition claim. + +Explicit `-T` selections are never overridden. + +## Validation + +The shared fixture executable now tests: + +- captured `GALE01`-shaped GameCube header classification; +- Wii header classification; +- unknown magic; +- truncated input. + +The Windows live-report validator also reads the first 32 bytes of a +cancelled output and requires the native report, console disc type, and +expected geometry to agree with the platform magic. diff --git a/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE7_XBOX_REGION.md b/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE7_XBOX_REGION.md new file mode 100644 index 0000000..65bc710 --- /dev/null +++ b/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE7_XBOX_REGION.md @@ -0,0 +1,39 @@ +# FriiDump candidate8 Xbox region correction + +Candidate6 completed the Red Faction II Xbox acquisition successfully but +serialized `Europe/PAL` in the native report. That value did not describe the +Xbox title. + +The copied Xbox metadata path had already parsed the `default.xbe` certificate +and printed: + +```text +Regions: North America +``` + +The stale report value came from the locked DVD-video view's sector-zero byte +3, interpreted through FriiDump's Nintendo `P`/`E`/`J` region mapping. That +mapping is valid for GameCube/Wii disc IDs, not Original Xbox XBE certificates. + +Candidate7 changes the source of Xbox region metadata: + +- Xbox region starts as unknown/null instead of inheriting PAL/NTSC from the + DVD-video view. +- The copied GDR-8050L path carries the XBE certificate `GameRegion` mask back + through `xbox_ref_dump_result`. +- The native report uses an Xbox-specific formatter. +- The raw mask is retained in a report note. + +For Red Faction II [TQ00501A], the expected result is: + +```json +"region": "North America" +``` + +and: + +```text +Xbox XBE GameRegion mask: 0x00000001. +``` + +The `friidump-test-result.v1` schema is unchanged. diff --git a/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE8_MSVC_BOOL_FIX.md b/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE8_MSVC_BOOL_FIX.md new file mode 100644 index 0000000..7832e50 --- /dev/null +++ b/docs/reports/FRIIDUMP_NATIVE_REPORT_CANDIDATE8_MSVC_BOOL_FIX.md @@ -0,0 +1,28 @@ +# FriiDump candidate8 MSVC bool-collision correction + +Candidate7 correctly implemented Original Xbox XBE `GameRegion` reporting, but its first Visual Studio 2019 x86 build failed before linking. + +## Failure + +```text +libfriidump/xbox_ref/utils.h(47): error C2632: 'int' followed by 'bool' is illegal +``` + +`xbox_ref/xbe_cert.h` includes `xbox_region.h`. Candidate7's `xbox_region.h` included ``, while the copied Xbox support header later declared: + +```c +typedef int bool; +``` + +Under MSVC this created an incompatible duplicate/macro-expanded `bool` declaration. + +## Correction + +The small region-formatting API now uses `int` for its success return and internal boolean state. It no longer includes `` and therefore does not modify the legacy copied Xbox header environment. Region mask meanings and output strings are unchanged. + +## Identity + +- Version: `0.5.3.16-pf1-candidate8` +- Implementation commit: `a2e1cdfd35978547096ba072eef8bd60cd952955` +- Implementation tree: `649bc746a690b96e19e88d66f5d979e5c2888633` +- Logical parent: candidate7 diff --git a/friidump-handoff.md b/friidump-handoff.md index 93f3c75..4be8970 100644 --- a/friidump-handoff.md +++ b/friidump-handoff.md @@ -1,101 +1,37 @@ +# FriiDump Candidate21 Handoff -# FriiDump / GC-Wii Work Handoff +Candidate20 established the Linux Xbox game view twice on the USB +`HL-DT-ST DVD-ROM GDR8050L 0012`, read Red Faction II XDVDFS data, captured the +32-sector game lead-in, produced a 482,115,584-byte controlled partial image, +hashed it, wrote an atomic native report, and completed STOP UNIT. -**Purpose:** Continue the current FriiDump + Project Frankenstein GameCube/Wii support work without requiring the user to re-explain project context. +Candidate20 evidence SHA-256: -## Scope +`f82ceb50ca7e41d3a9c962557b2dadb47276c767da756721bb9e4337572742d2` -This is **not** a project specification. It is a handoff for the next engineering agent. +Two acceptance findings remained: -## Current Objective +1. Step 9 sticky descrambling returned CHECK CONDITION `05/81/00`, but both + final capacity checks reported 3,431,264 sectors and XDVDFS data was + readable. The transport result remains logged as FAIL, while the live + acceptance policy treats it as non-blocking when game-view proof succeeds, + matching the copied Windows path's outcome-based authority. +2. Media ID was empty. The portable READ DVD STRUCTURE helper placed Format in + CDB byte 11 instead of byte 7. The copied Windows `GetMediaID()` path uses + byte 7. Candidate21 corrects this boundary and adds persistent + `[XBOX-DVD-STRUCTURE]` PFI/DMI diagnostics. -Use Project Frankenstein Stage5B firmware analysis to improve FriiDump's HLDS GameCube/Wii support, then validate every supported profile on real hardware. +Candidate21 does not add RecoveryKick or any generic recovery/authentication +sequence. -The workflow is: +Validation order: -1. Recover parser information from firmware (Stage5B). -2. Integrate into FriiDump. -3. Validate on owned hardware. -4. Feed results back into the analyzer. +```bash +bash ./validation/friidump-v0.5.3.16-pf1-candidate21-linux-live-validation.sh build +bash ./validation/friidump-v0.5.3.16-pf1-candidate21-linux-live-validation.sh capability +``` -## Major accomplishments - -- Added analyzer-derived HLDS profile support. -- Added media preflight before vendor seed retrieval. -- Added STOP UNIT after successful dumps. -- Added profile reporting. -- Added live INQUIRY alias support. -- Validated multiple GDR-8163B firmware revisions on real hardware. -- Validated GCC-4244 B101 and B103 on real hardware. -- Validated GCC-4243N A102 on real hardware. -- Captured canonical firmware dumps for GCC-4243N A102 and GCC-4244 B101. - -## Important engineering observations - -- Trust live firmware/INQUIRY over stickers. -- Preserve firmware provenance. -- Prefer exact Stage5B-derived parser profiles over legacy Type3 fallbacks. -- Hardware validates analyzer discoveries. - -## Current hardware validation status - -### Validated - -**GDR-8163B** -- 0B30 -- 0D20 -- 0E15 -- 0L20 -- 0L23 -- 0L30 -- 0M26 - -**GCC-4244** -- B101 -- B103 - -**GCC-4243N** -- A102 - -## Firmware dumps completed - -- GCC-4243N A102 (canonical) -- GCC-4244 B101 (canonical) - -These should be analyzed by Stage5B to determine whether new promoted parser profiles or aliases are appropriate. - -## Owned hardware inventory (validation targets) - -- GDR-8163B x15 -- GDR-8081N x3 -- GCC-4241N x5 -- GCC-4243N x5 -- GCC-4244N x9 -- GCC-4160N x3 -- GWA-4164B x1 -- GSA-H73N x1 -- GH22NS30 x1 - -Do **not** ask the user to test drives outside this inventory. - -## Immediate next work - -1. Analyze GCC-4243N A102 firmware. -2. Analyze GCC-4244 B101 firmware. -3. Update FriiDump with any new aliases/promoted profiles. -4. Continue owned-hardware validation in this order: - - GCC-4241N - - GCC-4160N - - GWA-4164B - - GSA-H73N - - GH22NS30 - -GDR-8081N remains a separate research effort and should not be modified until its firmware evidence supports doing so. - -## Notes for the next agent - -- Avoid re-implementing already completed features (media preflight, STOP UNIT, profile reporting). -- Use analyzer results as the primary source of truth. -- Prefer evidence over assumptions. -- When suggesting validation work, consult the user's current hardware inventory first. -- The long-term architectural document is PFES; this file is only a transition handoff. +Windows MSVC validation is required before the final physical Linux Xbox smoke. +The final smoke must report media ID `TQ00501A`, successful DMI capture, both +game-view transitions, controlled cancellation, hashes, native report, and +STOP UNIT. diff --git a/libfriidump/CMakeLists.txt b/libfriidump/CMakeLists.txt index 0670c87..e2b414e 100644 --- a/libfriidump/CMakeLists.txt +++ b/libfriidump/CMakeLists.txt @@ -22,6 +22,8 @@ add_library ( lite-on.c misc.h misc.c + nintendo_disc_header.h + nintendo_disc_header.c renesas.c rs.h rs.c @@ -31,6 +33,10 @@ add_library ( vanilla_2384.c win32compat.h win32compat.c + xbox_region.h + xbox_region.c + xbox_portable.h + xbox_portable.c xbox_ref_bridge.h xbox_ref_bridge.c xbox_xgd1_raw_id_probe.c diff --git a/libfriidump/disc.c b/libfriidump/disc.c index c132d59..6e90138 100644 --- a/libfriidump/disc.c +++ b/libfriidump/disc.c @@ -44,6 +44,8 @@ #include "byteorder.h" #include "disc.h" #include "dvd_drive.h" +#include "nintendo_disc_header.h" +#include "xbox_ref/xbox_ref_log.h" static void hlds_e7_visible_probe_log (const char *fmt, ...) { static bool started = false; @@ -122,12 +124,109 @@ struct disc_s { /* Read cache */ u_int32_t cache_size; //!< The number of blocks that will be cached when read. bool hlds_e7_read_schedule_logged; //!< True once the selected HLDS 0xE7 read schedule has been logged for this run. + bool seed_diagnostic_active; //!< True while the initial 20 Nintendo seed blocks are being acquired. + unsigned int seed_diagnostic_block; //!< Zero-based seed-block index currently being acquired. + disc_seed_diagnostic seed_diagnostic; //!< Last actionable seed/cache failure evidence. u_int8_t **raw_cache; //!< Memory area for raw sectors cache. u_int8_t **cache; //!< Memory area for unscrambled sectors cache. u_int32_t *cache_map; //!< Data structure used by the caching system to know which blocks are in memory. }; + +static void disc_seed_diag_clear (disc *d) { + if (!d) + return; + memset (&d -> seed_diagnostic, 0, sizeof (d -> seed_diagnostic)); +} + +static void disc_seed_diag_log (disc *d, const char *fmt, ...) { + va_list ap; + if (!d || !d -> seed_diagnostic_active || !fmt) + return; + xbox_ref_log_fprintf (stderr, "[SEED-DIAG] "); + va_start (ap, fmt); + /* xbox_ref_log_fprintf is variadic rather than vfprintf-based, so format + * into a bounded buffer before sending the line to both console and log. */ + { + char line[768]; + vsnprintf (line, sizeof (line), fmt, ap); + xbox_ref_log_fprintf (stderr, "%s\n", line); + } + va_end (ap); +} + +static void disc_seed_diag_fail ( + disc *d, + u_int32_t sector_start, + u_int32_t sector_end, + int retry, + const char *stage, + const char *detail, + int transport_result, + bool capture_command +) { + dvd_command_diagnostic command; + char cdb_text[3 * 12 + 1]; + int i; + int used; + + if (!d || !d -> seed_diagnostic_active) + return; + disc_seed_diag_clear (d); + d -> seed_diagnostic.valid = true; + d -> seed_diagnostic.seed_block_index = d -> seed_diagnostic_block; + d -> seed_diagnostic.sector_start = sector_start; + d -> seed_diagnostic.sector_end = sector_end; + d -> seed_diagnostic.retry = retry; + snprintf (d -> seed_diagnostic.stage, sizeof (d -> seed_diagnostic.stage), "%s", stage ? stage : "unknown"); + snprintf (d -> seed_diagnostic.detail, sizeof (d -> seed_diagnostic.detail), "%s", detail ? detail : ""); + d -> seed_diagnostic.transport_result = transport_result; + + memset (&command, 0, sizeof (command)); + if (capture_command && dvd_get_last_command_diagnostic (d -> dvd, &command)) { + d -> seed_diagnostic.transport_result = command.transport_result; + d -> seed_diagnostic.os_error = command.os_error; + d -> seed_diagnostic.scsi_status = command.scsi_status; + d -> seed_diagnostic.sense_key = command.sense_key; + d -> seed_diagnostic.asc = command.asc; + d -> seed_diagnostic.ascq = command.ascq; + d -> seed_diagnostic.cdb_length = command.cdb_length; + memcpy (d -> seed_diagnostic.cdb, command.cdb, sizeof (d -> seed_diagnostic.cdb)); + } + + cdb_text[0] = '\0'; + used = 0; + for (i = 0; i < d -> seed_diagnostic.cdb_length && i < 12; i++) { + int wrote = snprintf (cdb_text + used, sizeof (cdb_text) - (size_t) used, + "%s%02X", i ? " " : "", d -> seed_diagnostic.cdb[i]); + if (wrote < 0 || wrote >= (int) (sizeof (cdb_text) - (size_t) used)) + break; + used += wrote; + } + + disc_seed_diag_log (d, + "block=%u/20 sectors=%u..%u retry=%d stage=%s result=FAIL rc=%d os=%d scsi=0x%02X sense=%02X/%02X/%02X cdb=[%s] detail=%s", + d -> seed_diagnostic.seed_block_index + 1, + sector_start, sector_end, retry, + d -> seed_diagnostic.stage, + d -> seed_diagnostic.transport_result, + d -> seed_diagnostic.os_error, + d -> seed_diagnostic.scsi_status & 0xff, + d -> seed_diagnostic.sense_key & 0xff, + d -> seed_diagnostic.asc & 0xff, + d -> seed_diagnostic.ascq & 0xff, + cdb_text[0] ? cdb_text : "none", + d -> seed_diagnostic.detail[0] ? d -> seed_diagnostic.detail : "none"); +} + +bool disc_get_seed_diagnostic (disc *d, disc_seed_diagnostic *out) { + if (!d || !out || !d -> seed_diagnostic.valid) + return false; + *out = d -> seed_diagnostic; + return true; +} + static void disc_cache_init (disc *d, u_int32_t size) { u_int32_t i; @@ -522,6 +621,8 @@ static bool disc_read_sector_8_split_recover_block (disc *d, u_int32_t block_sec u_int8_t raw_block[RAW_BLOCK_SIZE]; u_int8_t iso_block[BLOCK_SIZE]; u_int8_t readbuf[BLOCK_SIZE]; + char detail[160]; + req_sense sense; block_no = block_sector / SECTORS_PER_BLOCK; @@ -530,22 +631,42 @@ static bool disc_read_sector_8_split_recover_block (disc *d, u_int32_t block_sec memset (raw_block, 0, sizeof (raw_block)); memset (iso_block, 0, sizeof (iso_block)); out = true; + disc_seed_diag_clear (d); warning ("Method 8 split recovery: trying %d-sector chunks for sectors %u..%u", chunk_len, block_sector, block_sector + SECTORS_PER_BLOCK - 1); + disc_seed_diag_log (d, + "block=%u/20 sectors=%u..%u retry=%d stage=split-profile result=BEGIN detail=chunk-sectors-%d", + d -> seed_diagnostic_block + 1, block_sector, + block_sector + SECTORS_PER_BLOCK - 1, c, chunk_len); for (chunk_start = 0; chunk_start < SECTORS_PER_BLOCK && out; chunk_start += chunk_len) { u_int32_t chunk_sector = block_sector + (u_int32_t) chunk_start; memset (readbuf, 0, sizeof (readbuf)); + memset (&sense, 0, sizeof (sense)); if (chunk_sector + 992 + 16 <= d -> sectors_no) - dvd_read_sector_dummy (d -> dvd, chunk_sector + 992, 16, NULL, NULL, 0); + ret = dvd_read_sector_dummy (d -> dvd, chunk_sector + 992, 16, &sense, NULL, 0); else if (chunk_sector >= 992) - dvd_read_sector_dummy (d -> dvd, chunk_sector - 992, 16, NULL, NULL, 0); + ret = dvd_read_sector_dummy (d -> dvd, chunk_sector - 992, 16, &sense, NULL, 0); else - dvd_flush_cache_READ12 (d -> dvd, chunk_sector, NULL); + ret = dvd_flush_cache_READ12 (d -> dvd, chunk_sector, &sense); + if (ret < 0) { + disc_seed_diag_log (d, + "block=%u/20 sectors=%u..%u retry=%d stage=split-prefetch result=NONFATAL-FAIL rc=%d sense=%02X/%02X/%02X detail=chunk-sectors-%d", + d -> seed_diagnostic_block + 1, chunk_sector, + chunk_sector + (u_int32_t) chunk_len - 1, c, ret, + sense.sense_key & 0xff, sense.asc & 0xff, sense.ascq & 0xff, chunk_len); + } - ret = dvd_read_streaming (d -> dvd, chunk_sector, (u_int32_t) chunk_len, NULL, readbuf, (size_t) chunk_len * SECTOR_SIZE); + memset (&sense, 0, sizeof (sense)); + ret = dvd_read_streaming (d -> dvd, chunk_sector, (u_int32_t) chunk_len, + &sense, readbuf, (size_t) chunk_len * SECTOR_SIZE); if (ret < 0) { + snprintf (detail, sizeof (detail), "chunk-sectors=%d sense=%02X/%02X/%02X", + chunk_len, sense.sense_key & 0xff, sense.asc & 0xff, sense.ascq & 0xff); + disc_seed_diag_fail (d, chunk_sector, + chunk_sector + (u_int32_t) chunk_len - 1, c, + "split-streaming-read", detail, ret, true); warning ("Method 8 split recovery: READ12 streaming failed for sectors %u..%u with %d", chunk_sector, chunk_sector + (u_int32_t) chunk_len - 1, ret); out = false; break; @@ -555,12 +676,24 @@ static bool disc_read_sector_8_split_recover_block (disc *d, u_int32_t block_sec sect = &raw_block[(chunk_start + k) * RAW_SECTOR_SIZE]; ram_offset = (u_int32_t) k * RAW_SECTOR_SIZE; - if (dvd_memdump (d -> dvd, ram_offset, 1, 12, sect) < 0) { + ret = dvd_memdump (d -> dvd, ram_offset, 1, 12, sect); + if (ret < 0) { + snprintf (detail, sizeof (detail), "chunk-sectors=%d raw-offset=%u sector-index=%d", + chunk_len, ram_offset, k); + disc_seed_diag_fail (d, chunk_sector + (u_int32_t) k, + chunk_sector + (u_int32_t) k, c, + "split-header-memdump", detail, ret, true); warning ("Method 8 split recovery: header memdump failed at sector %u", chunk_sector + (u_int32_t) k); out = false; break; } - if (dvd_memdump (d -> dvd, ram_offset + 2060, 1, 4, sect + 2060) < 0) { + ret = dvd_memdump (d -> dvd, ram_offset + 2060, 1, 4, sect + 2060); + if (ret < 0) { + snprintf (detail, sizeof (detail), "chunk-sectors=%d raw-offset=%u sector-index=%d", + chunk_len, ram_offset + 2060, k); + disc_seed_diag_fail (d, chunk_sector + (u_int32_t) k, + chunk_sector + (u_int32_t) k, c, + "split-edc-memdump", detail, ret, true); warning ("Method 8 split recovery: EDC memdump failed at sector %u", chunk_sector + (u_int32_t) k); out = false; break; @@ -571,6 +704,10 @@ static bool disc_read_sector_8_split_recover_block (disc *d, u_int32_t block_sec } if (out && !unscrambler_unscramble_16sectors (d -> u, block_sector, raw_block, iso_block)) { + snprintf (detail, sizeof (detail), "chunk-sectors=%d", chunk_len); + disc_seed_diag_fail (d, block_sector, + block_sector + SECTORS_PER_BLOCK - 1, c, + "split-edc-unscramble-validation", detail, -1, false); warning ("Method 8 split recovery: EDC/unscramble validation failed for %d-sector chunks at sectors %u..%u", chunk_len, block_sector, block_sector + SECTORS_PER_BLOCK - 1); out = false; } @@ -578,6 +715,10 @@ static bool disc_read_sector_8_split_recover_block (disc *d, u_int32_t block_sec if (out) { disc_cache_add_block (d, block_no, iso_block, raw_block); warning ("Method 8 split recovery: recovered sectors %u..%u using %d-sector chunks", block_sector, block_sector + SECTORS_PER_BLOCK - 1, chunk_len); + disc_seed_diag_log (d, + "block=%u/20 sectors=%u..%u retry=%d stage=split-profile result=PASS detail=chunk-sectors-%d", + d -> seed_diagnostic_block + 1, block_sector, + block_sector + SECTORS_PER_BLOCK - 1, c, chunk_len); return true; } } @@ -791,18 +932,22 @@ static int disc_read_sector_8_gdr8081n_search_guided (disc *d, u_int32_t sector_ } static int disc_read_sector_8 (disc *d, u_int32_t sector_no, u_int8_t **data, u_int8_t **rawdata) { - if (disc_hlds_type_is_gdr8081n_search_guided (dvd_get_hlds_e7_type (d -> dvd))) - return disc_read_sector_8_gdr8081n_search_guided (d, sector_no, data, rawdata); - bool out; u_int32_t ram_offset; + u_int32_t start_block; + u_int32_t profile_blocks; + u_int32_t prefetch_sector; int j, k, ret, retry; + int prefetch_ret; u_int8_t *sect, buf[5][RAW_BLOCK_SIZE]; u_int8_t readbuf[BLOCK_SIZE]; u_int8_t buf_unscrambled[5][BLOCK_SIZE]; - u_int32_t start_block; - u_int32_t profile_blocks; -//fprintf (stdout,"disc_read_sector_8"); + req_sense sense; + char detail[160]; + + if (disc_hlds_type_is_gdr8081n_search_guided (dvd_get_hlds_e7_type (d -> dvd))) + return disc_read_sector_8_gdr8081n_search_guided (d, sector_no, data, rawdata); + start_block = sector_no / SECTORS_PER_BLOCK; profile_blocks = dvd_get_hlds_e7_mem_blocks (d -> dvd); if (profile_blocks < 1 || profile_blocks > 5) @@ -810,88 +955,138 @@ static int disc_read_sector_8 (disc *d, u_int32_t sector_no, u_int8_t **data, u_ out = false; for (retry = 0; !out && retry < MAX_READ_RETRIES; retry++) { - /* Assume everything will turn out well */ out = true; + disc_seed_diag_clear (d); + disc_seed_diag_log (d, + "block=%u/20 sectors=%u..%u retry=%d stage=normal-profile result=BEGIN detail=windows-%u", + d -> seed_diagnostic_block + 1, sector_no, + sector_no + SECTORS_PER_BLOCK - 1, retry, profile_blocks); if (retry > 0) { warning ("Read retry %d for sector %u", retry, sector_no); - - /* Try to reset in-memory data by seeking to a distant sector */ -// if (sector_no > 1000) -// dvd_read_sector_streaming (d -> dvd, 0, NULL, NULL, 0); -// else -// dvd_read_sector_streaming (d -> dvd, 1500, NULL, NULL, 0); - if (sector_no +992 +16 <= d -> sectors_no) //smaller than last sector - dvd_read_sector_dummy (d -> dvd, sector_no +992, 16, NULL, NULL, 0); - else if (sector_no -992 >= 0) //larger than first sector - dvd_read_sector_dummy (d -> dvd, sector_no -992, 16, NULL, NULL, 0); - else dvd_flush_cache_READ12 (d -> dvd, sector_no, NULL); + memset (&sense, 0, sizeof (sense)); + if (sector_no + 992 + 16 <= d -> sectors_no) + ret = dvd_read_sector_dummy (d -> dvd, sector_no + 992, 16, &sense, NULL, 0); + else if (sector_no >= 992) + ret = dvd_read_sector_dummy (d -> dvd, sector_no - 992, 16, &sense, NULL, 0); + else + ret = dvd_flush_cache_READ12 (d -> dvd, sector_no, &sense); + if (ret < 0) { + disc_seed_diag_log (d, + "block=%u/20 sectors=%u..%u retry=%d stage=retry-cache-reset result=NONFATAL-FAIL rc=%d sense=%02X/%02X/%02X", + d -> seed_diagnostic_block + 1, sector_no, + sector_no + SECTORS_PER_BLOCK - 1, retry, ret, + sense.sense_key & 0xff, sense.asc & 0xff, sense.ascq & 0xff); + } } - /* First READ command. Type3/Type4 drives can expose several 16-sector - * cache windows after a nearby READ. The modified GDR-8050L test firmware - * proved seed retrieval and the first data runs, but failed when we drove it - * with the normal five-window prefetch schedule. For that profile, avoid - * the distant prefetch and consume only the current 16-sector window. */ + /* Prime the volatile cache, then immediately consume it. */ + memset (&sense, 0, sizeof (sense)); if (disc_hlds_type_is_gdr8050l_no_prefetch (dvd_get_hlds_e7_type (d -> dvd))) { - /* GDR-8050L modified-firmware no-prefetch scheduling is selected/logged - * by the profile probe, not from this hot per-read path. Keeping - * logging here spams one line for every 16-sector read/cache probe. */ - dvd_flush_cache_READ12 (d -> dvd, sector_no, NULL); + prefetch_sector = sector_no; + prefetch_ret = dvd_flush_cache_READ12 (d -> dvd, sector_no, &sense); } else { if (sector_no > d -> sectors_no - 1000) - dvd_read_sector_streaming (d -> dvd, sector_no - 16 * 5 * 2, NULL, NULL, 0); + prefetch_sector = sector_no - 16 * 5 * 2; else - dvd_read_sector_streaming (d -> dvd, sector_no + 16 * 5, NULL, NULL, 0); + prefetch_sector = sector_no + 16 * 5; + prefetch_ret = dvd_read_sector_streaming (d -> dvd, prefetch_sector, &sense, NULL, 0); + } + if (prefetch_ret < 0) { + disc_seed_diag_log (d, + "block=%u/20 sectors=%u..%u retry=%d stage=prefetch result=NONFATAL-FAIL rc=%d sense=%02X/%02X/%02X detail=prefetch-lba-%u", + d -> seed_diagnostic_block + 1, sector_no, + sector_no + SECTORS_PER_BLOCK - 1, retry, prefetch_ret, + sense.sense_key & 0xff, sense.asc & 0xff, sense.ascq & 0xff, + prefetch_sector); } - if ((ret = dvd_read_sector_streaming (d -> dvd, sector_no, NULL, readbuf, sizeof (readbuf))) >= 0) { + + memset (&sense, 0, sizeof (sense)); + ret = dvd_read_sector_streaming (d -> dvd, sector_no, &sense, readbuf, sizeof (readbuf)); + if (ret >= 0) { for (j = 0; j < (int) profile_blocks && sector_no + j * 16 < d -> sectors_no && out; j++) { - /* Reconstruct raw sectors */ for (k = 0; k < 16; k++) { sect = &buf[j][k * RAW_SECTOR_SIZE]; ram_offset = (j * RAW_BLOCK_SIZE) + k * RAW_SECTOR_SIZE; - /* Get first 12 bytes (ID. IED and CPR_MAI fields) and last 4 bytes (EDC field) with memdump */ - if (dvd_memdump (d -> dvd, ram_offset, 1, 12, sect) < 0) { + ret = dvd_memdump (d -> dvd, ram_offset, 1, 12, sect); + if (ret < 0) { + snprintf (detail, sizeof (detail), + "window=%d sector-index=%d raw-offset=%u", j, k, ram_offset); + disc_seed_diag_fail (d, sector_no + (u_int32_t) (j * 16 + k), + sector_no + (u_int32_t) (j * 16 + k), retry, + "header-memdump", detail, ret, true); error ("Memdump (1) failed"); out = false; - retry = MAX_READ_RETRIES; /* Well, if this fails going on is useless */ - } else if (dvd_memdump (d -> dvd, ram_offset + 2060, 1, 4, sect + 2060) < 0) { /* Dumping in a single block is faster */ - error ("Memdump (2) failed"); - out = false; + retry = MAX_READ_RETRIES; + } else { + ret = dvd_memdump (d -> dvd, ram_offset + 2060, 1, 4, sect + 2060); + if (ret < 0) { + snprintf (detail, sizeof (detail), + "window=%d sector-index=%d raw-offset=%u", j, k, ram_offset + 2060); + disc_seed_diag_fail (d, sector_no + (u_int32_t) (j * 16 + k), + sector_no + (u_int32_t) (j * 16 + k), retry, + "edc-memdump", detail, ret, true); + error ("Memdump (2) failed"); + out = false; + } } } } - /* Now the same for remaining cached 16-sector blocks. Type1 drives only - * expose one validated cache window at their DIC-derived base address. */ for (j = 0; j < (int) profile_blocks && sector_no + j * 16 < d -> sectors_no && out; j++) { - if (j == 0 || (ret = dvd_read_sector_streaming (d -> dvd, sector_no + j * 16, NULL, readbuf, sizeof (readbuf))) >= 0) { - /* Copy "user data" field which has been incorrectly unscrambled by the DVD drive firmware */ - for (k = 0; k < 16; k++) { - sect = &buf[j][k * RAW_SECTOR_SIZE]; - memcpy (sect + 12, readbuf + k * SECTOR_SIZE, SECTOR_SIZE); + if (j != 0) { + memset (&sense, 0, sizeof (sense)); + ret = dvd_read_sector_streaming (d -> dvd, sector_no + (u_int32_t) j * 16, + &sense, readbuf, sizeof (readbuf)); + if (ret < 0) { + snprintf (detail, sizeof (detail), + "window=%d sense=%02X/%02X/%02X", j, + sense.sense_key & 0xff, sense.asc & 0xff, sense.ascq & 0xff); + disc_seed_diag_fail (d, sector_no + (u_int32_t) j * 16, + sector_no + (u_int32_t) j * 16 + SECTORS_PER_BLOCK - 1, + retry, "cached-window-streaming-read", detail, ret, true); + error ("dvd_read_sector_streaming() failed with %d", ret); + out = false; + break; } + } + + for (k = 0; k < 16; k++) { + sect = &buf[j][k * RAW_SECTOR_SIZE]; + memcpy (sect + 12, readbuf + k * SECTOR_SIZE, SECTOR_SIZE); + } #ifdef DEBUG - if (d -> unscrambling) { + if (d -> unscrambling) { #endif - /* Try to unscramble all data to see if EDC fails */ - if (!unscrambler_unscramble_16sectors (d -> u, sector_no + (j * 16), buf[j], buf_unscrambled[j])) - out = false; -#ifdef DEBUG + if (!unscrambler_unscramble_16sectors (d -> u, + sector_no + (u_int32_t) (j * 16), buf[j], buf_unscrambled[j])) { + snprintf (detail, sizeof (detail), "window=%d", j); + disc_seed_diag_fail (d, + sector_no + (u_int32_t) j * 16, + sector_no + (u_int32_t) j * 16 + SECTORS_PER_BLOCK - 1, + retry, "edc-unscramble-validation", detail, -1, false); + out = false; } -#endif - } else { - error ("dvd_read_sector_streaming() failed with %d", ret); - out = false; +#ifdef DEBUG } +#endif } if (out) { - /* It seems all data were unscrambled correctly, so cache them out */ for (j = 0; j < (int) profile_blocks && sector_no + j * SECTORS_PER_BLOCK < d -> sectors_no; j++) disc_cache_add_block (d, start_block + j, buf_unscrambled[j], buf[j]); + disc_seed_diag_clear (d); + disc_seed_diag_log (d, + "block=%u/20 sectors=%u..%u retry=%d stage=normal-profile result=PASS detail=windows-%u", + d -> seed_diagnostic_block + 1, sector_no, + sector_no + SECTORS_PER_BLOCK - 1, retry, profile_blocks); } } else { + snprintf (detail, sizeof (detail), "sense=%02X/%02X/%02X", + sense.sense_key & 0xff, sense.asc & 0xff, sense.ascq & 0xff); + disc_seed_diag_fail (d, sector_no, + sector_no + SECTORS_PER_BLOCK - 1, retry, + "streaming-read", detail, ret, true); error ("dvd_read_sector_streaming() failed with %d", ret); out = false; } @@ -912,13 +1107,17 @@ static int disc_read_sector_8 (disc *d, u_int32_t sector_no, u_int8_t **data, u_ if (!out) { u_int32_t block_sector = start_block * SECTORS_PER_BLOCK; warning ("Method 8 normal profile read failed at sectors %u..%u; entering split recovery", block_sector, block_sector + SECTORS_PER_BLOCK - 1); + disc_seed_diag_log (d, + "block=%u/20 sectors=%u..%u retry=%d stage=normal-profile result=EXHAUSTED detail=entering-split-recovery", + d -> seed_diagnostic_block + 1, block_sector, + block_sector + SECTORS_PER_BLOCK - 1, MAX_READ_RETRIES - 1); out = disc_read_sector_8_split_recover_block (d, block_sector); } if (!out) error ("Too many retries, giving up"); - return (out); + return out; } @@ -1139,10 +1338,30 @@ int disc_read_sector (disc *d, u_int32_t sector_no, u_int8_t **data, u_int8_t ** } -static bool disc_analyze (disc *d) { +static const char *disc_detected_type_name (disc_type type) { + switch (type) { + case DISC_TYPE_GAMECUBE: + return "GameCube"; + case DISC_TYPE_WII: + return "Wii"; + case DISC_TYPE_WII_DL: + return "Wii_DL"; + case DISC_TYPE_DVD: + return "DVD"; + case DISC_TYPE_XBOX: + return "Xbox"; + default: + return "Unknown"; + } +} + + +static bool disc_analyze (disc *d, bool allow_type_correction, bool allow_size_correction) { u_int8_t *buf; char tmp[0x03E0 + 1]; bool unscramble_old, out; + nintendo_disc_header_type header_type; + disc_type previous_type; /* Force unscrambling for this read */ unscramble_old = d -> unscrambling; @@ -1151,16 +1370,41 @@ static bool disc_analyze (disc *d) { if (disc_read_sector (d, 0, &buf, NULL)) { /* System ID */ d -> system_id = buf[0]; -// if (d -> system_id == 'G') { -// d -> type = DISC_TYPE_GAMECUBE; -// d -> sectors_no = DISC_GAMECUBE_SECTORS_NO; -// } else if (d -> system_id == 'R') { -// d -> type = DISC_TYPE_WII; -// d -> sectors_no = DISC_WII_SECTORS_NO; -// } else { -// error ("Unknown system ID: '%c'", d -> system_id); -// MY_ASSERT (false); -// } + + /* + * The legacy capacity probe can misclassify GameCube media on some + * HLDS drives because an out-of-range dummy read does not always + * return the expected ILLEGAL REQUEST / LBA OUT OF RANGE sense data. + * Once sector 0 has been unscrambled, its platform magic is the + * authoritative discriminator. + * + * Explicit -T selections remain authoritative and are not changed. + * Explicit -S geometry also remains authoritative. + */ + header_type = nintendo_disc_header_detect (buf, SECTOR_SIZE); + previous_type = d -> type; + + if (allow_type_correction && + header_type == NINTENDO_DISC_HEADER_GAMECUBE) { + d -> type = DISC_TYPE_GAMECUBE; + if (allow_size_correction) + d -> sectors_no = DISC_GAMECUBE_SECTORS_NO; + } else if (allow_type_correction && + header_type == NINTENDO_DISC_HEADER_WII && + d -> type != DISC_TYPE_WII && + d -> type != DISC_TYPE_WII_DL) { + d -> type = DISC_TYPE_WII; + if (allow_size_correction) + d -> sectors_no = DISC_WII_SECTORS_NO_SL; + } + + if (allow_type_correction && + header_type != NINTENDO_DISC_HEADER_UNKNOWN && + previous_type != d -> type) { + warning ("Disc type corrected from %s to %s using sector-0 platform magic", + disc_detected_type_name (previous_type), + disc_detected_type_name (d -> type)); + } /* Game ID */ strncpy (d -> game_id, (char *) buf + 1, 2); @@ -2998,17 +3242,56 @@ static bool disc_probe_gdr8081n_e7_profile (disc *d) { static bool disc_crack_seeds (disc *d) { int i; - /* As a Nintendo GameCube/Wii disc should not have too many keys, 20 should be enough */ + /* As a Nintendo GameCube/Wii disc should not have too many keys, 20 should be enough. */ debug ("Retrieving all DVD seeds"); - if (!disc_probe_gdr8050l_e7_speed_profile (d)) + disc_seed_diag_clear (d); + d -> seed_diagnostic_active = true; + d -> seed_diagnostic_block = 0; + xbox_ref_log_fprintf (stderr, "\n"); + disc_seed_diag_log (d, + "campaign=begin blocks=20 method=%d profile=%s cache-base=0x%08X windows=%u", + d -> read_method, + dvd_get_hlds_e7_profile_name (d -> dvd), + dvd_get_hlds_e7_cache_base (d -> dvd), + dvd_get_hlds_e7_mem_blocks (d -> dvd)); + + if (!disc_probe_gdr8050l_e7_speed_profile (d)) { + disc_seed_diag_fail (d, 0, 15, 0, "profile-probe", + "GDR-8050L runtime profile probe failed", -1, false); + d -> seed_diagnostic_active = false; return false; - if (!disc_probe_gdr8081n_e7_profile (d)) + } + if (!disc_probe_gdr8081n_e7_profile (d)) { + disc_seed_diag_fail (d, 0, 15, 0, "profile-probe", + "GDR-8081N runtime profile probe failed", -1, false); + d -> seed_diagnostic_active = false; return false; + } + for (i = 0; i < 20 * 16; i += 16) { - if (!disc_read_sector (d, i, NULL, NULL)) + d -> seed_diagnostic_block = (unsigned int) (i / 16); + disc_seed_diag_clear (d); + disc_seed_diag_log (d, + "block=%u/20 sectors=%d..%d stage=seed-block result=BEGIN", + d -> seed_diagnostic_block + 1, i, i + 15); + if (!disc_read_sector (d, (u_int32_t) i, NULL, NULL)) { + if (!d -> seed_diagnostic.valid) + disc_seed_diag_fail (d, (u_int32_t) i, (u_int32_t) i + 15, + 0, "seed-block-read", "disc_read_sector returned failure without a classified sub-stage", -1, false); + disc_seed_diag_log (d, + "campaign=fail block=%u/20 sectors=%d..%d", + d -> seed_diagnostic_block + 1, i, i + 15); + d -> seed_diagnostic_active = false; return false; + } + disc_seed_diag_log (d, + "block=%u/20 sectors=%d..%d stage=seed-block result=PASS", + d -> seed_diagnostic_block + 1, i, i + 15); } + disc_seed_diag_clear (d); + disc_seed_diag_log (d, "campaign=pass blocks=20"); + d -> seed_diagnostic_active = false; return true; } @@ -3118,7 +3401,11 @@ bool disc_init (disc *d, u_int32_t disctype, u_int32_t sectors_no) { disc_set_unscrambling (d, false); out = true; } - else if (disc_analyze (d)) { + else if (disc_analyze ( + d, + disctype > 4, + sectors_no == (u_int32_t) -1 + )) { disc_check_update (d); out = true; } else { @@ -3228,10 +3515,14 @@ int disc_xbox_read_capacity_10 (disc *d, u_int32_t *sectors, u_int32_t *sector_s -int disc_xbox_recovery_kick (disc *d, bool auth_recovery) { +int disc_xbox_prepare_game_view (disc *d, u_int32_t *sectors, u_int32_t *sector_size) { + int out; if (!d || d -> type != DISC_TYPE_XBOX) return -1; - return dvd_xbox_recovery_kick (d -> dvd, auth_recovery); + out = dvd_xbox_prepare_game_view (d -> dvd, sectors, sector_size); + if (out == 0 && sectors) + d -> sectors_no = *sectors; + return out; } int disc_refresh_volume (disc *d) { @@ -3270,8 +3561,20 @@ int disc_xbox_wait_ready (disc *d, unsigned int timeout_ms) { return dvd_wait_ready (d -> dvd, timeout_ms); } +char *disc_get_drive_vendor (disc *d) { + return d ? dvd_get_vendor (d -> dvd) : NULL; +} + +char *disc_get_drive_product_id (disc *d) { + return d ? dvd_get_product_id (d -> dvd) : NULL; +} + +char *disc_get_drive_firmware_revision (disc *d) { + return d ? dvd_get_product_revision (d -> dvd) : NULL; +} + char *disc_get_drive_model_string (disc *d) { - return (dvd_get_model_string (d -> dvd)); + return d ? dvd_get_model_string (d -> dvd) : NULL; } diff --git a/libfriidump/disc.h b/libfriidump/disc.h index 345e681..f8af6ea 100644 --- a/libfriidump/disc.h +++ b/libfriidump/disc.h @@ -55,6 +55,25 @@ typedef enum { typedef struct disc_s disc; +/* Always-available evidence for a failed Nintendo seed/cache read. */ +typedef struct { + bool valid; + unsigned int seed_block_index; + u_int32_t sector_start; + u_int32_t sector_end; + int retry; + char stage[48]; + char detail[160]; + int transport_result; + int os_error; + int scsi_status; + int sense_key; + int asc; + int ascq; + int cdb_length; + u_int8_t cdb[12]; +} disc_seed_diagnostic; + /* Functions */ FRIIDUMPLIB_EXPORT disc *disc_new (char *dvd_device, u_int32_t command); @@ -88,6 +107,9 @@ FRIIDUMPLIB_EXPORT char *disc_get_version (disc *d, u_int8_t *v, char **v_s); FRIIDUMPLIB_EXPORT char *disc_get_title (disc *d, char **t_s); FRIIDUMPLIB_EXPORT bool disc_get_update (disc *d); +FRIIDUMPLIB_EXPORT char *disc_get_drive_vendor (disc *d); +FRIIDUMPLIB_EXPORT char *disc_get_drive_product_id (disc *d); +FRIIDUMPLIB_EXPORT char *disc_get_drive_firmware_revision (disc *d); FRIIDUMPLIB_EXPORT char *disc_get_drive_model_string (disc *d); FRIIDUMPLIB_EXPORT char *disc_get_device (disc *d); FRIIDUMPLIB_EXPORT void *disc_get_native_handle (disc *d); @@ -116,7 +138,7 @@ FRIIDUMPLIB_EXPORT int disc_xbox_unlock (disc *d); FRIIDUMPLIB_EXPORT int disc_xbox_read_10 (disc *d, u_int32_t sector, u_int32_t sectors, u_int8_t *buf, size_t bufsize); FRIIDUMPLIB_EXPORT int disc_xbox_read_dvd_structure (disc *d, u_int8_t format, u_int8_t layer, u_int8_t *buf, size_t bufsize); FRIIDUMPLIB_EXPORT int disc_xbox_read_capacity_10 (disc *d, u_int32_t *sectors, u_int32_t *sector_size); -FRIIDUMPLIB_EXPORT int disc_xbox_recovery_kick (disc *d, bool auth_recovery); +FRIIDUMPLIB_EXPORT int disc_xbox_prepare_game_view (disc *d, u_int32_t *sectors, u_int32_t *sector_size); FRIIDUMPLIB_EXPORT int disc_xbox_refresh_volume (disc *d); FRIIDUMPLIB_EXPORT int disc_lock_volume (disc *d); FRIIDUMPLIB_EXPORT int disc_refresh_volume (disc *d); @@ -124,6 +146,7 @@ FRIIDUMPLIB_EXPORT int disc_xbox_lock_volume (disc *d); FRIIDUMPLIB_EXPORT int disc_xbox_media_cycle (disc *d); FRIIDUMPLIB_EXPORT int disc_xbox_wait_ready (disc *d, unsigned int timeout_ms); FRIIDUMPLIB_EXPORT int disc_media_preflight (disc *d, unsigned int timeout_ms, int *sense_key, int *asc, int *ascq); +FRIIDUMPLIB_EXPORT bool disc_get_seed_diagnostic (disc *d, disc_seed_diagnostic *out); #ifdef __cplusplus } diff --git a/libfriidump/dumper.c b/libfriidump/dumper.c index 5279535..1b95623 100644 --- a/libfriidump/dumper.c +++ b/libfriidump/dumper.c @@ -29,11 +29,13 @@ #include "disc.h" #include "dumper.h" #include "xbox_ref_bridge.h" +#include "xbox_portable.h" #include "xbox_ref/xbox_ref_log.h" #ifndef WIN32 #include #include +#include #endif struct dumper_s { @@ -58,10 +60,232 @@ struct dumper_s { progress_func progress; void *progress_data; + dumper_cancel_func cancel; + void *cancel_data; + bool cancelled; }; #define DUMPER_READ_BLOCK_RETRIES 3 +static bool dumper_cancel_requested (dumper *dmp) { + if (!dmp || !dmp -> cancel) + return false; + if (!dmp -> cancel (dmp -> cancel_data)) + return false; + dmp -> cancelled = true; + return true; +} + +static int dumper_xbox_ref_cancel_requested (void *cancel_data) { + return dumper_cancel_requested ((dumper *) cancel_data) ? 1 : 0; +} + +static double xbox_portable_now_seconds (void) { + struct timeval now; + gettimeofday (&now, NULL); + return (double) now.tv_sec + ((double) now.tv_usec / 1000000.0); +} + +static u_int64_t xbox_portable_file_size (const char *path) { + FILE *file; + my_off_t position; + + if (!path || path[0] == '\0') + return 0; + file = fopen (path, "rb"); + if (!file) + return 0; + if (my_fseek (file, 0, SEEK_END) != 0) { + fclose (file); + return 0; + } + position = my_ftell (file); + fclose (file); + if (position < 0) + return 0; + return (u_int64_t) position; +} + +static void xbox_portable_copy_string (char *destination, size_t destination_size, const char *source) { + if (!destination || destination_size == 0) + return; + if (!source) + source = ""; + snprintf (destination, destination_size, "%s", source); +} + +static void xbox_portable_result_begin (dumper *dmp, const char *path, char mode) { + if (!dmp) + return; + xbox_ref_dump_result_init (&(dmp -> xbox_ref_result)); + dmp -> xbox_ref_result.attempted = 1; + dmp -> xbox_ref_result.mode = mode; + xbox_portable_copy_string (dmp -> xbox_ref_result.output_path, + sizeof (dmp -> xbox_ref_result.output_path), path); +} + +static void xbox_portable_sanitize_filename_component (char *value) { + size_t read_index; + size_t write_index; + bool previous_space; + + if (!value) + return; + + write_index = 0; + previous_space = true; + for (read_index = 0; value[read_index] != '\0'; ++read_index) { + unsigned char c = (unsigned char) value[read_index]; + bool forbidden = c < 32 || c == '"' || c == '*' || c == '/' || + c == ':' || c == '<' || c == '>' || c == '?' || c == '\\' || c == '|'; + + if (forbidden) + continue; + if (c == ' ') { + if (previous_space) + continue; + value[write_index++] = ' '; + previous_space = true; + } else { + value[write_index++] = (char) c; + previous_space = false; + } + } + + while (write_index > 0 && (value[write_index - 1] == ' ' || + value[write_index - 1] == '.')) + --write_index; + value[write_index] = '\0'; +} + +static bool xbox_portable_resolve_output_path (dumper *dmp, bool xiso_mode) { + xbox_portable_metadata metadata; + char title[XBOX_PORTABLE_TITLE_MAX]; + char path[XBOX_REF_RESULT_PATH_MAX]; + char **target; + const char *extension; + + if (!dmp) + return false; + + target = xiso_mode ? &(dmp -> outfile_xiso) : &(dmp -> outfile_iso); + if (!*target || (*target)[0] != '\0') + return true; + + xbox_ref_log_fprintf (stderr, + "[XBOX-PORTABLE] Resolving the default output filename from the unlocked XBE/DMI view.\n"); + disc_xbox_wait_ready (dmp -> dsk, 30000); + if (disc_xbox_unlock (dmp -> dsk) < 0) + xbox_ref_log_fprintf (stderr, + "[XBOX-PORTABLE][WARN] Filename preflight unlock returned failure; attempting metadata reads anyway.\n"); + disc_xbox_refresh_volume (dmp -> dsk); + disc_xbox_wait_ready (dmp -> dsk, 30000); + + xbox_portable_metadata_init (&metadata); + if (!xbox_portable_read_metadata_from_disc (dmp -> dsk, &metadata)) { + error ("Could not derive an Xbox output filename because XBE/DMI metadata was unavailable; provide an explicit output path"); + return false; + } + + xbox_portable_copy_string (title, sizeof (title), + metadata.title[0] ? metadata.title : "Xbox DVD"); + xbox_portable_sanitize_filename_component (title); + if (!title[0]) + xbox_portable_copy_string (title, sizeof (title), "Xbox DVD"); + extension = xiso_mode ? ".xiso" : ".iso"; + + if (metadata.media_id[0]) + snprintf (path, sizeof (path), "%s[%s]%s", title, metadata.media_id, extension); + else + snprintf (path, sizeof (path), "%s%s", title, extension); + + my_free (*target); + my_strdup (*target, path); + xbox_ref_log_retarget (*target); + xbox_ref_log_fprintf (stderr, + "[XBOX-PORTABLE] Derived output filename: %s\n", *target); + return true; +} + +static void xbox_portable_result_capture_metadata (dumper *dmp) { + xbox_portable_metadata metadata; + + if (!dmp || (dmp -> xbox_ref_result.title[0] && + dmp -> xbox_ref_result.have_game_region)) + return; + + xbox_portable_metadata_init (&metadata); + if (!xbox_portable_read_metadata_from_disc (dmp -> dsk, &metadata)) { + xbox_ref_log_fprintf (stderr, + "[XBOX-PORTABLE][WARN] XBE title/region metadata was not available from the unlocked view.\n"); + return; + } + + xbox_portable_copy_string (dmp -> xbox_ref_result.title, + sizeof (dmp -> xbox_ref_result.title), metadata.title); + xbox_portable_copy_string (dmp -> xbox_ref_result.media_id, + sizeof (dmp -> xbox_ref_result.media_id), metadata.media_id); + xbox_portable_copy_string (dmp -> xbox_ref_result.region, + sizeof (dmp -> xbox_ref_result.region), metadata.region); + dmp -> xbox_ref_result.have_game_region = metadata.have_game_region; + dmp -> xbox_ref_result.game_region = metadata.game_region; + + xbox_ref_log_fprintf (stderr, + "[XBOX-PORTABLE] XBE metadata: title='%s', media_id='%s', region='%s', GameRegion=0x%08x.\n", + dmp -> xbox_ref_result.title[0] ? dmp -> xbox_ref_result.title : "(unknown)", + dmp -> xbox_ref_result.media_id[0] ? dmp -> xbox_ref_result.media_id : "(unknown)", + dmp -> xbox_ref_result.region[0] ? dmp -> xbox_ref_result.region : "(unknown)", + dmp -> xbox_ref_result.game_region); +} + +static void xbox_portable_result_finish (dumper *dmp, + bool success, + u_int32_t expected_output_sectors, + double started_seconds, + bool xiso_mode) { + xbox_ref_dump_result *result; + const char *crc32; + const char *md5; + const char *sha1; + const char *sha256; + + if (!dmp || !(dmp -> xbox_ref_result).attempted) + return; + + result = &(dmp -> xbox_ref_result); + result -> dump_success = success ? 1 : 0; + result -> cancelled = dmp -> cancelled ? 1 : 0; + result -> output_sectors = expected_output_sectors; + result -> output_size = xbox_portable_file_size (result -> output_path); + result -> completed_sectors = (u_int32_t) (result -> output_size / SECTOR_SIZE); + result -> elapsed_seconds = xbox_portable_now_seconds () - started_seconds; + + if (!dmp -> hashing || result -> output_size == 0) + return; + + if (xiso_mode) { + crc32 = dumper_get_xiso_crc32 (dmp); + md5 = dumper_get_xiso_md5 (dmp); + sha1 = dumper_get_xiso_sha1 (dmp); + sha256 = dumper_get_xiso_sha2 (dmp); + } else { + crc32 = dumper_get_iso_crc32 (dmp); + md5 = dumper_get_iso_md5 (dmp); + sha1 = dumper_get_iso_sha1 (dmp); + sha256 = dumper_get_iso_sha2 (dmp); + } + + if (!crc32 || !md5 || !sha1 || !sha256 || + !crc32[0] || !md5[0] || !sha1[0] || !sha256[0]) + return; + + xbox_portable_copy_string (result -> crc32, sizeof (result -> crc32), crc32); + xbox_portable_copy_string (result -> md5, sizeof (result -> md5), md5); + xbox_portable_copy_string (result -> sha1, sizeof (result -> sha1), sha1); + xbox_portable_copy_string (result -> sha256, sizeof (result -> sha256), sha256); + result -> hashes_complete = 1; +} + static bool dumper_read_sector_with_retry (dumper *dmp, u_int32_t sector_no, u_int8_t **isobuf, u_int8_t **rawbuf) { int attempt; bool need_iso, need_raw; @@ -257,12 +481,25 @@ static bool xbox_write_binary_file (const char *path, const u_int8_t *data, size } static void xbox_capture_pfi_dmi (disc *d, xbox_redump_metadata *meta) { + int rc; if (!d || !meta) return; - if (!meta -> has_pfi) - meta -> has_pfi = disc_xbox_read_dvd_structure (d, 0x00, 0x00, meta -> pfi, sizeof (meta -> pfi)) >= 0; - if (!meta -> has_dmi) - meta -> has_dmi = disc_xbox_read_dvd_structure (d, 0x04, 0x00, meta -> dmi, sizeof (meta -> dmi)) >= 0; + if (!meta -> has_pfi) { + rc = disc_xbox_read_dvd_structure (d, 0x00, 0x00, meta -> pfi, sizeof (meta -> pfi)); + meta -> has_pfi = rc >= 0; + xbox_ref_log_fprintf ( + stderr, + "[XBOX-DVD-STRUCTURE] format=0x00 name=PFI cdb_format_byte=7 result=%s\n", + meta -> has_pfi ? "PASS" : "FAIL"); + } + if (!meta -> has_dmi) { + rc = disc_xbox_read_dvd_structure (d, 0x04, 0x00, meta -> dmi, sizeof (meta -> dmi)); + meta -> has_dmi = rc >= 0; + xbox_ref_log_fprintf ( + stderr, + "[XBOX-DVD-STRUCTURE] format=0x04 name=DMI cdb_format_byte=7 result=%s\n", + meta -> has_dmi ? "PASS" : "FAIL"); + } } static void xbox_write_redump_metadata (dumper *dmp, xbox_redump_metadata *meta) { @@ -377,6 +614,8 @@ static void xbox_write_redump_metadata (dumper *dmp, xbox_redump_metadata *meta) } static bool xbox_write_iso_sectors (dumper *dmp, const u_int8_t *buf, u_int32_t sectors, u_int32_t output_lba, u_int32_t total_sectors) { + if (dumper_cancel_requested (dmp)) + return false; if (!dmp || !dmp -> fp_iso || !buf) return false; clearerr (dmp -> fp_iso); @@ -399,7 +638,10 @@ static bool xbox_write_zero_iso_range (dumper *dmp, u_int32_t sectors, u_int32_t memset (zero, 0, sizeof (zero)); debug ("Writing Xbox %s zero-fill: output LBA %u, sectors %u", label ? label : "range", output_lba, sectors); while (done < sectors) { - u_int32_t todo = sectors - done; + u_int32_t todo; + if (dumper_cancel_requested (dmp)) + return false; + todo = sectors - done; if (todo > SECTORS_PER_BLOCK) todo = SECTORS_PER_BLOCK; if (!xbox_write_iso_sectors (dmp, zero, todo, output_lba + done, total_sectors)) @@ -409,23 +651,23 @@ static bool xbox_write_zero_iso_range (dumper *dmp, u_int32_t sectors, u_int32_t return true; } -static bool xbox_dump_read10_iso_range (dumper *dmp, u_int32_t source_lba, u_int32_t sectors, u_int32_t output_lba, u_int32_t total_sectors, const char *label, bool auth_recovery, u_int32_t *current_sector) { +static bool xbox_dump_read10_iso_range (dumper *dmp, u_int32_t source_lba, u_int32_t sectors, u_int32_t output_lba, u_int32_t total_sectors, const char *label, u_int32_t *current_sector) { u_int8_t buf[BLOCK_SIZE]; u_int8_t one[SECTOR_SIZE]; u_int32_t done = 0; debug ("Reading Xbox %s: source LBA %u, output LBA %u, sectors %u", label ? label : "range", source_lba, output_lba, sectors); - disc_xbox_recovery_kick (dmp -> dsk, auth_recovery); while (done < sectors) { - u_int32_t todo = sectors - done; + u_int32_t todo; int retry; bool ok = false; + if (dumper_cancel_requested (dmp)) + return false; + todo = sectors - done; if (todo > SECTORS_PER_BLOCK) todo = SECTORS_PER_BLOCK; if (current_sector) *current_sector = output_lba + done; for (retry = 0; retry < 3 && !ok; retry++) { - if (retry > 0) - disc_xbox_recovery_kick (dmp -> dsk, auth_recovery); memset (buf, 0, sizeof (buf)); if (disc_xbox_read_10 (dmp -> dsk, source_lba + done, todo, buf, sizeof (buf)) >= 0) ok = true; @@ -438,9 +680,9 @@ static bool xbox_dump_read10_iso_range (dumper *dmp, u_int32_t source_lba, u_int bool singles_ok = true; for (i = 0; i < todo; i++) { bool one_ok = false; + if (dumper_cancel_requested (dmp)) + return false; for (retry = 0; retry < 3 && !one_ok; retry++) { - if (retry > 0) - disc_xbox_recovery_kick (dmp -> dsk, auth_recovery); memset (one, 0, sizeof (one)); if (disc_xbox_read_10 (dmp -> dsk, source_lba + done + i, 1, one, sizeof (one)) >= 0) one_ok = true; @@ -471,23 +713,23 @@ static bool xbox_dump_read10_iso_range (dumper *dmp, u_int32_t source_lba, u_int return true; } -static bool xbox_read_range_to_memory (disc *d, u_int32_t source_lba, u_int32_t sectors, u_int8_t *out, size_t out_size, const char *label, bool auth_recovery) { +static bool xbox_read_range_to_memory (dumper *dmp, u_int32_t source_lba, u_int32_t sectors, u_int8_t *out, size_t out_size, const char *label) { u_int8_t one[SECTOR_SIZE]; u_int32_t done = 0; - if (!d || !out || out_size < (size_t) sectors * SECTOR_SIZE) + if (!dmp || !dmp -> dsk || !out || out_size < (size_t) sectors * SECTOR_SIZE) return false; debug ("Capturing Xbox %s to memory: source LBA %u, sectors %u", label ? label : "range", source_lba, sectors); - disc_xbox_recovery_kick (d, auth_recovery); while (done < sectors) { - u_int32_t todo = sectors - done; + u_int32_t todo; int retry; bool ok = false; + if (dumper_cancel_requested (dmp)) + return false; + todo = sectors - done; if (todo > SECTORS_PER_BLOCK) todo = SECTORS_PER_BLOCK; for (retry = 0; retry < 3 && !ok; retry++) { - if (retry > 0) - disc_xbox_recovery_kick (d, auth_recovery); - if (disc_xbox_read_10 (d, source_lba + done, todo, out + ((size_t) done * SECTOR_SIZE), out_size - ((size_t) done * SECTOR_SIZE)) >= 0) + if (disc_xbox_read_10 (dmp -> dsk, source_lba + done, todo, out + ((size_t) done * SECTOR_SIZE), out_size - ((size_t) done * SECTOR_SIZE)) >= 0) ok = true; } if (!ok && todo > 1) { @@ -495,11 +737,11 @@ static bool xbox_read_range_to_memory (disc *d, u_int32_t source_lba, u_int32_t bool singles_ok = true; for (i = 0; i < todo; i++) { bool one_ok = false; + if (dumper_cancel_requested (dmp)) + return false; for (retry = 0; retry < 3 && !one_ok; retry++) { - if (retry > 0) - disc_xbox_recovery_kick (d, auth_recovery); memset (one, 0, sizeof (one)); - if (disc_xbox_read_10 (d, source_lba + done + i, 1, one, sizeof (one)) >= 0) + if (disc_xbox_read_10 (dmp -> dsk, source_lba + done + i, 1, one, sizeof (one)) >= 0) one_ok = true; } if (!one_ok) { @@ -522,16 +764,17 @@ static bool xbox_read_range_to_memory (disc *d, u_int32_t source_lba, u_int32_t return true; } -static bool xbox_write_leadin_iso_range (dumper *dmp, u_int32_t source_lba, u_int32_t output_lba, u_int32_t total_sectors, xbox_redump_metadata *meta, const char *label, bool auth_recovery, u_int32_t *current_sector) { +static bool xbox_write_leadin_iso_range (dumper *dmp, u_int32_t source_lba, u_int32_t output_lba, u_int32_t total_sectors, xbox_redump_metadata *meta, const char *label, u_int32_t *current_sector) { u_int8_t buf[SECTOR_SIZE]; u_int8_t zero[SECTOR_SIZE]; u_int32_t i; bool warned = false; memset (zero, 0, sizeof (zero)); debug ("Attempting Xbox %s lead-in from source LBA %u..%u", label ? label : "game", source_lba, source_lba + XBOX_XISO_LEADIN_SECTORS - 1); - disc_xbox_recovery_kick (dmp -> dsk, auth_recovery); for (i = 0; i < XBOX_XISO_LEADIN_SECTORS; i++) { const u_int8_t *src = buf; + if (dumper_cancel_requested (dmp)) + return false; if (current_sector) *current_sector = output_lba + i; memset (buf, 0, sizeof (buf)); @@ -772,6 +1015,11 @@ bool dumper_prepare_xiso (dumper *dmp) { dmp -> start_sector = dmp -> start_sector_xiso; +#ifndef WIN32 + if (!xbox_portable_resolve_output_path (dmp, true)) + return false; +#endif + #ifdef WIN32 if (dmp -> outfile_xiso[0] == '\0' && disc_is_xbox_challenge_drive (dmp -> dsk)) { if (dmp -> hashing) @@ -831,6 +1079,13 @@ bool dumper_prepare (dumper *dmp) { MY_ASSERT (0); } +#ifndef WIN32 + if (dmp -> outfile_iso && dmp -> outfile_iso[0] == '\0' && + disc_is_xbox_challenge_drive (dmp -> dsk) && + !xbox_portable_resolve_output_path (dmp, false)) + return false; +#endif + #ifdef WIN32 if (dmp -> outfile_iso && dmp -> outfile_iso[0] == '\0' && disc_is_xbox_challenge_drive (dmp -> dsk)) { if (dmp -> hashing) { @@ -919,6 +1174,8 @@ static int dumper_dump_xbox_redump_iso (dumper *dmp, u_int32_t *current_sector) bool locked_view_is_xdfs; disc_type type_id; char *type = NULL; + double portable_start = 0.0; + int volume_lock_result; if (!dmp) return false; @@ -947,7 +1204,9 @@ static int dumper_dump_xbox_redump_iso (dumper *dmp, u_int32_t *current_sector) if (current_sector) *current_sector = 0; xbox_ref_log_fprintf (stderr, "[XBOX] Dispatching GDR-8050L -i directly to copied original dumper code using FriiDump's existing drive HANDLE.\n"); - ref_status = xbox_ref_gdr8050l_dump_with_handle (disc_get_native_handle (dmp -> dsk), disc_get_device (dmp -> dsk), dmp -> outfile_iso, '1', &(dmp -> xbox_ref_result)); + ref_status = xbox_ref_gdr8050l_dump_with_handle (disc_get_native_handle (dmp -> dsk), disc_get_device (dmp -> dsk), dmp -> outfile_iso, '1', dumper_xbox_ref_cancel_requested, dmp, &(dmp -> xbox_ref_result)); + if (current_sector) + *current_sector = dmp -> xbox_ref_result.completed_sectors; return ref_status == 0; } #endif @@ -956,6 +1215,8 @@ static int dumper_dump_xbox_redump_iso (dumper *dmp, u_int32_t *current_sector) return false; memset (&meta, 0, sizeof (meta)); + portable_start = xbox_portable_now_seconds (); + xbox_portable_result_begin (dmp, dmp -> outfile_iso, '1'); /* Original GDR-8050L dumper order for redump-style output: * 1. unlock/authenticate into the game view; @@ -966,26 +1227,19 @@ static int dumper_dump_xbox_redump_iso (dumper *dmp, u_int32_t *current_sector) * The video partition is not encrypted/locked; "lock" here only means the * drive-visible DVD-video state before the Xbox game-view unlock. */ if (disc_is_xbox_challenge_drive (dmp -> dsk)) { - xbox_ref_log_fprintf (stderr, "[XBOX] GDR-8050L flow follows original main.c/DumpXboxGameDisc option 1.\n"); - xbox_ref_log_fprintf (stderr, "[XBOX] Stage 1/6: primary UnlockDrive() handshake.\n"); - disc_xbox_wait_ready (dmp -> dsk, 30000); - if (disc_xbox_unlock (dmp -> dsk) < 0) - xbox_ref_log_fprintf (stderr, "[XBOX][WARN] Stage 1 UnlockDrive() returned failure; continuing like the reference dumper and letting later reads prove state.\n"); - xbox_ref_log_fprintf (stderr, "[XBOX] Stage 2/6: media cycle after primary handshake.\n"); - if (disc_xbox_media_cycle (dmp -> dsk) < 0) { - xbox_ref_log_fprintf (stderr, "[XBOX][FATAL] Stage 2 failed: required media-cycle failed.\n"); - return false; + xbox_ref_log_fprintf (stderr, "[XBOX] GDR-8050L Linux flow mirrors the proven Windows state-aware sequence.\n"); + xbox_ref_log_fprintf (stderr, "[XBOX] Windows sequence 1/4: establish and verify the game view.\n"); + if (disc_xbox_prepare_game_view (dmp -> dsk, &unlocked_sectors, §or_size) < 0) { + xbox_ref_log_fprintf (stderr, "[XBOX][FATAL] Windows-sequence game-view preparation failed.\n"); + goto cleanup; } - xbox_ref_log_fprintf (stderr, "[XBOX] Stage 3/6: re-apply UnlockDrive() after media change.\n"); - if (disc_xbox_unlock (dmp -> dsk) < 0) - xbox_ref_log_fprintf (stderr, "[XBOX][WARN] Stage 3 UnlockDrive() returned failure; continuing like the reference dumper and letting GetXboxGameInfo/XDVDFS probe prove state.\n"); - xbox_ref_log_fprintf (stderr, "[XBOX] Stage 4/6: RefreshVolume, settle, EnsureDriveReady, SetDriveSpeedMax, and volume lock before metadata probe.\n"); - disc_xbox_refresh_volume (dmp -> dsk); - disc_xbox_wait_ready (dmp -> dsk, 30000); - disc_set_speed (dmp -> dsk, 0xFFFF); - if (disc_xbox_lock_volume (dmp -> dsk) < 0) - xbox_ref_log_fprintf (stderr, "[XBOX][WARN] FSCTL_LOCK_VOLUME equivalent failed; continuing like reference dumper warning path.\n"); + xbox_ref_log_fprintf (stderr, "[XBOX] Windows sequence 2/4: acquire the volume lock and probe metadata.\n"); + volume_lock_result = disc_xbox_lock_volume (dmp -> dsk); + if (volume_lock_result < 0) + xbox_ref_log_fprintf (stderr, "[XBOX][WARN] Exclusive volume lock failed; continuing like reference dumper warning path.\n"); + else if (volume_lock_result > 0) + xbox_ref_log_fprintf (stderr, "[XBOX][INFO] Linux exclusive optical-device lock is unavailable; continuing without one.\n"); disc_xbox_read_capacity_10 (dmp -> dsk, &unlocked_sectors, §or_size); meta.unlocked_visible_sectors = unlocked_sectors; xbox_capture_pfi_dmi (dmp -> dsk, &meta); @@ -996,13 +1250,16 @@ static int dumper_dump_xbox_redump_iso (dumper *dmp, u_int32_t *current_sector) meta.game_source_lba = XBOX_XISO_START_LBA_MAGIC; meta.game_source_sectors = XBOX_XGD1_GAME_SOURCE_SECTORS; } + xbox_portable_result_capture_metadata (dmp); - xbox_ref_log_fprintf (stderr, "[XBOX] Stage 5/6: media cycle back to visible DVD-video view before sector 0 capture.\n"); + xbox_ref_log_fprintf (stderr, "[XBOX] Windows sequence 3/4: media cycle back to visible DVD-video view before sector 0 capture.\n"); if (disc_xbox_media_cycle (dmp -> dsk) < 0) { xbox_ref_log_fprintf (stderr, "[XBOX][FATAL] Stage 5 failed: could not restore visible DVD-video view.\n"); - return false; + goto cleanup; } - disc_xbox_wait_ready (dmp -> dsk, 45000); + /* Windows raw path: AutomateTrayCycle(); RefreshVolume(); SetDriveSpeedMax(). */ + disc_xbox_refresh_volume (dmp -> dsk); + disc_set_speed (dmp -> dsk, 0xFFFF); } else if (disc_is_xbox_vendor_unlock_drive (dmp -> dsk)) { /* GDR-3120L/Kreon-style FF 08 01 profiles can explicitly select * state 0 for the visible DVD-video view. */ @@ -1017,12 +1274,12 @@ static int dumper_dump_xbox_redump_iso (dumper *dmp, u_int32_t *current_sector) locked_view_is_xdfs = xbox_probe_xdfs_volume (dmp -> dsk, XBOX_XISO_STANDARD_GAME_LBA, NULL, NULL); if (locked_view_is_xdfs && disc_is_xbox_challenge_drive (dmp -> dsk)) { error ("Current GDR-8050L view still exposes XDVDFS at LBA 32 after media cycle; cannot capture visible DVD-video view for redump-style ISO"); - return false; + goto cleanup; } if (!xbox_redump_layout_constants_match_dic ()) { error ("Xbox redump layout constants do not match the DiscImageCreator-compatible XGD1 layout"); - return false; + goto cleanup; } meta.output_sectors = XBOX_XGD1_FULL_REDUMP_SECTORS; @@ -1035,28 +1292,32 @@ static int dumper_dump_xbox_redump_iso (dumper *dmp, u_int32_t *current_sector) video_l1 = (u_int8_t *) malloc ((size_t) XBOX_XGD1_VIDEO_L1_SECTORS * SECTOR_SIZE); if (!video_l1) { error ("Could not allocate Xbox VIDEO-L1 capture buffer"); - return false; + goto cleanup; } /* Capture the tail of the visible DVD-video view before switching to the game view. */ - if (!xbox_read_range_to_memory (dmp -> dsk, XBOX_XGD1_VIDEO_L0_SECTORS, XBOX_XGD1_VIDEO_L1_SECTORS, - video_l1, (size_t) XBOX_XGD1_VIDEO_L1_SECTORS * SECTOR_SIZE, "VIDEO-L1", false)) + if (!xbox_read_range_to_memory (dmp, XBOX_XGD1_VIDEO_L0_SECTORS, XBOX_XGD1_VIDEO_L1_SECTORS, + video_l1, (size_t) XBOX_XGD1_VIDEO_L1_SECTORS * SECTOR_SIZE, "VIDEO-L1")) goto cleanup; - if (!xbox_dump_read10_iso_range (dmp, 0, XBOX_XGD1_VIDEO_L0_SECTORS, 0, meta.output_sectors, "VIDEO-L0", false, current_sector)) + if (!xbox_dump_read10_iso_range (dmp, 0, XBOX_XGD1_VIDEO_L0_SECTORS, 0, meta.output_sectors, "VIDEO-L0", current_sector)) goto cleanup; if (current_sector) *current_sector = XBOX_XGD1_VIDEO_L0_SECTORS; if (!xbox_write_zero_iso_range (dmp, pregame_padding, XBOX_XGD1_VIDEO_L0_SECTORS, meta.output_sectors, "PREGAME-PADDING")) goto cleanup; - xbox_ref_log_fprintf (stderr, "[XBOX] Stage 6/6: re-apply UnlockDrive() for unlocked game/XDVDFS data.\n"); - if (disc_xbox_unlock (dmp -> dsk) < 0) - xbox_ref_log_fprintf (stderr, "[XBOX][WARN] Stage 6 UnlockDrive() returned failure; continuing until the XDVDFS probe/read proves failure.\n"); - disc_xbox_refresh_volume (dmp -> dsk); - disc_xbox_wait_ready (dmp -> dsk, 30000); - disc_set_speed (dmp -> dsk, 0xFFFF); - disc_xbox_read_capacity_10 (dmp -> dsk, &unlocked_sectors, §or_size); + xbox_ref_log_fprintf (stderr, "[XBOX] Windows sequence 4/4: re-establish and verify the game view for XDVDFS data.\n"); + if (disc_is_xbox_challenge_drive (dmp -> dsk)) { + if (disc_xbox_prepare_game_view (dmp -> dsk, &unlocked_sectors, §or_size) < 0) { + xbox_ref_log_fprintf (stderr, "[XBOX][FATAL] Final Windows-sequence game-view preparation failed.\n"); + goto cleanup; + } + } else { + if (disc_xbox_unlock (dmp -> dsk) < 0) + xbox_ref_log_fprintf (stderr, "[XBOX][WARN] Xbox vendor unlock returned failure; XDVDFS probe remains authoritative.\n"); + disc_xbox_read_capacity_10 (dmp -> dsk, &unlocked_sectors, §or_size); + } meta.unlocked_visible_sectors = unlocked_sectors; xbox_capture_pfi_dmi (dmp -> dsk, &meta); @@ -1071,6 +1332,7 @@ static int dumper_dump_xbox_redump_iso (dumper *dmp, u_int32_t *current_sector) meta.game_source_lba = game_source_lba; meta.game_source_sectors = XBOX_XGD1_GAME_SOURCE_SECTORS; + xbox_portable_result_capture_metadata (dmp); if (game_source_lba != XBOX_XISO_STANDARD_GAME_LBA && disc_is_xbox_challenge_drive (dmp -> dsk)) warning ("Xbox XDVDFS was detected at LBA %u, not the expected GDR-8050L game-view LBA 32", game_source_lba); @@ -1085,11 +1347,11 @@ static int dumper_dump_xbox_redump_iso (dumper *dmp, u_int32_t *current_sector) (game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ? game_source_lba - 1 : XBOX_XISO_LEADIN_SECTORS - 1); if (!xbox_write_leadin_iso_range (dmp, (game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ? game_source_lba - XBOX_XISO_LEADIN_SECTORS : 0, - XBOX_XGD1_GAME_OUTPUT_START_LBA, meta.output_sectors, &meta, "redump game", true, current_sector)) + XBOX_XGD1_GAME_OUTPUT_START_LBA, meta.output_sectors, &meta, "redump game", current_sector)) goto cleanup; if (!xbox_dump_read10_iso_range (dmp, game_source_lba, XBOX_XGD1_GAME_SOURCE_SECTORS, - XBOX_XGD1_GAME_OUTPUT_START_LBA + XBOX_XISO_LEADIN_SECTORS, meta.output_sectors, "GAME-XDVDFS", true, current_sector)) + XBOX_XGD1_GAME_OUTPUT_START_LBA + XBOX_XISO_LEADIN_SECTORS, meta.output_sectors, "GAME-XDVDFS", current_sector)) goto cleanup; if (current_sector) *current_sector = XBOX_XGD1_GAME_OUTPUT_START_LBA + XBOX_XGD1_UNLOCKED_GAME_VIEW_SECTORS; @@ -1111,8 +1373,22 @@ static int dumper_dump_xbox_redump_iso (dumper *dmp, u_int32_t *current_sector) out = true; cleanup: + if (!out) { + /* A failed or cancelled Xbox reconstruction still owns an open output + * stream. Finalize the partial streaming hash and close the file so the + * native report can observe the exact completed byte count and the caller + * never leaks the handle. */ + if (dmp -> hashing) + multihash_finish (&(dmp -> hash_iso)); + if (dmp -> fp_iso) { + fflush (dmp -> fp_iso); + fclose (dmp -> fp_iso); + dmp -> fp_iso = NULL; + } + } if (video_l1) free (video_l1); + xbox_portable_result_finish (dmp, out, meta.output_sectors, portable_start, false); /* Keep current_sector at the last phase-specific LBA on failure so the * CLI can report a useful range instead of collapsing every setup failure * back to 0..15. */ @@ -1169,6 +1445,12 @@ int dumper_dump (dumper *dmp, u_int32_t *current_sector) { last_sector=sectors_no-1; for (i = dmp -> start_sector, out = true; i < sectors_no && out; i++) { + if (dumper_cancel_requested (dmp)) { + out = false; + if (current_sector) + *current_sector = i; + break; + } if (!dumper_read_sector_with_retry (dmp, i, &isobuf, &rawbuf)) { error ("Read failed after dump-level retries at sectors %u..%u", i, i + SECTORS_PER_BLOCK - 1); out = false; @@ -1251,6 +1533,7 @@ int dumper_dump_xiso (dumper *dmp, u_int32_t *current_sector) { u_int8_t sector_buf[SECTOR_SIZE]; u_int32_t source_lba, source_sectors, target_sectors, i, source_sector, last_sector, leadin_source_lba; bool leadin_fallback_warned; + double portable_start = 0.0; if (!dmp) { error ("XISO dumper is not prepared"); @@ -1267,7 +1550,9 @@ int dumper_dump_xiso (dumper *dmp, u_int32_t *current_sector) { if (current_sector) *current_sector = 0; xbox_ref_log_fprintf (stderr, "[XBOX] Dispatching GDR-8050L -X directly to copied original dumper code using FriiDump's existing drive HANDLE.\n"); - ref_status = xbox_ref_gdr8050l_dump_with_handle (disc_get_native_handle (dmp -> dsk), disc_get_device (dmp -> dsk), dmp -> outfile_xiso, '2', &(dmp -> xbox_ref_result)); + ref_status = xbox_ref_gdr8050l_dump_with_handle (disc_get_native_handle (dmp -> dsk), disc_get_device (dmp -> dsk), dmp -> outfile_xiso, '2', dumper_xbox_ref_cancel_requested, dmp, &(dmp -> xbox_ref_result)); + if (current_sector) + *current_sector = dmp -> xbox_ref_result.completed_sectors; return ref_status == 0; } #endif @@ -1277,16 +1562,22 @@ int dumper_dump_xiso (dumper *dmp, u_int32_t *current_sector) { return false; } + portable_start = xbox_portable_now_seconds (); + xbox_portable_result_begin (dmp, dmp -> outfile_xiso, '2'); + if (!xbox_get_xiso_range (dmp -> dsk, &source_lba, &source_sectors, &target_sectors)) { if (current_sector) *current_sector = dmp -> start_sector; + xbox_portable_result_finish (dmp, false, 0, portable_start, true); return false; } + xbox_portable_result_capture_metadata (dmp); if (dmp -> start_sector >= target_sectors) { error ("Cannot resume Xbox XISO at output sector %u; target has only %u sectors", dmp -> start_sector, target_sectors); if (current_sector) *current_sector = dmp -> start_sector; + xbox_portable_result_finish (dmp, false, target_sectors, portable_start, true); return false; } @@ -1302,6 +1593,12 @@ int dumper_dump_xiso (dumper *dmp, u_int32_t *current_sector) { dmp -> progress (true, dmp -> start_sector, target_sectors, dmp -> progress_data); for (i = dmp -> start_sector; i < target_sectors && out; i++) { + if (dumper_cancel_requested (dmp)) { + out = false; + if (current_sector) + *current_sector = i; + break; + } if (i < XBOX_XISO_LEADIN_SECTORS) { isobuf = NULL; rawbuf = NULL; @@ -1368,6 +1665,8 @@ int dumper_dump_xiso (dumper *dmp, u_int32_t *current_sector) { dmp -> fp_xiso = NULL; } + xbox_portable_result_finish (dmp, out, target_sectors, portable_start, true); + return (out); } @@ -1394,6 +1693,19 @@ void dumper_set_progress_callback (dumper *dmp, progress_func progress, void *pr } +void dumper_set_cancel_callback (dumper *dmp, dumper_cancel_func cancel, void *cancel_data) { + if (!dmp) + return; + dmp -> cancel = cancel; + dmp -> cancel_data = cancel_data; +} + + +bool dumper_was_cancelled (dumper *dmp) { + return dmp && dmp -> cancelled; +} + + void dumper_set_hashing (dumper *dmp, bool h) { dmp -> hashing = h; debug ("Hashing %s", h ? "enabled" : "disabled"); @@ -1505,3 +1817,7 @@ bool dumper_get_xbox_reference_result (dumper *dmp, xbox_ref_dump_result *result *result = dmp -> xbox_ref_result; return true; } + +u_int32_t dumper_get_start_sector (dumper *dmp) { + return dmp ? dmp -> start_sector : 0; +} diff --git a/libfriidump/dumper.h b/libfriidump/dumper.h index c3756ca..edb59df 100644 --- a/libfriidump/dumper.h +++ b/libfriidump/dumper.h @@ -33,6 +33,7 @@ extern "C" { typedef struct dumper_s dumper; typedef void (*progress_func) (bool start, u_int32_t current_sector, u_int32_t total_sectors, void *progress_data); +typedef bool (*dumper_cancel_func) (void *cancel_data); FRIIDUMPLIB_EXPORT bool dumper_set_raw_output_file (dumper *dmp, char *outfile_raw, bool resume); FRIIDUMPLIB_EXPORT bool dumper_set_iso_output_file (dumper *dmp, char *outfile_iso, bool resume); @@ -43,6 +44,8 @@ FRIIDUMPLIB_EXPORT int dumper_dump (dumper *dmp, u_int32_t *current_sector); FRIIDUMPLIB_EXPORT int dumper_dump_xiso (dumper *dmp, u_int32_t *current_sector); FRIIDUMPLIB_EXPORT dumper *dumper_new (disc *d); FRIIDUMPLIB_EXPORT void dumper_set_progress_callback (dumper *dmp, progress_func progress, void *progress_data); +FRIIDUMPLIB_EXPORT void dumper_set_cancel_callback (dumper *dmp, dumper_cancel_func cancel, void *cancel_data); +FRIIDUMPLIB_EXPORT bool dumper_was_cancelled (dumper *dmp); FRIIDUMPLIB_EXPORT void dumper_set_hashing (dumper *dmp, bool h); FRIIDUMPLIB_EXPORT void dumper_set_flushing (dumper *dmp, bool f); FRIIDUMPLIB_EXPORT void *dumper_destroy (dumper *dmp); @@ -64,6 +67,7 @@ FRIIDUMPLIB_EXPORT char *dumper_get_iso_sha2 (dumper *dmp); FRIIDUMPLIB_EXPORT char *dumper_get_xiso_sha2 (dumper *dmp); FRIIDUMPLIB_EXPORT char *dumper_get_raw_sha2 (dumper *dmp); FRIIDUMPLIB_EXPORT bool dumper_get_xbox_reference_result (dumper *dmp, xbox_ref_dump_result *result); +FRIIDUMPLIB_EXPORT u_int32_t dumper_get_start_sector (dumper *dmp); #ifdef __cplusplus } diff --git a/libfriidump/dvd_drive.c b/libfriidump/dvd_drive.c index 036d38b..fe71f80 100644 --- a/libfriidump/dvd_drive.c +++ b/libfriidump/dvd_drive.c @@ -44,12 +44,14 @@ #include "dvd_drive.h" #include "disc.h" #include "sha1.h" +#include "xbox_ref/xbox_ref_log.h" #ifdef WIN32 #include #include #else #include +#include #include #include #include @@ -99,6 +101,9 @@ struct dvd_drive_s { const char *hlds_e7_record_id; const char *hlds_e7_notes; + /* Last transport command evidence for release-build diagnostics. */ + dvd_command_diagnostic last_command; + /* Device-dependent internal memory dump function */ /*! The intended area should start where sector data is stored upon a READ command. Here we assume that sectors are * stored one after the other, as heuristics showed it is the case for the Hitachi MN103-based drives, but this model @@ -509,6 +514,41 @@ void dvd_init_command (mmc_command *mmc, u_int8_t *buf, int len, req_sense *sens } +static void dvd_record_command_diagnostic ( + dvd_drive *dvd, + mmc_command *mmc, + int transport_result, + int os_error, + int scsi_status, + int sense_key, + int asc, + int ascq +) { + if (!dvd || !mmc) + return; + memset (&dvd -> last_command, 0, sizeof (dvd -> last_command)); + dvd -> last_command.valid = true; + dvd -> last_command.transport_result = transport_result; + dvd -> last_command.os_error = os_error; + dvd -> last_command.scsi_status = scsi_status; + dvd -> last_command.sense_key = sense_key & 0x0f; + dvd -> last_command.asc = asc & 0xff; + dvd -> last_command.ascq = ascq & 0xff; + dvd -> last_command.cdb_length = mmc -> cmdlen; + if (dvd -> last_command.cdb_length < 0) + dvd -> last_command.cdb_length = 0; + if (dvd -> last_command.cdb_length > (int) sizeof (dvd -> last_command.cdb)) + dvd -> last_command.cdb_length = (int) sizeof (dvd -> last_command.cdb); + memcpy (dvd -> last_command.cdb, mmc -> cmd, sizeof (dvd -> last_command.cdb)); +} + +bool dvd_get_last_command_diagnostic (dvd_drive *dvd, dvd_command_diagnostic *out) { + if (!dvd || !out || !dvd -> last_command.valid) + return false; + *out = dvd -> last_command; + return true; +} + #ifdef WIN32 /* Doc is under the UNIX function */ @@ -516,6 +556,8 @@ int dvd_execute_cmd (dvd_drive *dvd, mmc_command *mmc, bool ignore_errors) { SCSI_PASS_THROUGH_DIRECT *sptd; unsigned char sptd_sense[sizeof (*sptd) + 18], *sense; DWORD bytes; + DWORD win_error; + BOOL ioctl_ok; int out; sptd = (SCSI_PASS_THROUGH_DIRECT *) sptd_sense; @@ -533,39 +575,34 @@ int dvd_execute_cmd (dvd_drive *dvd, mmc_command *mmc, bool ignore_errors) { else sptd -> DataIn = SCSI_IOCTL_DATA_IN; sptd -> DataBuffer = mmc -> buffer; - // Quick hack: Windows hates sptd->DataTransferLength = 1, so we set it to 2 and ignore the second byte. - if (mmc -> buflen == 1) // TODO + /* Quick hack: Windows hates DataTransferLength = 1. */ + if (mmc -> buflen == 1) sptd -> DataTransferLength = 2; else sptd -> DataTransferLength = mmc -> buflen; sptd -> TimeOutValue = MMC_CMD_TIMEOUT; sptd -> SenseInfoOffset = sizeof (*sptd); - //fprintf (stdout,"mmc->cmd[00] = %d \n",mmc->cmd[00]); - //Set streaming hack - if (mmc->cmd[00]==0xB6) { + if (mmc -> cmd[0] == 0xB6) { sptd -> DataIn = SCSI_IOCTL_DATA_OUT; sptd -> DataTransferLength = 28; } - { - BOOL ioctl_ok = DeviceIoControl (dvd -> fd, IOCTL_SCSI_PASS_THROUGH_DIRECT, - sptd, sizeof (*sptd) + 18, sptd, sizeof (*sptd) + 18, &bytes, NULL); - /* DeviceIoControl may succeed while the drive returns CHECK CONDITION. - * Treat any non-GOOD SCSI status as command failure. ignore_errors only - * suppresses diagnostics; it must not turn failed commands into success. */ - if (!ioctl_ok || sptd -> ScsiStatus != 0) { - out = -1; /* Failure */ - if (!ignore_errors) { - error ("Execution of MMC command failed: Win32=%lu SCSI=0x%02X", - (unsigned long) GetLastError (), sptd -> ScsiStatus); - debug ("Command was: "); - hex_and_ascii_print ("", mmc -> cmd, sizeof (mmc -> cmd)); - debug ("Sense data: %02X/%02X/%02X\n", sense[2] & 0x0F, sense[12], sense[13]); - } - } else { - out = 0; + ioctl_ok = DeviceIoControl (dvd -> fd, IOCTL_SCSI_PASS_THROUGH_DIRECT, + sptd, sizeof (*sptd) + 18, sptd, sizeof (*sptd) + 18, &bytes, NULL); + win_error = ioctl_ok ? ERROR_SUCCESS : GetLastError (); + /* DeviceIoControl may succeed while the drive returns CHECK CONDITION. */ + if (!ioctl_ok || sptd -> ScsiStatus != 0) { + out = -1; + if (!ignore_errors) { + error ("Execution of MMC command failed: Win32=%lu SCSI=0x%02X", + (unsigned long) win_error, sptd -> ScsiStatus); + debug ("Command was: "); + hex_and_ascii_print ("", mmc -> cmd, sizeof (mmc -> cmd)); + debug ("Sense data: %02X/%02X/%02X\n", sense[2] & 0x0F, sense[12], sense[13]); } + } else { + out = 0; } if (mmc -> sense) { @@ -573,8 +610,10 @@ int dvd_execute_cmd (dvd_drive *dvd, mmc_command *mmc, bool ignore_errors) { mmc -> sense -> asc = sense[12]; mmc -> sense -> ascq = sense[13]; } + dvd_record_command_diagnostic (dvd, mmc, out, (int) win_error, + (int) sptd -> ScsiStatus, sense[2], sense[12], sense[13]); - return (out); + return out; } #else @@ -588,6 +627,7 @@ int dvd_execute_cmd (dvd_drive *dvd, mmc_command *mmc, bool ignore_errors) { */ int dvd_execute_cmd (dvd_drive *dvd, mmc_command *mmc, bool ignore_errors) { int out; + int saved_errno; struct cdrom_generic_command cgc; struct request_sense sense; @@ -596,8 +636,8 @@ int dvd_execute_cmd (dvd_drive *dvd, mmc_command *mmc, bool ignore_errors) { hex_and_ascii_print ("", mmc -> cmd, sizeof (mmc -> cmd)); #endif - /* Init Linux-format MMC command */ - memset (&cgc, 0, sizeof (struct cdrom_generic_command)); + memset (&sense, 0, sizeof (sense)); + memset (&cgc, 0, sizeof (cgc)); memcpy (cgc.cmd, mmc -> cmd, sizeof (mmc -> cmd)); cgc.buffer = (unsigned char *) mmc -> buffer; cgc.buflen = mmc -> buflen; @@ -607,17 +647,19 @@ int dvd_execute_cmd (dvd_drive *dvd, mmc_command *mmc, bool ignore_errors) { cgc.data_direction = CGC_DATA_NONE; else cgc.data_direction = CGC_DATA_READ; - cgc.timeout = MMC_CMD_TIMEOUT * 1000; /* Linux uses milliseconds */ + cgc.timeout = MMC_CMD_TIMEOUT * 1000; cgc.sense = &sense; if (ioctl (dvd -> fd, CDROM_SEND_PACKET, &cgc) < 0) { - out = -1; /* Failure */ + saved_errno = errno; + out = -1; if (!ignore_errors) { - error ("Execution of MMC command failed: %s", strerror (errno)); + error ("Execution of MMC command failed: %s", strerror (saved_errno)); debug ("Command was:"); hex_and_ascii_print ("", cgc.cmd, sizeof (cgc.cmd)); debug ("Sense data: %02X/%02X/%02X", sense.sense_key, sense.asc, sense.ascq); } } else { + saved_errno = 0; out = 0; } @@ -626,8 +668,220 @@ int dvd_execute_cmd (dvd_drive *dvd, mmc_command *mmc, bool ignore_errors) { mmc -> sense -> asc = sense.asc; mmc -> sense -> ascq = sense.ascq; } + dvd_record_command_diagnostic (dvd, mmc, out, saved_errno, + (int) cgc.stat, sense.sense_key, sense.asc, sense.ascq); - return (out); + return out; +} +#endif + +#ifndef WIN32 +/* + * Linux equivalent of the copied Windows SCSI_PASS_THROUGH_DIRECT transport + * used by UnlockDrive(). CDROM_SEND_PACKET does not expose an explicit CDB + * length and applies a single generic timeout. The GDR-8050L handshake uses + * 6-, 10-, and 12-byte CDBs plus 120-second command timeouts (10 seconds only + * for sticky descrambling). SG_IO preserves those boundaries exactly. + */ +static int dvd_xbox_sgio_exact (dvd_drive *dvd, + const char *step, + const u_int8_t *cdb, + int cdb_len, + u_int8_t *buf, + u_int32_t buf_len, + dvd_data_direction direction, + unsigned int timeout_ms) { + sg_io_hdr_t io; + u_int8_t sense[32]; + mmc_command diagnostic; + int rc; + int saved_errno = 0; + int transport_result; + int i; + + if (!dvd || !cdb || cdb_len < 1 || cdb_len > 12) + return -1; + + memset (&io, 0, sizeof (io)); + memset (sense, 0, sizeof (sense)); + memset (&diagnostic, 0, sizeof (diagnostic)); + + io.interface_id = 'S'; + io.cmdp = (unsigned char *) cdb; + io.cmd_len = (unsigned char) cdb_len; + io.sbp = sense; + io.mx_sb_len = sizeof (sense); + io.timeout = timeout_ms; + io.dxferp = buf; + io.dxfer_len = buf_len; + + switch (direction) { + case DVD_DATA_OUT: + io.dxfer_direction = SG_DXFER_TO_DEV; + break; + case DVD_DATA_NONE: + io.dxfer_direction = SG_DXFER_NONE; + io.dxferp = NULL; + io.dxfer_len = 0; + break; + case DVD_DATA_IN: + default: + io.dxfer_direction = SG_DXFER_FROM_DEV; + break; + } + + errno = 0; + rc = ioctl (dvd -> fd, SG_IO, &io); + if (rc < 0) + saved_errno = errno; + + transport_result = + (rc == 0 && + io.status == 0 && + io.host_status == 0 && + io.driver_status == 0) ? 0 : -1; + + diagnostic.cmdlen = cdb_len; + diagnostic.direction = direction; + diagnostic.buffer = buf; + diagnostic.buflen = (int) buf_len; + memcpy (diagnostic.cmd, cdb, (size_t) cdb_len); + dvd_record_command_diagnostic ( + dvd, + &diagnostic, + transport_result, + saved_errno, + (int) io.status, + sense[2], + sense[12], + sense[13]); + + xbox_ref_log_fprintf ( + stderr, + "[XBOX-SGIO] step=%s rc=%d errno=%d status=0x%02X host=0x%04X driver=0x%04X sense=%02X/%02X/%02X resid=%d timeout_ms=%u cdb=[", + step ? step : "unnamed", + rc, + saved_errno, + (unsigned int) io.status, + (unsigned int) io.host_status, + (unsigned int) io.driver_status, + (unsigned int) (sense[2] & 0x0F), + (unsigned int) sense[12], + (unsigned int) sense[13], + io.resid, + timeout_ms); + for (i = 0; i < cdb_len; i++) + xbox_ref_log_fprintf ( + stderr, + "%s%02X", + i ? " " : "", + (unsigned int) cdb[i]); + xbox_ref_log_fprintf ( + stderr, + "] xfer=%u direction=%s result=%s\n", + buf_len, + direction == DVD_DATA_OUT ? "out" : + direction == DVD_DATA_NONE ? "none" : "in", + transport_result == 0 ? "PASS" : "FAIL"); + + return transport_result; +} + +static int dvd_xbox_exact_read_capacity (dvd_drive *dvd, + const char *step, + u_int32_t *sectors, + u_int32_t *sector_size) { + u_int8_t cdb[10]; + u_int8_t buf[8]; + u_int32_t max_lba; + + memset (cdb, 0, sizeof (cdb)); + memset (buf, 0, sizeof (buf)); + cdb[0] = 0x25; + + if (dvd_xbox_sgio_exact ( + dvd, step, cdb, sizeof (cdb), buf, sizeof (buf), + DVD_DATA_IN, 120000) < 0) + return -1; + + max_lba = + ((u_int32_t) buf[0] << 24) | + ((u_int32_t) buf[1] << 16) | + ((u_int32_t) buf[2] << 8) | + (u_int32_t) buf[3]; + + if (sectors) + *sectors = max_lba + 1; + if (sector_size) + *sector_size = + ((u_int32_t) buf[4] << 24) | + ((u_int32_t) buf[5] << 16) | + ((u_int32_t) buf[6] << 8) | + (u_int32_t) buf[7]; + return 0; +} + +static int dvd_xbox_exact_mode_sense_10 (dvd_drive *dvd, + const char *step, + u_int8_t page, + u_int8_t *buf, + size_t buf_len) { + u_int8_t cdb[10]; + + if (!buf || buf_len > 0xFFFF) + return -1; + memset (cdb, 0, sizeof (cdb)); + memset (buf, 0, buf_len); + cdb[0] = 0x5A; + cdb[2] = page; + cdb[7] = (u_int8_t) ((buf_len >> 8) & 0xFF); + cdb[8] = (u_int8_t) (buf_len & 0xFF); + + return dvd_xbox_sgio_exact ( + dvd, step, cdb, sizeof (cdb), buf, (u_int32_t) buf_len, + DVD_DATA_IN, 120000); +} + +static int dvd_xbox_exact_mode_select_10 (dvd_drive *dvd, + const char *step, + const u_int8_t *buf, + size_t buf_len) { + u_int8_t cdb[10]; + u_int8_t tmp[256]; + + if (!buf || buf_len > sizeof (tmp) || buf_len > 0xFFFF) + return -1; + memset (cdb, 0, sizeof (cdb)); + memset (tmp, 0, sizeof (tmp)); + memcpy (tmp, buf, buf_len); + cdb[0] = 0x55; + cdb[7] = (u_int8_t) ((buf_len >> 8) & 0xFF); + cdb[8] = (u_int8_t) (buf_len & 0xFF); + + return dvd_xbox_sgio_exact ( + dvd, step, cdb, sizeof (cdb), tmp, (u_int32_t) buf_len, + DVD_DATA_OUT, 120000); +} + +static int dvd_xbox_exact_mode_select_6 (dvd_drive *dvd, + const char *step, + const u_int8_t *buf, + size_t buf_len) { + u_int8_t cdb[6]; + u_int8_t tmp[64]; + + if (!buf || buf_len > sizeof (tmp) || buf_len > 0xFF) + return -1; + memset (cdb, 0, sizeof (cdb)); + memset (tmp, 0, sizeof (tmp)); + memcpy (tmp, buf, buf_len); + cdb[0] = 0x15; + cdb[1] = 0x11; + cdb[4] = (u_int8_t) buf_len; + + return dvd_xbox_sgio_exact ( + dvd, step, cdb, sizeof (cdb), tmp, (u_int32_t) buf_len, + DVD_DATA_OUT, 10000); } #endif @@ -1012,6 +1266,20 @@ int dvd_stop_unit (dvd_drive *dvd, bool start, req_sense *sense) { return dvd_start_stop_unit (dvd, start, false, sense); } +int dvd_set_door_lock (dvd_drive *dvd, bool locked) { +#ifdef WIN32 + (void) dvd; + (void) locked; + return 0; +#else + if (!dvd) + return -EINVAL; + if (ioctl (dvd -> fd, CDROM_LOCKDOOR, locked ? 1 : 0) < 0) + return -errno; + return 0; +#endif +} + int dvd_wait_ready (dvd_drive *dvd, unsigned int timeout_ms) { unsigned int waited = 0; if (!dvd) @@ -1031,28 +1299,59 @@ int dvd_media_cycle (dvd_drive *dvd, req_sense *sense) { int i; int out; +#ifndef WIN32 + /* Linux commonly applies CDO_LOCK while an optical device is open. A raw + * START STOP UNIT eject then fails immediately even though the process owns + * the only intentional handle. Release that kernel/drive door lock before + * the GDR-8050L software tray cycle, then restore it after the tray is loaded + * and ready. CDROM_LOCKDOOR is the documented Linux optical-door API. */ + out = dvd_set_door_lock (dvd, false); + if (out < 0) { + xbox_ref_log_fprintf (stderr, + "[XBOX][FATAL] Linux media-cycle could not release the optical door lock: %s (errno=%d).\n", + strerror (-out), -out); + return out; + } + xbox_ref_log_fprintf (stderr, "[XBOX] Linux media-cycle released the optical door lock before software eject.\n"); +#endif + /* Match the original GDR-8050L dumper's AutomateTrayCycle(): eject, wait * long enough for the tray to extend, close, poll readiness, then settle. */ out = dvd_start_stop_unit (dvd, false, true, sense); /* LoEj=1, Start=0: eject */ - if (out < 0) + if (out < 0) { + xbox_ref_log_fprintf (stderr, "[XBOX][FATAL] Media-cycle software eject command failed.\n"); return out; + } dvd_sleep_ms (3000); out = dvd_start_stop_unit (dvd, true, true, sense); /* LoEj=1, Start=1: load */ - if (out < 0) + if (out < 0) { + xbox_ref_log_fprintf (stderr, "[XBOX][FATAL] Media-cycle software load command failed; the door remains unlocked for recovery.\n"); return out; + } for (i = 0; i < 90; i++) { dvd_sleep_ms (500); if (dvd_test_unit_ready (dvd, NULL) == 0) { dvd_sleep_ms (1500); +#ifndef WIN32 + out = dvd_set_door_lock (dvd, true); + if (out < 0) + xbox_ref_log_fprintf (stderr, + "[XBOX][WARN] Linux media-cycle completed, but the optical door could not be re-locked: %s (errno=%d).\n", + strerror (-out), -out); + else + xbox_ref_log_fprintf (stderr, "[XBOX] Linux media-cycle restored the optical door lock after load.\n"); +#endif return 0; } } /* Original dumper falls back to a fixed 10s settle delay if TUR never - * reports ready after tray close. */ + * reports ready after tray close. Keep the door unlocked on failure so the + * user can recover the media without power-cycling the external drive. */ dvd_sleep_ms (10000); + fprintf (stderr, "[XBOX][FATAL] Media-cycle tray closed, but the drive never became ready; the door remains unlocked for recovery.\n"); return -1; } @@ -1324,10 +1623,13 @@ int dvd_read_dvd_structure (dvd_drive *dvd, u_int8_t format, u_int8_t layer, u_i dvd_init_command (&mmc, extbuf, (int) extbufsize, sense); mmc.cmd[0] = MMC_READ_DVD_STRUCTURE; + /* MMC READ DVD STRUCTURE places Format in CDB byte 7. Byte 11 is + * Control and must remain zero. The copied Windows GetMediaID() path + * uses the same byte-7 boundary for DMI format 0x04. */ mmc.cmd[6] = layer; + mmc.cmd[7] = format; mmc.cmd[8] = (u_int8_t) ((extbufsize & 0xFF00) >> 8); mmc.cmd[9] = (u_int8_t) (extbufsize & 0x00FF); - mmc.cmd[11] = format; mmc.cmdlen = 12; return dvd_execute_cmd (dvd, &mmc, false); @@ -1430,47 +1732,109 @@ int dvd_xbox_vendor_unlock_wxripper (dvd_drive *dvd, u_int32_t *unlocked_sectors } -int dvd_xbox_recovery_kick (dvd_drive *dvd, bool auth_recovery) { - mmc_command mmc; - u_int8_t dummy[2048]; - u_int8_t auth_probe_buf[8]; - int i; +#define XBOX_LOCKED_VIDEO_VIEW_MAX_SECTORS 200000U + +static int dvd_xbox_refresh_ready_capacity (dvd_drive *dvd, + u_int32_t *sectors, + u_int32_t *sector_size) { + u_int32_t observed_sectors = 0; + u_int32_t observed_sector_size = 0; if (!dvd) return -1; - /* Mirrors the proven dumper recovery cadence: after the GDR-8050L - * authentication/view switch, re-trigger the Xbox media auth command, set - * maximum speed, then issue a few harmless READ(10) probes. All commands - * deliberately ignore errors because the point is to wake/re-prime firmware - * state, not to make any one probe authoritative. - */ - if (auth_recovery) { - /* GDR-8050L / HLDS auth kick: READ DVD STRUCTURE C0 magic trigger. */ - dvd_init_command (&mmc, auth_probe_buf, sizeof (auth_probe_buf), NULL); - mmc.cmd[0] = MMC_READ_DVD_STRUCTURE; - mmc.cmd[2] = 0xFF; - mmc.cmd[3] = 0x02; - mmc.cmd[4] = 0xFD; - mmc.cmd[11] = 0xC0; - mmc.cmdlen = 12; - dvd_execute_cmd (dvd, &mmc, true); - - /* Hitachi/Kreon-style fallback auth kick: FF 08 01. */ - dvd_init_command (&mmc, NULL, 0, NULL); - mmc.cmd[0] = 0xFF; - mmc.cmd[1] = 0x08; - mmc.cmd[2] = 0x01; - mmc.cmdlen = 10; - mmc.direction = DVD_DATA_NONE; - dvd_execute_cmd (dvd, &mmc, true); + /* Exact portable equivalent of xbox_ref_refresh_ready_capacity(): + * RefreshVolume(); Sleep(2000); EnsureDriveReady(30000); GetTotalSectors(). */ + dvd_refresh_volume (dvd); + dvd_sleep_ms (2000); + if (dvd_wait_ready (dvd, 30000) < 0) + return -1; + if (dvd_read_capacity_10 (dvd, &observed_sectors, &observed_sector_size, NULL) < 0) + return -1; + + if (sectors) + *sectors = observed_sectors; + if (sector_size) + *sector_size = observed_sector_size; + return 0; +} + +int dvd_xbox_prepare_game_view (dvd_drive *dvd, + u_int32_t *sectors, + u_int32_t *sector_size) { + u_int32_t entry_sectors = 0; + u_int32_t observed_sectors = 0; + u_int32_t observed_sector_size = 0; + + if (!dvd || !dvd_is_xbox_challenge_drive (dvd)) + return -1; + + /* Port xbox_ref_gdr8050l_dump_core() state preparation exactly, replacing + * Win32 handle/volume calls with FriiDump's portable Linux equivalents. + * No RecoveryKick, media-auth kick, or synthetic LBA-zero read cadence is + * part of this stock/cross-flashed GDR-8050L sequence. */ + if (dvd_wait_ready (dvd, 30000) < 0) { + xbox_ref_log_fprintf (stderr, + "[XBOX-WINSEQ][FATAL] Drive did not become ready before Xbox state preparation.\n"); + return -1; + } + if (dvd_read_capacity_10 (dvd, &entry_sectors, &observed_sector_size, NULL) < 0) { + xbox_ref_log_fprintf (stderr, + "[XBOX-WINSEQ][FATAL] Entry READ CAPACITY failed.\n"); + return -1; + } + xbox_ref_log_fprintf (stderr, + "[XBOX-WINSEQ] Entry READ CAPACITY: %u sectors.\n", + entry_sectors); + + if (entry_sectors > XBOX_LOCKED_VIDEO_VIEW_MAX_SECTORS) { + xbox_ref_log_fprintf (stderr, + "[XBOX-WINSEQ] Entry state already exposes the Xbox game view; skipping the redundant initial handshake and tray cycle.\n"); + if (dvd_xbox_refresh_ready_capacity (dvd, &observed_sectors, &observed_sector_size) < 0) + return -1; + } else { + xbox_ref_log_fprintf (stderr, + "[XBOX-WINSEQ] Entry state appears locked/video; attempting the full handshake directly without a media transition.\n"); + if (dvd_xbox_gdr8050l_unlock (dvd, NULL) < 0) + xbox_ref_log_fprintf (stderr, + "[XBOX-WINSEQ][WARN] Direct UnlockDrive transport returned failure; READ CAPACITY remains authoritative.\n"); + if (dvd_xbox_refresh_ready_capacity (dvd, &observed_sectors, &observed_sector_size) < 0) + return -1; + xbox_ref_log_fprintf (stderr, + "[XBOX-WINSEQ] Direct-handshake READ CAPACITY: %u sectors.\n", + observed_sectors); + + if (observed_sectors <= XBOX_LOCKED_VIDEO_VIEW_MAX_SECTORS) { + xbox_ref_log_fprintf (stderr, + "[XBOX-WINSEQ][WARN] Direct handshake did not expose the Xbox game view; performing one tray-cycle recovery and retry.\n"); + if (dvd_media_cycle (dvd, NULL) < 0) + return -1; + xbox_ref_log_fprintf (stderr, + "[XBOX-WINSEQ] Re-applying the full handshake after recovery media change.\n"); + if (dvd_xbox_gdr8050l_unlock (dvd, NULL) < 0) + xbox_ref_log_fprintf (stderr, + "[XBOX-WINSEQ][WARN] Recovery UnlockDrive transport returned failure; READ CAPACITY remains authoritative.\n"); + if (dvd_xbox_refresh_ready_capacity (dvd, &observed_sectors, &observed_sector_size) < 0) + return -1; + xbox_ref_log_fprintf (stderr, + "[XBOX-WINSEQ] Recovery-handshake READ CAPACITY: %u sectors.\n", + observed_sectors); + + if (observed_sectors <= XBOX_LOCKED_VIDEO_VIEW_MAX_SECTORS) { + xbox_ref_log_fprintf (stderr, + "[XBOX-WINSEQ][FATAL] Xbox game view was not established after direct and recovery handshakes.\n"); + return -1; + } + } } + /* Exact next Windows step after state preparation. */ dvd_set_speed (dvd, 0xFFFF, NULL); - for (i = 0; i < 10; i++) - dvd_read_10 (dvd, 0, 1, NULL, dummy, sizeof (dummy)); - + if (sectors) + *sectors = observed_sectors; + if (sector_size) + *sector_size = observed_sector_size; return 0; } @@ -1496,11 +1860,16 @@ int dvd_lock_volume (dvd_drive *dvd) { #ifdef WIN32 DWORD bytesReturned = 0; if (!dvd) - return -1; - return DeviceIoControl (dvd -> fd, FSCTL_LOCK_VOLUME, NULL, 0, NULL, 0, &bytesReturned, NULL) ? 0 : -1; + return DVD_VOLUME_LOCK_FAILED; + return DeviceIoControl (dvd -> fd, FSCTL_LOCK_VOLUME, NULL, 0, NULL, 0, &bytesReturned, NULL) + ? DVD_VOLUME_LOCK_OK + : DVD_VOLUME_LOCK_FAILED; #else + /* Linux CDROM_SEND_PACKET has no FSCTL_LOCK_VOLUME equivalent here. + * Return a distinct result instead of falsely reporting an exclusive lock. + * The caller may continue after warning about automount/media polling. */ (void) dvd; - return 0; + return DVD_VOLUME_LOCK_UNAVAILABLE; #endif } @@ -1519,6 +1888,7 @@ static int dvd_xbox_read_host_challenge_table (dvd_drive *dvd, u_int8_t *table, if (!table || table_len < 0x664) return -1; +#ifdef WIN32 /* GDR-8050L / Xbox READ DVD STRUCTURE format C0. */ dvd_init_command (&mmc, table, 0x664, NULL); mmc.cmd[0] = MMC_READ_DVD_STRUCTURE; @@ -1543,14 +1913,65 @@ static int dvd_xbox_read_host_challenge_table (dvd_drive *dvd, u_int8_t *table, mmc.cmdlen = 12; out = dvd_execute_cmd (dvd, &mmc, true); } +#else + { + u_int8_t cdb[12]; + + memset (cdb, 0, sizeof (cdb)); + memset (table, 0, table_len); + cdb[0] = 0xAD; + cdb[2] = 0xFF; + cdb[3] = 0x02; + cdb[4] = 0xFD; + cdb[5] = 0xFF; + cdb[6] = 0xFE; + cdb[8] = 0x06; + cdb[9] = 0x64; + cdb[11] = 0xC0; + out = dvd_xbox_sgio_exact ( + dvd, + "3-read-dvd-structure-c0", + cdb, + sizeof (cdb), + table, + 0x664, + DVD_DATA_IN, + 120000); + + if (out < 0 || table[772] != 1 || table[773] == 0) { + memset (cdb, 0, sizeof (cdb)); + memset (table, 0, table_len); + cdb[0] = 0xFD; + cdb[1] = 0x01; + cdb[8] = 0x06; + cdb[9] = 0x64; + out = dvd_xbox_sgio_exact ( + dvd, + "3-read-host-table-fallback-fd", + cdb, + sizeof (cdb), + table, + 0x664, + DVD_DATA_IN, + 120000); + } + } +#endif if (out < 0 || table[772] != 1) return -1; + + xbox_ref_log_fprintf ( + stderr, + "[XBOX-SGIO] challenge-table marker=%u entries-byte=%u result=PASS\n", + (unsigned int) table[772], + (unsigned int) table[773]); return 0; } int dvd_xbox_gdr8050l_unlock (dvd_drive *dvd, u_int32_t *unlocked_sectors) { int i, k, l; + int out; int chalpos[24]; u_int8_t table[0x664]; u_int8_t restable[261]; @@ -1567,8 +1988,15 @@ int dvd_xbox_gdr8050l_unlock (dvd_drive *dvd, u_int32_t *unlocked_sectors) { /* Step 1/2: read current capacity and the Xbox mode page. If the drive is * already unlocked, this is harmless; the final capacity check below becomes * the authority. */ +#ifdef WIN32 dvd_read_capacity_10 (dvd, §ors, §or_size, NULL); dvd_mode_sense_10 (dvd, 0x3E, page, sizeof (page), NULL); +#else + dvd_xbox_exact_read_capacity ( + dvd, "1-initial-read-capacity", §ors, §or_size); + dvd_xbox_exact_mode_sense_10 ( + dvd, "2-mode-sense-3e", 0x3E, page, sizeof (page)); +#endif /* Step 3: retrieve and decode the host challenge table. */ if (dvd_xbox_read_host_challenge_table (dvd, table, sizeof (table)) < 0) { @@ -1610,8 +2038,15 @@ int dvd_xbox_gdr8050l_unlock (dvd_drive *dvd, u_int32_t *unlocked_sectors) { /* Match the original dumper: send the challenge and continue even if * Windows reports a transport failure. The later XDVDFS probe is the * authority for whether the drive actually entered the game view. */ +#ifdef WIN32 dvd_mode_select_10 (dvd, page, sizeof (page), NULL); dvd_mode_sense_10 (dvd, 0x3E, page, sizeof (page), NULL); +#else + dvd_xbox_exact_mode_select_10 ( + dvd, "4-mode-select-challenge-1", page, sizeof (page)); + dvd_xbox_exact_mode_sense_10 ( + dvd, "5-mode-sense-verify-1", 0x3E, page, sizeof (page)); +#endif /* Step 6: second host challenge. */ memset (page, 0, sizeof (page)); @@ -1620,8 +2055,15 @@ int dvd_xbox_gdr8050l_unlock (dvd_drive *dvd, u_int32_t *unlocked_sectors) { page[9] = 0x12; page[12] = 0x01; memcpy (&page[15], &restable[1 + chalpos[k - 1] * 11], 5); +#ifdef WIN32 dvd_mode_select_10 (dvd, page, sizeof (page), NULL); dvd_mode_sense_10 (dvd, 0x3E, page, sizeof (page), NULL); +#else + dvd_xbox_exact_mode_select_10 ( + dvd, "6-mode-select-challenge-2", page, sizeof (page)); + dvd_xbox_exact_mode_sense_10 ( + dvd, "7-mode-sense-verify-2", 0x3E, page, sizeof (page)); +#endif /* Step 8: unlock partition 1. */ memset (page, 0, sizeof (page)); @@ -1634,14 +2076,24 @@ int dvd_xbox_gdr8050l_unlock (dvd_drive *dvd, u_int32_t *unlocked_sectors) { page[13] = 0xD1; page[14] = 0x01; memcpy (&page[15], &restable[1 + chalpos[k - 1] * 11], 5); +#ifdef WIN32 dvd_mode_select_10 (dvd, page, sizeof (page), NULL); +#else + dvd_xbox_exact_mode_select_10 ( + dvd, "8-mode-select-partition-1-unlock", page, sizeof (page)); +#endif /* Step 9: sticky descrambling, mode page 0x31. */ memset (sticky, 0, sizeof (sticky)); sticky[4] = 0x31; sticky[5] = 0x06; sticky[6] = 0x01; +#ifdef WIN32 dvd_mode_select_6 (dvd, sticky, sizeof (sticky), NULL); +#else + dvd_xbox_exact_mode_select_6 ( + dvd, "9-mode-select-sticky-descrambling", sticky, sizeof (sticky)); +#endif /* Step 10: final capacity observation. The original UnlockDrive() only * prints this verification and does not fail if the capacity has not changed @@ -1649,12 +2101,24 @@ int dvd_xbox_gdr8050l_unlock (dvd_drive *dvd, u_int32_t *unlocked_sectors) { * prime the drive before the required media-change event. */ sectors = 0; sector_size = 0; - if (dvd_read_capacity_10 (dvd, §ors, §or_size, NULL) == 0) { +#ifdef WIN32 + out = dvd_read_capacity_10 (dvd, §ors, §or_size, NULL); +#else + out = dvd_xbox_exact_read_capacity ( + dvd, "10-final-read-capacity", §ors, §or_size); +#endif + if (out == 0) { if (unlocked_sectors) *unlocked_sectors = sectors; - fprintf (stderr, "[XBOX] GDR-8050L handshake complete; READ CAPACITY reports %u sectors of %u bytes.\n", sectors, sector_size); + xbox_ref_log_fprintf ( + stderr, + "[XBOX] GDR-8050L handshake complete; READ CAPACITY reports %u sectors of %u bytes.\n", + sectors, + sector_size); } else { - fprintf (stderr, "[XBOX] GDR-8050L handshake sent; final READ CAPACITY verify failed, continuing like original dumper.\n"); + xbox_ref_log_fprintf ( + stderr, + "[XBOX] GDR-8050L handshake sent; final READ CAPACITY verify failed, continuing like original dumper.\n"); } return 0; diff --git a/libfriidump/dvd_drive.h b/libfriidump/dvd_drive.h index 2676b0a..cac3627 100644 --- a/libfriidump/dvd_drive.h +++ b/libfriidump/dvd_drive.h @@ -29,12 +29,33 @@ typedef struct dvd_drive_s dvd_drive; +typedef enum { + DVD_VOLUME_LOCK_OK = 0, + DVD_VOLUME_LOCK_UNAVAILABLE = 1, + DVD_VOLUME_LOCK_FAILED = -1 +} dvd_volume_lock_result; + typedef struct { int sense_key; int asc; int ascq; } req_sense; +/* Snapshot of the most recently executed MMC command. This is retained even + * when the caller requested quiet error handling so release builds can report + * actionable seed/cache transport diagnostics without enabling DEBUG/VERBOSE. */ +typedef struct { + bool valid; + int transport_result; + int os_error; + int scsi_status; + int sense_key; + int asc; + int ascq; + int cdb_length; + u_int8_t cdb[12]; +} dvd_command_diagnostic; + typedef enum { DVD_DATA_NONE = 0, @@ -60,6 +81,7 @@ int dvd_read_sector_streaming (dvd_drive *dvd, u_int32_t sector, req_sense *sens int dvd_read_streaming (dvd_drive *dvd, u_int32_t sector, u_int32_t sectors, req_sense *sense, u_int8_t *extbuf, size_t extbufsize); int dvd_flush_cache_READ12 (dvd_drive *dvd, u_int32_t sector, req_sense *sense); int dvd_start_stop_unit (dvd_drive *dvd, bool start, bool load_eject, req_sense *sense); +int dvd_set_door_lock (dvd_drive *dvd, bool locked); int dvd_stop_unit (dvd_drive *dvd, bool start, req_sense *sense); int dvd_media_cycle (dvd_drive *dvd, req_sense *sense); int dvd_wait_ready (dvd_drive *dvd, unsigned int timeout_ms); @@ -74,7 +96,7 @@ int dvd_xbox_gdr8050l_unlock (dvd_drive *dvd, u_int32_t *unlocked_sectors); int dvd_xbox_vendor_lock (dvd_drive *dvd); int dvd_xbox_vendor_unlock_wxripper (dvd_drive *dvd, u_int32_t *unlocked_sectors); int dvd_xbox_vendor_set_error_skip (dvd_drive *dvd, bool enabled); -int dvd_xbox_recovery_kick (dvd_drive *dvd, bool auth_recovery); +int dvd_xbox_prepare_game_view (dvd_drive *dvd, u_int32_t *sectors, u_int32_t *sector_size); int dvd_refresh_volume (dvd_drive *dvd); int dvd_lock_volume (dvd_drive *dvd); int dvd_xbox_refresh_volume (dvd_drive *dvd); @@ -115,5 +137,6 @@ void dvd_set_hlds_e7_runtime_profile (dvd_drive *dvd, u_int32_t type, u_int32_t typedef int (*dvd_drive_memdump_func) (dvd_drive *dvd, u_int32_t block_off, u_int32_t block_len, u_int32_t block_size, u_int8_t *buf); void dvd_init_command (mmc_command *mmc, u_int8_t *buf, int len, req_sense *sense); int dvd_execute_cmd (dvd_drive *dvd, mmc_command *mmc, bool ignore_errors); +bool dvd_get_last_command_diagnostic (dvd_drive *dvd, dvd_command_diagnostic *out); #endif diff --git a/libfriidump/nintendo_disc_header.c b/libfriidump/nintendo_disc_header.c new file mode 100644 index 0000000..463b728 --- /dev/null +++ b/libfriidump/nintendo_disc_header.c @@ -0,0 +1,49 @@ +/*************************************************************************** + * Nintendo GameCube/Wii disc-header classifier * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + ***************************************************************************/ + +#include "nintendo_disc_header.h" + +#define NINTENDO_WII_MAGIC 0x5d1c9ea3u +#define NINTENDO_GAMECUBE_MAGIC 0xc2339f3du + +static uint32_t nintendo_read_be32(const uint8_t *data) { + return ((uint32_t)data[0] << 24) | + ((uint32_t)data[1] << 16) | + ((uint32_t)data[2] << 8) | + (uint32_t)data[3]; +} + +nintendo_disc_header_type nintendo_disc_header_detect( + const uint8_t *sector, + size_t sector_length +) { + if (!sector || sector_length < 0x20) + return NINTENDO_DISC_HEADER_UNKNOWN; + + if (nintendo_read_be32(sector + 0x18) == NINTENDO_WII_MAGIC) + return NINTENDO_DISC_HEADER_WII; + + if (nintendo_read_be32(sector + 0x1c) == NINTENDO_GAMECUBE_MAGIC) + return NINTENDO_DISC_HEADER_GAMECUBE; + + return NINTENDO_DISC_HEADER_UNKNOWN; +} + +const char *nintendo_disc_header_type_name( + nintendo_disc_header_type type +) { + switch (type) { + case NINTENDO_DISC_HEADER_GAMECUBE: + return "GameCube"; + case NINTENDO_DISC_HEADER_WII: + return "Wii"; + default: + return "Unknown"; + } +} diff --git a/libfriidump/nintendo_disc_header.h b/libfriidump/nintendo_disc_header.h new file mode 100644 index 0000000..1aaf627 --- /dev/null +++ b/libfriidump/nintendo_disc_header.h @@ -0,0 +1,38 @@ +/*************************************************************************** + * Nintendo GameCube/Wii disc-header classifier * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + ***************************************************************************/ + +#ifndef NINTENDO_DISC_HEADER_H_INCLUDED +#define NINTENDO_DISC_HEADER_H_INCLUDED + +#include +#include + +typedef enum { + NINTENDO_DISC_HEADER_UNKNOWN = 0, + NINTENDO_DISC_HEADER_GAMECUBE, + NINTENDO_DISC_HEADER_WII +} nintendo_disc_header_type; + +/* + * Classify an unscrambled Nintendo optical-disc header. + * + * Wii discs carry big-endian magic 0x5d1c9ea3 at byte 0x18. + * GameCube discs carry big-endian magic 0xc2339f3d at byte 0x1c. + * Wii is checked first, matching the console-side discrimination order. + */ +nintendo_disc_header_type nintendo_disc_header_detect( + const uint8_t *sector, + size_t sector_length +); + +const char *nintendo_disc_header_type_name( + nintendo_disc_header_type type +); + +#endif diff --git a/libfriidump/xbox_portable.c b/libfriidump/xbox_portable.c new file mode 100644 index 0000000..ecb13a9 --- /dev/null +++ b/libfriidump/xbox_portable.c @@ -0,0 +1,373 @@ +#include "xbox_portable.h" +#ifndef XBOX_PORTABLE_NO_DISC_WRAPPER +#include "disc.h" +#endif +#include "xbox_region.h" + +#include +#include +#include + +#define XBOX_SECTOR_SIZE 2048U +#define XBOX_XDFS_MAGIC "MICROSOFT*XBOX*MEDIA" +#define XBOX_XDFS_MAGIC_SIZE 20U +#define XBOX_XDFS_STANDARD_VOLUME_LBA 32U +#define XBOX_XDFS_LEGACY_VOLUME_LBA 306112U +#define XBOX_MAX_ROOT_DIRECTORY_SECTORS 4096U +#define XBOX_XBE_CERTIFICATE_MIN_SIZE 0x0B0U + +static uint16_t xbox_le16(const unsigned char *p) +{ + return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); +} + +static uint32_t xbox_le32(const unsigned char *p) +{ + return (uint32_t)p[0] | + ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | + ((uint32_t)p[3] << 24); +} + +static int xbox_ascii_case_equal(const unsigned char *value, + size_t value_length, + const char *expected) +{ + size_t index; + size_t expected_length; + + if (!value || !expected) + return 0; + + expected_length = strlen(expected); + if (value_length != expected_length) + return 0; + + for (index = 0; index < value_length; ++index) { + if (tolower((unsigned char)value[index]) != + tolower((unsigned char)expected[index])) + return 0; + } + + return 1; +} + +static size_t xbox_utf8_append(char *output, + size_t output_size, + size_t position, + uint32_t codepoint) +{ + if (!output || output_size == 0 || position >= output_size) + return position; + + if (codepoint <= 0x7FU) { + if (position + 1 >= output_size) + return position; + output[position++] = (char)codepoint; + } else if (codepoint <= 0x7FFU) { + if (position + 2 >= output_size) + return position; + output[position++] = (char)(0xC0U | (codepoint >> 6)); + output[position++] = (char)(0x80U | (codepoint & 0x3FU)); + } else { + if (position + 3 >= output_size) + return position; + output[position++] = (char)(0xE0U | (codepoint >> 12)); + output[position++] = (char)(0x80U | ((codepoint >> 6) & 0x3FU)); + output[position++] = (char)(0x80U | (codepoint & 0x3FU)); + } + + output[position] = '\0'; + return position; +} + +static void xbox_decode_title(const unsigned char *certificate, + size_t certificate_size, + char *output, + size_t output_size) +{ + size_t index; + size_t position; + + if (!output || output_size == 0) + return; + + output[0] = '\0'; + if (!certificate || certificate_size < 0x00CU + 80U) + return; + + position = 0; + for (index = 0; index < 40U; ++index) { + uint16_t codepoint = xbox_le16(certificate + 0x00CU + (index * 2U)); + if (codepoint == 0) + break; + if (codepoint >= 0xD800U && codepoint <= 0xDFFFU) + codepoint = (uint16_t)'?'; + position = xbox_utf8_append(output, + output_size, + position, + codepoint); + } +} + +static int xbox_parse_media_id(const unsigned char *dmi, + size_t dmi_size, + char *output, + size_t output_size) +{ + size_t index; + size_t position; + + if (!output || output_size == 0) + return 0; + + output[0] = '\0'; + if (!dmi || dmi_size <= 8U) + return 0; + + position = 0; + for (index = 8U; index < dmi_size && index < 24U; ++index) { + unsigned char c = dmi[index]; + if (isalnum(c)) { + if (position + 1U >= output_size) + break; + output[position++] = (char)c; + } + } + output[position] = '\0'; + + return position >= 4U; +} + +static int xbox_find_default_xbe(xbox_portable_read10_func read10, + void *context, + uint32_t root_lba, + uint32_t root_size, + uint32_t *xbe_lba) +{ + unsigned char sector[XBOX_SECTOR_SIZE]; + uint32_t sectors; + uint32_t sector_index; + + if (!read10 || !xbe_lba || root_size == 0) + return 0; + + sectors = (root_size + XBOX_SECTOR_SIZE - 1U) / XBOX_SECTOR_SIZE; + if (sectors > XBOX_MAX_ROOT_DIRECTORY_SECTORS) + sectors = XBOX_MAX_ROOT_DIRECTORY_SECTORS; + + for (sector_index = 0; sector_index < sectors; ++sector_index) { + size_t offset = 0; + if (read10(context, + root_lba + sector_index, + 1, + sector, + sizeof(sector)) < 0) + return 0; + + while (offset + 14U <= sizeof(sector)) { + uint16_t left_entry; + uint8_t name_length; + uint32_t start_lba; + size_t next_offset; + + left_entry = xbox_le16(sector + offset); + name_length = sector[offset + 13U]; + if (left_entry == 0xFFFFU || name_length == 0U || + name_length == 0xFFU) + break; + if (offset + 14U + name_length > sizeof(sector)) + break; + + start_lba = xbox_le32(sector + offset + 4U); + if (xbox_ascii_case_equal(sector + offset + 14U, + name_length, + "default.xbe")) { + *xbe_lba = start_lba; + return 1; + } + + next_offset = (14U + (size_t)name_length + 3U) & ~((size_t)3U); + if (next_offset == 0) + break; + offset += next_offset; + } + } + + return 0; +} + +static int xbox_parse_xbe_certificate(xbox_portable_read10_func read10, + void *context, + uint32_t xbe_lba, + xbox_portable_metadata *metadata) +{ + unsigned char header[XBOX_SECTOR_SIZE]; + unsigned char certificate[XBOX_SECTOR_SIZE * 2U]; + uint32_t base_va; + uint32_t certificate_va; + uint32_t file_offset; + uint32_t certificate_lba; + size_t inner_offset; + uint32_t sectors_to_read; + const unsigned char *cert; + + if (read10(context, xbe_lba, 1, header, sizeof(header)) < 0) + return 0; + if (xbox_le32(header) != 0x48454258U) + return 0; + + base_va = xbox_le32(header + 0x104U); + certificate_va = xbox_le32(header + 0x118U); + if (certificate_va < base_va) + return 0; + + file_offset = certificate_va - base_va; + certificate_lba = xbe_lba + (file_offset / XBOX_SECTOR_SIZE); + inner_offset = file_offset % XBOX_SECTOR_SIZE; + sectors_to_read = + (inner_offset + XBOX_XBE_CERTIFICATE_MIN_SIZE > XBOX_SECTOR_SIZE) ? 2U : 1U; + + memset(certificate, 0, sizeof(certificate)); + if (read10(context, + certificate_lba, + sectors_to_read, + certificate, + sizeof(certificate)) < 0) + return 0; + + cert = certificate + inner_offset; + metadata->title_id = xbox_le32(cert + 0x008U); + metadata->allowed_media = xbox_le32(cert + 0x09CU); + metadata->game_region = xbox_le32(cert + 0x0A0U); + metadata->game_ratings = xbox_le32(cert + 0x0A4U); + metadata->disc_number = xbox_le32(cert + 0x0A8U); + metadata->version = xbox_le32(cert + 0x0ACU); + metadata->have_game_region = 1; + xbox_decode_title(cert, + XBOX_XBE_CERTIFICATE_MIN_SIZE, + metadata->title, + sizeof(metadata->title)); + xbox_region_format(metadata->game_region, + metadata->region, + sizeof(metadata->region)); + + return 1; +} + +void xbox_portable_metadata_init(xbox_portable_metadata *metadata) +{ + if (metadata) + memset(metadata, 0, sizeof(*metadata)); +} + +int xbox_portable_read_metadata(xbox_portable_read10_func read10, + xbox_portable_read_structure_func read_structure, + void *context, + xbox_portable_metadata *metadata) +{ + static const uint32_t volume_candidates[] = { + XBOX_XDFS_STANDARD_VOLUME_LBA, + XBOX_XDFS_LEGACY_VOLUME_LBA + }; + unsigned char volume[XBOX_SECTOR_SIZE]; + unsigned char dmi[XBOX_SECTOR_SIZE]; + uint32_t root_lba = 0; + uint32_t root_size = 0; + uint32_t xbe_lba = 0; + size_t candidate_index; + + if (!read10 || !metadata) + return 0; + + xbox_portable_metadata_init(metadata); + + for (candidate_index = 0; + candidate_index < sizeof(volume_candidates) / sizeof(volume_candidates[0]); + ++candidate_index) { + memset(volume, 0, sizeof(volume)); + if (read10(context, + volume_candidates[candidate_index], + 1, + volume, + sizeof(volume)) < 0) + continue; + if (memcmp(volume, XBOX_XDFS_MAGIC, XBOX_XDFS_MAGIC_SIZE) != 0) + continue; + + metadata->xdfs_volume_lba = volume_candidates[candidate_index]; + root_lba = xbox_le32(volume + 0x14U); + root_size = xbox_le32(volume + 0x18U); + break; + } + + if (root_lba == 0 || root_size == 0) + return 0; + if (!xbox_find_default_xbe(read10, + context, + root_lba, + root_size, + &xbe_lba)) + return 0; + if (!xbox_parse_xbe_certificate(read10, + context, + xbe_lba, + metadata)) + return 0; + + if (read_structure) { + memset(dmi, 0, sizeof(dmi)); + if (read_structure(context, + 0x04U, + 0x00U, + dmi, + sizeof(dmi)) >= 0) + xbox_parse_media_id(dmi, + sizeof(dmi), + metadata->media_id, + sizeof(metadata->media_id)); + } + + metadata->valid = 1; + return 1; +} + +#ifndef XBOX_PORTABLE_NO_DISC_WRAPPER +static int xbox_disc_read10(void *context, + uint32_t lba, + uint32_t sectors, + unsigned char *buffer, + size_t buffer_size) +{ + return disc_xbox_read_10((disc *)context, + lba, + sectors, + buffer, + buffer_size); +} + +static int xbox_disc_read_structure(void *context, + uint8_t format, + uint8_t layer, + unsigned char *buffer, + size_t buffer_size) +{ + return disc_xbox_read_dvd_structure((disc *)context, + format, + layer, + buffer, + buffer_size); +} + +int xbox_portable_read_metadata_from_disc(struct disc_s *disc_value, + xbox_portable_metadata *metadata) +{ + if (!disc_value) + return 0; + + return xbox_portable_read_metadata(xbox_disc_read10, + xbox_disc_read_structure, + disc_value, + metadata); +} +#endif diff --git a/libfriidump/xbox_portable.h b/libfriidump/xbox_portable.h new file mode 100644 index 0000000..f48d351 --- /dev/null +++ b/libfriidump/xbox_portable.h @@ -0,0 +1,56 @@ +#ifndef XBOX_PORTABLE_H_INCLUDED +#define XBOX_PORTABLE_H_INCLUDED + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define XBOX_PORTABLE_TITLE_MAX 256 +#define XBOX_PORTABLE_MEDIA_ID_MAX 64 +#define XBOX_PORTABLE_REGION_MAX 128 + +typedef int (*xbox_portable_read10_func)(void *context, + uint32_t lba, + uint32_t sectors, + unsigned char *buffer, + size_t buffer_size); + +typedef int (*xbox_portable_read_structure_func)(void *context, + uint8_t format, + uint8_t layer, + unsigned char *buffer, + size_t buffer_size); + +typedef struct xbox_portable_metadata_s { + int valid; + uint32_t xdfs_volume_lba; + uint32_t title_id; + uint32_t allowed_media; + uint32_t game_region; + uint32_t game_ratings; + uint32_t disc_number; + uint32_t version; + int have_game_region; + char title[XBOX_PORTABLE_TITLE_MAX]; + char media_id[XBOX_PORTABLE_MEDIA_ID_MAX]; + char region[XBOX_PORTABLE_REGION_MAX]; +} xbox_portable_metadata; + +struct disc_s; + +void xbox_portable_metadata_init(xbox_portable_metadata *metadata); +int xbox_portable_read_metadata(xbox_portable_read10_func read10, + xbox_portable_read_structure_func read_structure, + void *context, + xbox_portable_metadata *metadata); +int xbox_portable_read_metadata_from_disc(struct disc_s *disc, + xbox_portable_metadata *metadata); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/libfriidump/xbox_ref/utils.c b/libfriidump/xbox_ref/utils.c index c63b03b..f437450 100644 --- a/libfriidump/xbox_ref/utils.c +++ b/libfriidump/xbox_ref/utils.c @@ -33,6 +33,24 @@ #define GDR_8163B OL23 +/* The copied Xbox dumper is single-threaded. FriiDump installs one + * per-invocation cancellation callback before entering the copied path and + * clears it during cleanup. */ +static int (*g_xbox_dump_cancel)(void *cancel_data) = NULL; +static void *g_xbox_dump_cancel_data = NULL; +static xbox_ref_dump_result *g_xbox_dump_cancel_result = NULL; + +static BOOL XboxDumpCancelRequested(void) +{ + if (!g_xbox_dump_cancel || !g_xbox_dump_cancel(g_xbox_dump_cancel_data)) + return FALSE; + + if (g_xbox_dump_cancel_result) + g_xbox_dump_cancel_result->cancelled = 1; + + return TRUE; +} + HANDLE OpenDrive(char driveLetter) { char devicePath[16]; @@ -1232,23 +1250,6 @@ static uint32_t DetectXboxVolumeStart(HANDLE hDevice) return 0xFFFFFFFFu; } -static void RecoveryKick(HANDLE hDevice, BOOL authRecovery) -{ - unsigned char dummy[2048] = {0}; - - if (authRecovery) - KickXboxMediaAuth(hDevice); - - SetDriveSpeedMax(hDevice); - - for (int i = 0; i < 10; i++) - { - ScsiReadSectors(hDevice, 0, 1, dummy); - Sleep(50); - } -} - - static void GetDirectoryForPath(const char *filename, char *outDir, DWORD outDirSize) { DWORD len; @@ -1436,8 +1437,7 @@ static BOOL DumpSectorRangeWithRetry(HANDLE hDevice, uint32_t sourceStartLba, uint32_t sectorsToRead, uint32_t outputBaseLba, - const char *phaseName, - BOOL authRecovery) + const char *phaseName) { const uint32_t batchSize = 32; unsigned char *buffer = NULL; @@ -1459,11 +1459,19 @@ static BOOL DumpSectorRangeWithRetry(HANDLE hDevice, printf("\n--- STARTING %s RANGE ---\n", phaseName); printf("Source LBA: %u | Output LBA: %u | Sectors: %u\n", sourceStartLba, outputBaseLba, sectorsToRead); - RecoveryKick(hDevice, authRecovery); - while (sectorsDone < sectorsToRead) { - uint32_t currentLba = sourceStartLba + sectorsDone; + uint32_t currentLba; + + if (XboxDumpCancelRequested()) + { + printf("\n[CANCEL] User cancellation requested during %s at output LBA %u.\n", + phaseName, outputBaseLba + sectorsDone); + VirtualFree(buffer, 0, MEM_RELEASE); + return FALSE; + } + + currentLba = sourceStartLba + sectorsDone; const char *currentLayerStr = "L0"; uint32_t burstLimit = batchSize; uint32_t toRead; @@ -1492,6 +1500,14 @@ static BOOL DumpSectorRangeWithRetry(HANDLE hDevice, for (int retry = 0; retry <= MAX_RETRIES; retry++) { + if (XboxDumpCancelRequested()) + { + printf("\n[CANCEL] User cancellation requested during %s retry at output LBA %u.\n", + phaseName, outputBaseLba + sectorsDone); + VirtualFree(buffer, 0, MEM_RELEASE); + return FALSE; + } + if (ScsiReadSectors(hDevice, currentLba, (uint16_t)toRead, buffer)) { if (!WriteOutputBytes(outFile, buffer, (size_t)toRead * 2048U, phaseName, currentLba, outputBaseLba + sectorsDone)) @@ -1505,7 +1521,6 @@ static BOOL DumpSectorRangeWithRetry(HANDLE hDevice, break; } - RecoveryKick(hDevice, authRecovery); Sleep(500); } @@ -1528,6 +1543,15 @@ static BOOL DumpSectorRangeWithRetry(HANDLE hDevice, for (int sRetry = 0; sRetry <= MAX_RETRIES; sRetry++) { + if (XboxDumpCancelRequested()) + { + printf("\n[CANCEL] User cancellation requested during %s sector recovery at output LBA %u.\n", + phaseName, outputBaseLba + sectorsDone); + VirtualFree(smallBuffer, 0, MEM_RELEASE); + VirtualFree(buffer, 0, MEM_RELEASE); + return FALSE; + } + if (ScsiReadSectors(hDevice, currentLba + i, 1, smallBuffer)) { if (!WriteOutputBytes(outFile, smallBuffer, 2048, phaseName, currentLba + i, outputBaseLba + sectorsDone)) @@ -1542,7 +1566,6 @@ static BOOL DumpSectorRangeWithRetry(HANDLE hDevice, break; } - RecoveryKick(hDevice, authRecovery); Sleep(500); } @@ -1613,7 +1636,17 @@ static BOOL WriteZeroSectorsOutput(FILE *outFile, while (sectorsDone < sectorCount) { - uint32_t toWrite = (sectorCount - sectorsDone > batchSectors) ? batchSectors : (sectorCount - sectorsDone); + uint32_t toWrite; + + if (XboxDumpCancelRequested()) + { + printf("\n[CANCEL] User cancellation requested during %s at output LBA %u.\n", + phaseName ? phaseName : "ZERO", outputBaseLba + sectorsDone); + VirtualFree(zeroBuffer, 0, MEM_RELEASE); + return FALSE; + } + + toWrite = (sectorCount - sectorsDone > batchSectors) ? batchSectors : (sectorCount - sectorsDone); uint32_t outputLba = outputBaseLba + sectorsDone; if (!WriteOutputBytes(outFile, zeroBuffer, (size_t)toWrite * 2048U, phaseName ? phaseName : "ZERO", 0, outputLba)) @@ -2746,7 +2779,12 @@ static BOOL WriteXboxDvdSidecarFiles(const char *isoFilename, return ok; } -BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uint32_t totalDiscSectors, bool isDualLayer, bool EjectOnSuccess, xbox_ref_dump_result *result) +BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, + uint32_t totalDiscSectors, bool isDualLayer, + bool EjectOnSuccess, + int (*cancel)(void *cancel_data), + void *cancel_data, + xbox_ref_dump_result *result) { HCRYPTPROV hProv = 0; HCRYPTHASH hHash = 0; @@ -2772,6 +2810,10 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin memset(&sidecarCapture, 0, sizeof(sidecarCapture)); + g_xbox_dump_cancel = cancel; + g_xbox_dump_cancel_data = cancel_data; + g_xbox_dump_cancel_result = result; + if (result) { result->attempted = 1; result->mode = xisoFormat; @@ -2786,15 +2828,18 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin if (totalDiscSectors == 0) totalDiscSectors = REDUMP_SECTORS; + if (XboxDumpCancelRequested()) + { + printf("\n[CANCEL] User cancellation requested before Xbox output preparation.\n"); + goto cleanup; + } + rawTargetSectors = NormalizeRawIsoTargetSectors(totalDiscSectors, isDualLayer); if (!CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) - return FALSE; + goto cleanup; if (!CryptCreateHash(hProv, CALG_SHA1, 0, 0, &hHash)) - { - CryptReleaseContext(hProv, 0); - return FALSE; - } + goto cleanup; EnsureDriveReady(hDevice, 30000); SetDriveSpeedMax(hDevice); @@ -2873,7 +2918,7 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin rawGameSectors = 0; CaptureUnlockedSidecarState(hDevice, &sidecarCapture); rawSidecarAvailable = TRUE; - dumpOk = DumpSectorRangeWithRetry(hDevice, outFile, hHash, 0, rawTargetSectors, 0, "RAW", FALSE); + dumpOk = DumpSectorRangeWithRetry(hDevice, outFile, hHash, 0, rawTargetSectors, 0, "RAW"); } else if (rawTargetSectors == XGD1_FULL_REDUMP_SECTORS) { @@ -2914,7 +2959,7 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin } printf("[RAW] Writing video L0 from locked source LBA 0..%u.\n", XGD1_VIDEO_L0_SECTORS - 1); - if (!DumpSectorRangeWithRetry(hDevice, outFile, hHash, 0, XGD1_VIDEO_L0_SECTORS, 0, "VIDEO-L0", FALSE)) + if (!DumpSectorRangeWithRetry(hDevice, outFile, hHash, 0, XGD1_VIDEO_L0_SECTORS, 0, "VIDEO-L0")) { VirtualFree(videoL1Buffer, 0, MEM_RELEASE); goto cleanup; @@ -2932,8 +2977,6 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin Sleep(2000); EnsureDriveReady(hDevice, 30000); SetDriveSpeedMax(hDevice); - KickXboxMediaAuth(hDevice); - RecoveryKick(hDevice, TRUE); CaptureUnlockedSidecarState(hDevice, &sidecarCapture); rawSidecarAvailable = TRUE; @@ -2969,8 +3012,7 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin 0, XGD1_XISO_LEADIN_SECTORS, XGD1_GAME_OUTPUT_START_LBA, - "GAME-XISO-LEADIN", - TRUE)) + "GAME-XISO-LEADIN")) { VirtualFree(videoL1Buffer, 0, MEM_RELEASE); goto cleanup; @@ -2986,8 +3028,7 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin XGD1_GAME_SOURCE_START_LBA, XGD1_GAME_SOURCE_SECTORS, XGD1_GAME_OUTPUT_START_LBA + XGD1_XISO_LEADIN_SECTORS, - "GAME-XISO", - TRUE)) + "GAME-XISO")) { VirtualFree(videoL1Buffer, 0, MEM_RELEASE); goto cleanup; @@ -3015,7 +3056,7 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin rawVideoSectors = videoSectors; printf("[RAW] Dumping contiguous visible/video area first: source LBA 0..%u.\n", videoSectors - 1); - if (!DumpSectorRangeWithRetry(hDevice, outFile, hHash, 0, videoSectors, 0, "VIDEO", FALSE)) + if (!DumpSectorRangeWithRetry(hDevice, outFile, hHash, 0, videoSectors, 0, "VIDEO")) goto cleanup; printf("[RAW] Re-applying full Xbox handshake after media transition for hidden game/data area.\n"); @@ -3024,8 +3065,6 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin Sleep(2000); EnsureDriveReady(hDevice, 30000); SetDriveSpeedMax(hDevice); - KickXboxMediaAuth(hDevice); - RecoveryKick(hDevice, TRUE); CaptureUnlockedSidecarState(hDevice, &sidecarCapture); rawSidecarAvailable = TRUE; @@ -3041,7 +3080,7 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin rawGameSectors = gameSectors; printf("[RAW] Appending hidden game/data area from unlocked source LBA %u for %u sectors.\n", gameSourceLba, gameSectors); - dumpOk = DumpSectorRangeWithRetry(hDevice, outFile, hHash, gameSourceLba, gameSectors, videoSectors, "GAME", TRUE); + dumpOk = DumpSectorRangeWithRetry(hDevice, outFile, hHash, gameSourceLba, gameSectors, videoSectors, "GAME"); } } else if (xisoFormat == '2') @@ -3057,8 +3096,6 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin goto cleanup; SetDriveSpeedMax(hDevice); - KickXboxMediaAuth(hDevice); - RecoveryKick(hDevice, TRUE); vol = (XDFS_VOLUME_DESCRIPTOR *)sectorBuffer; @@ -3120,12 +3157,18 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin printf("Writing 64KB XISO lead-in padding...\n"); for (int p = 0; p < 32; p++) { + if (XboxDumpCancelRequested()) + { + printf("\n[CANCEL] User cancellation requested during XISO lead-in padding at output LBA %d.\n", p); + goto cleanup; + } + if (!WriteOutputBytes(outFile, zeroSector, 2048, "XISO-PAD", 0, (uint32_t)p)) goto cleanup; CryptHashData(hHash, zeroSector, 2048, 0); } - dumpOk = DumpSectorRangeWithRetry(hDevice, outFile, hHash, startLba, sectorsToRead, 32, "XISO", TRUE); + dumpOk = DumpSectorRangeWithRetry(hDevice, outFile, hHash, startLba, sectorsToRead, 32, "XISO"); } else { @@ -3246,7 +3289,22 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin cleanup: if (outFile) - fclose(outFile); + { + if (result && result->cancelled) + { + if (!FlushAndCommitOutput(outFile, outputLabel)) + printf("\n[WARN] Could not fully flush the controlled partial %s output before hashing.\n", + outputLabel ? outputLabel : "Xbox"); + } + if (fclose(outFile) != 0) + { + printf("\n[WARN] fclose failed for partial %s output.\n", + outputLabel ? outputLabel : "Xbox"); + if (errno) + printf(" errno: %d (%s)\n", errno, strerror(errno)); + } + outFile = NULL; + } if (hHash) CryptDestroyHash(hHash); if (hProv) @@ -3254,8 +3312,47 @@ cleanup: if (result) { result->dump_success = dumpOk ? 1 : 0; result->elapsed_seconds = (double)(GetTickCount() - operationStartTick) / 1000.0; - if (dumpOk && result->output_size == 0) + if (result->output_size == 0 && filename && filename[0]) result->output_size = GetFileSizeBytes64(filename); + result->completed_sectors = (uint32_t)(result->output_size / 2048ULL); + + if (result->cancelled && + result->output_size > 0 && + !result->hashes_complete) + { + printf("\n[HASH] Finalizing controlled partial Xbox output hashes...\n"); + if (CalculateFileHashes(filename, + crc32String, sizeof(crc32String), + md5String, sizeof(md5String), + fileSha1String, sizeof(fileSha1String), + fileSha256String, sizeof(fileSha256String))) + { + strncpy(result->crc32, crc32String, sizeof(result->crc32) - 1); + result->crc32[sizeof(result->crc32) - 1] = '\0'; + strncpy(result->md5, md5String, sizeof(result->md5) - 1); + result->md5[sizeof(result->md5) - 1] = '\0'; + strncpy(result->sha1, fileSha1String, sizeof(result->sha1) - 1); + result->sha1[sizeof(result->sha1) - 1] = '\0'; + strncpy(result->sha256, fileSha256String, sizeof(result->sha256) - 1); + result->sha256[sizeof(result->sha256) - 1] = '\0'; + result->hashes_complete = + result->crc32[0] && + result->md5[0] && + result->sha1[0] && + result->sha256[0]; + if (result->hashes_complete) + printf("[OK] Controlled partial Xbox output hashes captured.\n"); + } + else + { + printf("[WARN] Controlled partial Xbox output hashing failed.\n"); + } + } } + + g_xbox_dump_cancel = NULL; + g_xbox_dump_cancel_data = NULL; + g_xbox_dump_cancel_result = NULL; + return dumpOk; } diff --git a/libfriidump/xbox_ref/utils.h b/libfriidump/xbox_ref/utils.h index f0f07fb..0c6d0da 100644 --- a/libfriidump/xbox_ref/utils.h +++ b/libfriidump/xbox_ref/utils.h @@ -139,6 +139,11 @@ void PrintGamePartitionHash(const char* filename); void GetDiscMetadata(HANDLE hDevice, uint32_t* totalSectors, bool* isDualLayer, XboxGameInfo *gameInfo); uint32_t GetGamePartitionSize(HANDLE hDevice, uint32_t totalDiscSectors, XDFS_VOLUME_DESCRIPTOR *vol); struct xbox_ref_dump_result_s; -BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uint32_t totalDiscSectors, bool isDualLayer, bool EjectOnSuccess, struct xbox_ref_dump_result_s *result); +BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, + uint32_t totalDiscSectors, bool isDualLayer, + bool EjectOnSuccess, + int (*cancel)(void *cancel_data), + void *cancel_data, + struct xbox_ref_dump_result_s *result); #endif diff --git a/libfriidump/xbox_ref/xbe_cert.h b/libfriidump/xbox_ref/xbe_cert.h index b326462..acad29a 100644 --- a/libfriidump/xbox_ref/xbe_cert.h +++ b/libfriidump/xbox_ref/xbe_cert.h @@ -1,44 +1,38 @@ -#ifndef XBE_CERT_H -#define XBE_CERT_H - -#include - -// --- XBE Certificate Bit Flags --- - -typedef enum { - XB_REGION_US_CANADA = 0x00000001, - XB_REGION_JAPAN = 0x00000002, - XB_REGION_EUROPE_AU_NZ = 0x00000004, - XB_REGION_REST_OF_WORLD = 0x00000008, - XB_REGION_MANUFACTURING = 0x80000000U // 'U' suffix forces Unsigned -} XboxRegion; - -typedef enum { - XB_MEDIA_HARD_DRIVE = 0x00000001, - XB_MEDIA_DVD_X2 = 0x00000002, - XB_MEDIA_DVD_CD = 0x00000004, - XB_MEDIA_CD = 0x00000008, - XB_MEDIA_DVD_5_RO = 0x00000010, - XB_MEDIA_DVD_9_RO = 0x00000020, - XB_MEDIA_DVD_5_RW = 0x00000040, - XB_MEDIA_DVD_9_RW = 0x00000080, - XB_MEDIA_DONGLE = 0x00000100, - XB_MEDIA_NON_SECURE_HARD_DRIVE = 0x40000000, - XB_MEDIA_NON_SECURE_USER_MASK = 0x80000000U -} XboxMedia; - -// --- The Data Struct --- - -typedef struct { - uint32_t TitleId; // Offset 0x008 - char TitleName[41]; // Offset 0x00C (Converted to ASCII) - uint32_t AllowedMedia; // Offset 0x09C - uint32_t GameRegion; // Offset 0x0A0 - uint32_t GameRatings; // Offset 0x0A4 - uint32_t DiscNumber; // Offset 0x0A8 - uint32_t Version; // Offset 0x0AC - uint64_t TotalSizeBytes; // Offset 0x01C - int Success; // Helper to check if read worked -} XboxGameInfo; - +#ifndef XBE_CERT_H +#define XBE_CERT_H + +#include +#include "../xbox_region.h" + +// --- XBE Certificate Bit Flags --- + + +typedef enum { + XB_MEDIA_HARD_DRIVE = 0x00000001, + XB_MEDIA_DVD_X2 = 0x00000002, + XB_MEDIA_DVD_CD = 0x00000004, + XB_MEDIA_CD = 0x00000008, + XB_MEDIA_DVD_5_RO = 0x00000010, + XB_MEDIA_DVD_9_RO = 0x00000020, + XB_MEDIA_DVD_5_RW = 0x00000040, + XB_MEDIA_DVD_9_RW = 0x00000080, + XB_MEDIA_DONGLE = 0x00000100, + XB_MEDIA_NON_SECURE_HARD_DRIVE = 0x40000000, + XB_MEDIA_NON_SECURE_USER_MASK = 0x80000000U +} XboxMedia; + +// --- The Data Struct --- + +typedef struct { + uint32_t TitleId; // Offset 0x008 + char TitleName[41]; // Offset 0x00C (Converted to ASCII) + uint32_t AllowedMedia; // Offset 0x09C + uint32_t GameRegion; // Offset 0x0A0 + uint32_t GameRatings; // Offset 0x0A4 + uint32_t DiscNumber; // Offset 0x0A8 + uint32_t Version; // Offset 0x0AC + uint64_t TotalSizeBytes; // Offset 0x01C + int Success; // Helper to check if read worked +} XboxGameInfo; + #endif \ No newline at end of file diff --git a/libfriidump/xbox_ref_bridge.c b/libfriidump/xbox_ref_bridge.c index 33e987c..25e317a 100644 --- a/libfriidump/xbox_ref_bridge.c +++ b/libfriidump/xbox_ref_bridge.c @@ -1,4 +1,5 @@ #include "xbox_ref_bridge.h" +#include "xbox_region.h" #include @@ -797,7 +798,12 @@ static int xbox_ref_xgd1_layout_probe_core(HANDLE hDevice, } -static int xbox_ref_gdr8050l_dump_core(HANDLE hDevice, const char *device, const char *filename, char mode, BOOL ownsHandle, xbox_ref_dump_result *result) +static int xbox_ref_gdr8050l_dump_core(HANDLE hDevice, const char *device, + const char *filename, char mode, + BOOL ownsHandle, + xbox_ref_cancel_func cancel, + void *cancel_data, + xbox_ref_dump_result *result) { char driveLetter; XboxGameInfo game; @@ -918,6 +924,13 @@ static int xbox_ref_gdr8050l_dump_core(HANDLE hDevice, const char *device, const strncpy(result->media_id, mediaId, sizeof(result->media_id) - 1); result->media_id[sizeof(result->media_id) - 1] = '\0'; } + if (game.Success) { + result->have_game_region = 1; + result->game_region = game.GameRegion; + xbox_region_format(game.GameRegion, + result->region, + sizeof(result->region)); + } } if (game.Success) { @@ -972,7 +985,7 @@ static int xbox_ref_gdr8050l_dump_core(HANDLE hDevice, const char *device, const } printf("Starting dump with copied DumpXboxGameDisc() mode %c...\n", mode); - ok = DumpXboxGameDisc(hDevice, finalFilename, mode, totalSectors, isDualLayerBool, FALSE, result); + ok = DumpXboxGameDisc(hDevice, finalFilename, mode, totalSectors, isDualLayerBool, FALSE, cancel, cancel_data, result); cleanup: if (volumeLocked) @@ -990,7 +1003,7 @@ cleanup: } else if (ok) { printf("[XBOX-REF] Returning finalized output evidence to FriiDump's shared Redump verifier.\n"); } else { - printf("[XBOX-REF] No completed output evidence is available for shared Redump verification.\n"); + printf("[XBOX-REF] No completed full-output evidence is available for shared Redump verification.\n"); } return ok ? 0 : 1; } @@ -1037,9 +1050,14 @@ int xbox_ref_xgd1_layout_probe(const char *device, TRUE); } -int xbox_ref_gdr8050l_dump_with_handle(void *native_handle, const char *device, const char *filename, char mode, xbox_ref_dump_result *result) +int xbox_ref_gdr8050l_dump_with_handle(void *native_handle, const char *device, + const char *filename, char mode, + xbox_ref_cancel_func cancel, + void *cancel_data, + xbox_ref_dump_result *result) { - return xbox_ref_gdr8050l_dump_core((HANDLE)native_handle, device, filename, mode, FALSE, result); + return xbox_ref_gdr8050l_dump_core((HANDLE)native_handle, device, filename, + mode, FALSE, cancel, cancel_data, result); } int xbox_ref_gdr8050l_dump(const char *device, const char *filename, char mode, xbox_ref_dump_result *result) @@ -1063,7 +1081,7 @@ int xbox_ref_gdr8050l_dump(const char *device, const char *filename, char mode, return 1; } - return xbox_ref_gdr8050l_dump_core(hDevice, device, filename, mode, TRUE, result); + return xbox_ref_gdr8050l_dump_core(hDevice, device, filename, mode, TRUE, NULL, NULL, result); } #else int xbox_ref_xgd1_layout_probe_with_handle(void *native_handle, @@ -1084,9 +1102,16 @@ int xbox_ref_xgd1_layout_probe(const char *device, return 1; } -int xbox_ref_gdr8050l_dump_with_handle(void *native_handle, const char *device, const char *filename, char mode, xbox_ref_dump_result *result) +int xbox_ref_gdr8050l_dump_with_handle(void *native_handle, const char *device, + const char *filename, char mode, + xbox_ref_cancel_func cancel, + void *cancel_data, + xbox_ref_dump_result *result) { - (void)native_handle; (void)device; (void)filename; (void)mode; xbox_ref_dump_result_init(result); return 1; + (void)native_handle; (void)device; (void)filename; (void)mode; + (void)cancel; (void)cancel_data; + xbox_ref_dump_result_init(result); + return 1; } int xbox_ref_gdr8050l_dump(const char *device, const char *filename, char mode, xbox_ref_dump_result *result) diff --git a/libfriidump/xbox_ref_bridge.h b/libfriidump/xbox_ref_bridge.h index 6593856..59d191f 100644 --- a/libfriidump/xbox_ref_bridge.h +++ b/libfriidump/xbox_ref_bridge.h @@ -10,18 +10,26 @@ extern "C" { #define XBOX_REF_RESULT_PATH_MAX 4096 #define XBOX_REF_RESULT_TITLE_MAX 256 #define XBOX_REF_RESULT_MEDIA_ID_MAX 64 +#define XBOX_REF_RESULT_REGION_MAX 128 + +typedef int (*xbox_ref_cancel_func)(void *cancel_data); typedef struct xbox_ref_dump_result_s { int attempted; int dump_success; + int cancelled; int hashes_complete; char mode; uint32_t output_sectors; + uint32_t completed_sectors; uint64_t output_size; double elapsed_seconds; char output_path[XBOX_REF_RESULT_PATH_MAX]; char title[XBOX_REF_RESULT_TITLE_MAX]; char media_id[XBOX_REF_RESULT_MEDIA_ID_MAX]; + int have_game_region; + uint32_t game_region; + char region[XBOX_REF_RESULT_REGION_MAX]; char crc32[9]; char md5[33]; char sha1[41]; @@ -33,6 +41,8 @@ int xbox_ref_gdr8050l_dump_with_handle(void *native_handle, const char *device, const char *filename, char mode, + xbox_ref_cancel_func cancel, + void *cancel_data, xbox_ref_dump_result *result); int xbox_ref_gdr8050l_dump(const char *device, const char *filename, diff --git a/libfriidump/xbox_region.c b/libfriidump/xbox_region.c new file mode 100644 index 0000000..846a55c --- /dev/null +++ b/libfriidump/xbox_region.c @@ -0,0 +1,78 @@ +#include "xbox_region.h" + +#include +#include + +#define XBOX_REGION_KNOWN_MASK \ + (XB_REGION_US_CANADA | XB_REGION_JAPAN | \ + XB_REGION_EUROPE_AU_NZ | XB_REGION_REST_OF_WORLD | \ + XB_REGION_MANUFACTURING) + +static void xbox_region_append(char *output, + size_t output_size, + const char *text, + int *have_text) +{ + size_t used; + + if (!output || output_size == 0 || !text || !have_text) + return; + + used = strlen(output); + if (used >= output_size - 1) + return; + + if (*have_text) { + snprintf(output + used, output_size - used, "; "); + used = strlen(output); + if (used >= output_size - 1) + return; + } + + snprintf(output + used, output_size - used, "%s", text); + *have_text = 1; +} + +int xbox_region_format(uint32_t mask, char *output, size_t output_size) +{ + uint32_t execution_mask; + uint32_t unknown_mask; + int have_text; + char unknown[32]; + + if (!output || output_size == 0) + return 0; + + output[0] = '\0'; + have_text = 0; + execution_mask = mask & 0x7fffffffU; + + if (execution_mask == 0x7fffffffU) { + xbox_region_append(output, output_size, "Region Free", &have_text); + } else if (execution_mask == 0U) { + xbox_region_append(output, output_size, "None (Locked)", &have_text); + } else { + if (mask & XB_REGION_US_CANADA) + xbox_region_append(output, output_size, "North America", &have_text); + if (mask & XB_REGION_JAPAN) + xbox_region_append(output, output_size, "Japan", &have_text); + if (mask & XB_REGION_EUROPE_AU_NZ) + xbox_region_append(output, output_size, + "Europe/Australia/New Zealand", &have_text); + if (mask & XB_REGION_REST_OF_WORLD) + xbox_region_append(output, output_size, + "Rest of World", &have_text); + } + + if (mask & XB_REGION_MANUFACTURING) + xbox_region_append(output, output_size, "Manufacturing", &have_text); + + unknown_mask = mask & ~((uint32_t)XBOX_REGION_KNOWN_MASK); + if (execution_mask != 0x7fffffffU && unknown_mask != 0U) { + snprintf(unknown, sizeof(unknown), "Unknown flags 0x%08x", + unknown_mask); + xbox_region_append(output, output_size, unknown, &have_text); + } + + return 1; +} diff --git a/libfriidump/xbox_region.h b/libfriidump/xbox_region.h new file mode 100644 index 0000000..c40b7c8 --- /dev/null +++ b/libfriidump/xbox_region.h @@ -0,0 +1,20 @@ +#ifndef XBOX_REGION_H_INCLUDED +#define XBOX_REGION_H_INCLUDED + +#include +#include + +typedef enum { + XB_REGION_US_CANADA = 0x00000001U, + XB_REGION_JAPAN = 0x00000002U, + XB_REGION_EUROPE_AU_NZ = 0x00000004U, + XB_REGION_REST_OF_WORLD = 0x00000008U, + XB_REGION_MANUFACTURING = 0x80000000U +} XboxRegion; + +/* Formats an Original Xbox XBE certificate GameRegion bit mask. + * The result describes the XBE execution region, not a television standard. + * Returns false only when the output buffer is unusable. */ +int xbox_region_format(uint32_t mask, char *output, size_t output_size); + +#endif diff --git a/msvc32_friidump.rsp b/msvc32_friidump.rsp index ad9c2e3..f23dedc 100644 --- a/msvc32_friidump.rsp +++ b/msvc32_friidump.rsp @@ -2,6 +2,7 @@ /O2 /MD /TC +/we4020 /DWIN32 /DHAVE_CONFIG_H /D_CRT_SECURE_NO_DEPRECATE @@ -29,12 +30,15 @@ libfriidump\ecma-267.c libfriidump\hitachi.c libfriidump\lite-on.c libfriidump\misc.c +libfriidump\nintendo_disc_header.c libfriidump\renesas.c libfriidump\rs.c libfriidump\unscrambler.c libfriidump\vanilla_2064.c libfriidump\vanilla_2384.c libfriidump\win32compat.c +libfriidump\xbox_region.c +libfriidump\xbox_portable.c libfriidump/xbox_ref_bridge.c libfriidump/xbox_xgd1_raw_id_probe.c libfriidump/xbox_ref/utils.c @@ -45,4 +49,5 @@ libfriidump/xbox_ref/xbox_ref_log.c src\getopt-win32.c src\getopt_long-win32.c src\friidump.c +src\native_report.c src\redump_dat.c diff --git a/msvc32_native_report_tests.rsp b/msvc32_native_report_tests.rsp new file mode 100644 index 0000000..7dba7fa --- /dev/null +++ b/msvc32_native_report_tests.rsp @@ -0,0 +1,17 @@ +/nologo +/O2 +/MD +/TC +/DWIN32 +/D_CRT_SECURE_NO_DEPRECATE +/D_CRT_NONSTDC_NO_DEPRECATE +/D_CRT_SECURE_NO_WARNINGS +/I. +/Isrc +/Ilibfriidump +/Fobuild-msvc32\ +/Fefriidump-report-fixtures.exe +tests\test_native_report.c +libfriidump\nintendo_disc_header.c +libfriidump\xbox_region.c +src\native_report.c diff --git a/msvc32_xbox_portable_tests.rsp b/msvc32_xbox_portable_tests.rsp new file mode 100644 index 0000000..4c35117 --- /dev/null +++ b/msvc32_xbox_portable_tests.rsp @@ -0,0 +1,15 @@ +/nologo +/O2 +/MD +/TC +/DWIN32 +/DXBOX_PORTABLE_NO_DISC_WRAPPER +/D_CRT_SECURE_NO_DEPRECATE +/D_CRT_NONSTDC_NO_DEPRECATE +/D_CRT_SECURE_NO_WARNINGS +/Ilibfriidump +/Fobuild-msvc32\ +/Fefriidump-xbox-portable-metadata-test.exe +tests\test_xbox_portable_metadata.c +libfriidump\xbox_portable.c +libfriidump\xbox_region.c diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6a13fd2..bbb9c4d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,15 +20,37 @@ if (WIN32) ) endif (WIN32) +# Keep the stable Redump DAT resources with the CMake-built executable. +# This makes source-tree builds independent of the process working directory. +file (MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/redump_dat") +foreach (FRIIDUMP_REDUMP_DAT_FILE + "Microsoft - Xbox.dat" + "Nintendo - GameCube.dat" + "Nintendo - Wii.dat" + "README.txt" +) + configure_file ( + "${FriiDump_SOURCE_DIR}/redump_dat/${FRIIDUMP_REDUMP_DAT_FILE}" + "${CMAKE_CURRENT_BINARY_DIR}/redump_dat/${FRIIDUMP_REDUMP_DAT_FILE}" + COPYONLY + ) +endforeach () + add_executable ( friidump friidump.c + native_report.c + native_report.h redump_dat.c redump_dat.h ${additional_sources} ) +if (UNIX AND NOT WIN32) + target_sources (friidump PRIVATE linux_rawio.c linux_rawio.h) +endif () + # Link the executable to the Hello library. target_link_libraries ( friidump @@ -36,6 +58,17 @@ target_link_libraries ( friidumplib ) +if (FRIIDUMP_BUILD_COMMIT) + target_compile_definitions ( + friidump + PRIVATE FRIIDUMP_BUILD_COMMIT="${FRIIDUMP_BUILD_COMMIT}" + ) +endif (FRIIDUMP_BUILD_COMMIT) + +if (WIN32) + target_link_libraries (friidump ole32) +endif (WIN32) + if (WIN32) # We only want a version number on Windows (Is it any use?) set_target_properties (friidump PROPERTIES VERSION 0.2) @@ -44,9 +77,66 @@ if (WIN32) TARGETS friidump RUNTIME DESTINATION / ) + install( + DIRECTORY "${FriiDump_SOURCE_DIR}/redump_dat/" + DESTINATION redump_dat + ) else (WIN32) install( TARGETS friidump RUNTIME DESTINATION bin ) -endif (WIN32) \ No newline at end of file + install( + DIRECTORY "${FriiDump_SOURCE_DIR}/redump_dat/" + DESTINATION share/friidump/redump_dat + ) +endif (WIN32) +if (BUILD_NATIVE_REPORT_TESTS) + if (UNIX AND NOT WIN32) + add_executable ( + friidump-linux-rawio-test + ${FriiDump_SOURCE_DIR}/tests/test_linux_rawio.c + linux_rawio.c + linux_rawio.h + ) + target_include_directories (friidump-linux-rawio-test PRIVATE ${FriiDump_SOURCE_DIR}/src) + endif () + + add_executable ( + friidump-report-fixtures + ${FriiDump_SOURCE_DIR}/tests/test_native_report.c + ${FriiDump_SOURCE_DIR}/libfriidump/nintendo_disc_header.c + ${FriiDump_SOURCE_DIR}/libfriidump/nintendo_disc_header.h + ${FriiDump_SOURCE_DIR}/libfriidump/xbox_region.c + ${FriiDump_SOURCE_DIR}/libfriidump/xbox_region.h + native_report.c + native_report.h + ) + target_include_directories (friidump-report-fixtures PRIVATE ${FriiDump_SOURCE_DIR}/src ${FriiDump_SOURCE_DIR}/libfriidump) + if (WIN32) + target_link_libraries (friidump-report-fixtures ole32) + endif (WIN32) + + add_executable ( + friidump-redump-dat-resolver-test + ${FriiDump_SOURCE_DIR}/tests/test_redump_dat_resolver.c + redump_dat.c + redump_dat.h + ) + target_include_directories (friidump-redump-dat-resolver-test PRIVATE ${FriiDump_SOURCE_DIR}/src) + + add_executable ( + friidump-redump-dat-verify-test + ${FriiDump_SOURCE_DIR}/tests/test_redump_dat_verify.c + redump_dat.c + redump_dat.h + ) + target_include_directories (friidump-redump-dat-verify-test PRIVATE ${FriiDump_SOURCE_DIR}/src) + + add_executable ( + friidump-xbox-portable-metadata-test + ${FriiDump_SOURCE_DIR}/tests/test_xbox_portable_metadata.c + ) + target_include_directories (friidump-xbox-portable-metadata-test PRIVATE ${FriiDump_SOURCE_DIR}/libfriidump) + target_link_libraries (friidump-xbox-portable-metadata-test friidumplib) +endif (BUILD_NATIVE_REPORT_TESTS) diff --git a/src/friidump.c b/src/friidump.c index 292f658..2a39979 100644 --- a/src/friidump.c +++ b/src/friidump.c @@ -21,6 +21,7 @@ #include "misc.h" #include +#include #include #include #include @@ -30,6 +31,11 @@ #include "xbox_ref/xbox_ref_log.h" #include "xbox_ref_bridge.h" #include "redump_dat.h" +#include "native_report.h" + +#ifndef WIN32 +#include "linux_rawio.h" +#endif #ifdef WIN32 #include @@ -62,10 +68,15 @@ typedef struct { char disc_type[64]; char game_id[64]; char title[256]; + char region[128]; char output[512]; u_int32_t command; int method; u_int32_t sectors; + u_int32_t source_sectors; + u_int32_t expected_output_sectors; + bool have_source_sectors; + bool have_expected_output_sectors; u_int32_t fail_sector; u_int32_t hlds_type; u_int32_t cache_base; @@ -79,6 +90,10 @@ static bool g_redump_attempted = false; static bool g_operation_duration_override_valid = false; static double g_operation_duration_override = 0.0; static char g_executable_dir[1024]; +static friidump_native_report g_native_report; +static bool g_native_report_dump_started = false; +static bool g_native_report_resumed = false; +static volatile sig_atomic_t g_native_cancel_requested = 0; static void friidump_summary_copy(char *dst, size_t dst_size, const char *src) { if (!dst || dst_size == 0) @@ -110,6 +125,235 @@ static void friidump_format_hms(double seconds, char *buf, size_t buf_size) { } +static bool friidump_capture_seed_diagnostic (disc *d) { + disc_seed_diagnostic diag; + char cdb_text[3 * 12 + 1]; + char note[768]; + int i; + int used; + + memset (&diag, 0, sizeof (diag)); + if (!disc_get_seed_diagnostic (d, &diag)) + return false; + + cdb_text[0] = '\0'; + used = 0; + for (i = 0; i < diag.cdb_length && i < 12; i++) { + int wrote = snprintf (cdb_text + used, sizeof (cdb_text) - (size_t) used, + "%s%02X", i ? " " : "", diag.cdb[i]); + if (wrote < 0 || wrote >= (int) (sizeof (cdb_text) - (size_t) used)) + break; + used += wrote; + } + + fprintf (stderr, + "\nSeed diagnostic final failure:\n" + " Block.............: %u/20\n" + " Sectors...........: %u..%u\n" + " Retry.............: %d\n" + " Stage.............: %s\n" + " Detail............: %s\n" + " Transport result..: %d\n" + " OS error..........: %d\n" + " SCSI status.......: 0x%02X\n" + " Sense.............: %02X/%02X/%02X\n" + " CDB...............: %s\n", + diag.seed_block_index + 1, + diag.sector_start, diag.sector_end, + diag.retry, + diag.stage[0] ? diag.stage : "unknown", + diag.detail[0] ? diag.detail : "none", + diag.transport_result, + diag.os_error, + diag.scsi_status & 0xff, + diag.sense_key & 0xff, + diag.asc & 0xff, + diag.ascq & 0xff, + cdb_text[0] ? cdb_text : "none"); + + snprintf (note, sizeof (note), + "Seed diagnostic: block %u/20, sectors %u..%u, retry %d, stage %s, detail %s, transport_result %d, os_error %d, scsi_status 0x%02X, sense %02X/%02X/%02X, CDB [%s].", + diag.seed_block_index + 1, + diag.sector_start, diag.sector_end, + diag.retry, + diag.stage[0] ? diag.stage : "unknown", + diag.detail[0] ? diag.detail : "none", + diag.transport_result, + diag.os_error, + diag.scsi_status & 0xff, + diag.sense_key & 0xff, + diag.asc & 0xff, + diag.ascq & 0xff, + cdb_text[0] ? cdb_text : "none"); + if (friidump_native_report_is_enabled (&g_native_report)) + friidump_native_report_add_note (&g_native_report, note); + return true; +} + +#ifndef WIN32 +static bool friidump_linux_vendor_rawio_required( + disc *d, + bool xbox_output_requested, + bool drive_supported, + bool probe_requested) { + int method; + + if (!d) + return false; + + if (probe_requested) + return true; + + if (xbox_output_requested) + return disc_is_xbox_unlock_drive(d); + + method = (int) disc_get_method(d); + return drive_supported && method >= 0 && method <= 9; +} + +static void friidump_linux_record_rawio_failure( + disc *d, + bool xbox_output_requested, + const char *note) { + bool stop_ok; + + if (friidump_native_report_is_enabled(&g_native_report)) { + friidump_native_report_set_seed( + &g_native_report, + false, + xbox_output_requested ? "not_applicable" : "not_attempted", + false, + 0.0); + friidump_native_report_set_dump( + &g_native_report, + false, + "not_attempted", + false, + 0, + false, + 0, + false, + 0.0, + NULL, + false, + 0, + NULL); + friidump_native_report_set_outcome(&g_native_report, "other", "fail"); + friidump_native_report_add_note(&g_native_report, note); + } + + stop_ok = disc_stop_unit(d, false); + fprintf(stderr, + "Issuing STOP UNIT / spin-down after Linux raw-I/O preflight failure... %s\n", + stop_ok ? "OK" : "Failed"); + + if (g_validation_summary.active) { + g_validation_summary.stop_attempted = true; + g_validation_summary.stop_ok = stop_ok; + } + + if (friidump_native_report_is_enabled(&g_native_report)) { + friidump_native_report_add_note( + &g_native_report, + stop_ok + ? "STOP UNIT after Linux raw-I/O preflight failure succeeded." + : "STOP UNIT after Linux raw-I/O preflight failure failed."); + } +} + +static bool friidump_linux_rawio_preflight( + disc *d, + bool xbox_output_requested, + bool drive_supported, + bool probe_requested) { + friidump_linux_rawio_state state; + unsigned long long effective_mask = 0; + char executable_path[1024]; + char note[1536]; + const char *display_path; + + if (!friidump_linux_vendor_rawio_required( + d, xbox_output_requested, drive_supported, probe_requested)) + return true; + + executable_path[0] = '\0'; + if (!friidump_linux_executable_path( + executable_path, sizeof(executable_path))) { + const char *fallback_dir = g_executable_dir[0] ? g_executable_dir : "."; + const char *suffix = "friidump"; + size_t used = strlen(fallback_dir); + size_t suffix_len = strlen(suffix); + + if (used >= sizeof(executable_path)) + used = sizeof(executable_path) - 1; + memcpy(executable_path, fallback_dir, used); + executable_path[used] = '\0'; + + if (used > 0 && executable_path[used - 1] != '/' && + used + 1 < sizeof(executable_path)) { + executable_path[used++] = '/'; + executable_path[used] = '\0'; + } + + if (suffix_len > sizeof(executable_path) - used - 1) + suffix_len = sizeof(executable_path) - used - 1; + memcpy(executable_path + used, suffix, suffix_len); + executable_path[used + suffix_len] = '\0'; + } + display_path = executable_path; + + if (geteuid() == 0) { + fprintf(stderr, + "\nERROR: FriiDump refuses this Linux vendor-command path while running as root.\n" + " Run FriiDump as your normal user and grant only CAP_SYS_RAWIO to the\n" + " exact validated executable. Do not use sudo to run the whole program.\n" + " Exact executable: %s\n" + " Install: sudo setcap cap_sys_rawio=ep '%s'\n" + " Verify: getcap '%s'\n" + " Rebuilding or replacing the executable clears file capabilities.\n\n", + display_path, display_path, display_path); + snprintf( + note, + sizeof(note), + "Linux raw-I/O preflight failed: FriiDump was running as root. The supported least-privilege configuration is a normal user process with cap_sys_rawio=ep on the exact validated executable (%s).", + display_path); + friidump_linux_record_rawio_failure(d, xbox_output_requested, note); + return false; + } + + state = friidump_linux_rawio_effective(&effective_mask); + if (state == FRIIDUMP_LINUX_RAWIO_PRESENT) { + fprintf(stderr, + "Linux raw-I/O preflight: PASS (CAP_SYS_RAWIO effective; CapEff=0x%016llX).\n", + effective_mask); + return true; + } + + fprintf(stderr, + "\nERROR: Linux vendor-command authorization is unavailable.\n" + " This operation uses vendor-specific SCSI commands and requires effective\n" + " CAP_SYS_RAWIO. Device access through the cdrom group is necessary but is\n" + " not sufficient for commands such as HLDS 0xE7 memory reads.\n" + " Exact executable: %s\n" + " Install: sudo setcap cap_sys_rawio=ep '%s'\n" + " Verify: getcap '%s'\n" + " Expected: %s cap_sys_rawio=ep\n" + " Do not run the entire FriiDump process as root. Rebuilding or replacing\n" + " the executable clears file capabilities, so reapply and verify afterward.\n\n", + display_path, display_path, display_path, display_path); + + snprintf( + note, + sizeof(note), + "Linux raw-I/O preflight failed: effective CAP_SYS_RAWIO was %s for executable %s (CapEff=0x%016llX). Apply cap_sys_rawio=ep to the exact validated executable; do not run FriiDump as root. Rebuilds clear file capabilities.", + state == FRIIDUMP_LINUX_RAWIO_MISSING ? "missing" : "unavailable/undetectable", + display_path, + effective_mask); + friidump_linux_record_rawio_failure(d, xbox_output_requested, note); + return false; +} +#endif + static void friidump_summary_begin(disc *d) { const char *target; if (!d || disc_get_hlds_e7_type(d) == 0) @@ -128,15 +372,39 @@ static void friidump_summary_begin(disc *d) { } static void friidump_print_validation_summary(double duration, bool have_duration) { - double mib_total, mib_per_hour; + double mib_completed, mib_per_hour; + bool have_completed_measurement; const char *seed_status; + const char *dump_status; if (!g_validation_summary.active) return; seed_status = g_validation_summary.seed_applicable ? (g_validation_summary.seed_ok ? "OK" : "FAILED/NOT REACHED") : "N/A (Xbox reference auth path)"; - mib_total = (double) g_validation_summary.sectors * 2048.0 / 1024.0 / 1024.0; - mib_per_hour = (have_duration && duration > 0.0) ? (mib_total / duration * 3600.0) : 0.0; + dump_status = "NOT ATTEMPTED"; + if (g_validation_summary.dump_attempted) { + if (g_validation_summary.dump_ok) + dump_status = "OK"; + else if (g_native_cancel_requested && g_native_report.have_byte_count && + g_native_report.byte_count > 0) + dump_status = "CANCELLED (PARTIAL)"; + else if (g_native_cancel_requested) + dump_status = "CANCELLED"; + else if (g_native_report.have_byte_count && g_native_report.byte_count > 0) + dump_status = "FAILED (PARTIAL)"; + else + dump_status = "FAILED"; + } + have_completed_measurement = + g_native_report.dump_attempted && + g_native_report.have_byte_count && + !g_native_report_resumed; + mib_completed = have_completed_measurement + ? (double) g_native_report.byte_count / 1024.0 / 1024.0 + : 0.0; + mib_per_hour = (have_completed_measurement && have_duration && duration > 0.0) + ? (mib_completed / duration * 3600.0) + : 0.0; fprintf(stderr, "\nHLDS 0xE7 validation summary:\n" " Model.............: %s\n" @@ -146,35 +414,61 @@ static void friidump_print_validation_summary(double duration, bool have_duratio " Memory windows....: %u\n" " Disc type.........: %s\n" " Game/Media ID.....: %s\n" - " Title.............: %s\n" - " Output............: %s\n" - " Seed read.........: %s\n" - " Dump status.......: %s\n" - " STOP UNIT.........: %s\n", + " Title.............: %s\n", g_validation_summary.model[0] ? g_validation_summary.model : "(unknown)", g_validation_summary.profile ? g_validation_summary.profile : "(unknown)", g_validation_summary.command, g_validation_summary.method, g_validation_summary.cache_base, g_validation_summary.mem_blocks, g_validation_summary.disc_type[0] ? g_validation_summary.disc_type : "(unknown)", g_validation_summary.game_id[0] ? g_validation_summary.game_id : "(unknown)", - g_validation_summary.title[0] ? g_validation_summary.title : "(unknown)", + g_validation_summary.title[0] ? g_validation_summary.title : "(unknown)"); + if (g_validation_summary.region[0]) + fprintf(stderr, " Region............: %s\n", g_validation_summary.region); + fprintf(stderr, + " Output............: %s\n" + " Seed read.........: %s\n" + " Dump status.......: %s\n" + " STOP UNIT.........: %s\n", g_validation_summary.output[0] ? g_validation_summary.output : "(none)", seed_status, - g_validation_summary.dump_attempted ? (g_validation_summary.dump_ok ? "OK" : "FAILED") : "NOT ATTEMPTED", + dump_status, g_validation_summary.stop_attempted ? (g_validation_summary.stop_ok ? "OK" : "FAILED") : "NOT ATTEMPTED"); if (g_validation_summary.have_seed_duration) { char seed_buf[32]; friidump_format_hms(g_validation_summary.seed_duration, seed_buf, sizeof(seed_buf)); fprintf(stderr, " Seed elapsed......: %s (%.2f seconds)\n", seed_buf, g_validation_summary.seed_duration); } - if (g_validation_summary.dump_attempted && !g_validation_summary.dump_ok && g_validation_summary.fail_sector != 0xFFFFFFFFU) - fprintf(stderr, " Failure sector....: %u..%u\n", g_validation_summary.fail_sector, g_validation_summary.fail_sector + 15); - if (g_validation_summary.sectors) - fprintf(stderr, " Sectors...........: %u\n", g_validation_summary.sectors); + if (g_validation_summary.dump_attempted && !g_validation_summary.dump_ok && + g_validation_summary.fail_sector != 0xFFFFFFFFU) { + if (g_native_cancel_requested) + fprintf(stderr, " Cancelled at......: sector %u\n", g_validation_summary.fail_sector); + else + fprintf(stderr, " Failure sector....: %u..%u\n", g_validation_summary.fail_sector, g_validation_summary.fail_sector + 15); + } + if (g_validation_summary.have_source_sectors) + fprintf(stderr, " Source sectors....: %u\n", g_validation_summary.source_sectors); + if (g_validation_summary.have_expected_output_sectors) + fprintf(stderr, " Expected output...: %u sectors\n", g_validation_summary.expected_output_sectors); + else if (g_validation_summary.sectors) + fprintf(stderr, " Expected sectors..: %u\n", g_validation_summary.sectors); + if (g_native_report.have_sector_count) + fprintf(stderr, g_native_report_resumed + ? " Output sectors....: %llu (includes prior resumed data)\n" + : " Completed sectors.: %llu\n", + (unsigned long long) g_native_report.sector_count); + if (g_native_report.have_byte_count) + fprintf(stderr, g_native_report_resumed + ? " Output bytes......: %llu (%.2f MiB; includes prior resumed data)\n" + : " Completed bytes...: %llu (%.2f MiB)\n", + (unsigned long long) g_native_report.byte_count, + (double) g_native_report.byte_count / 1024.0 / 1024.0); if (have_duration) fprintf(stderr, " Duration..........: %.2f seconds\n", duration); if (have_duration && mib_per_hour > 0.0) - fprintf(stderr, " Observed average..: %.2f MiB/h over %.2f MiB ISO payload\n", mib_per_hour, mib_total); + fprintf(stderr, " Observed average..: %.2f MiB/h over %.2f MiB completed output\n", + mib_per_hour, mib_completed); + else if (g_native_report_resumed) + fprintf(stderr, " Observed average..: N/A (resumed output; measurement scope unknown)\n"); if (g_validation_summary.dump_ok) { if (g_redump_attempted) { fprintf(stderr, " Redump verify.....: %s\n", redump_verify_overall_string(&g_redump_result)); @@ -197,7 +491,7 @@ static void friidump_print_validation_summary(double duration, bool have_duratio #define PACKAGE_NAME "FriiDump" /* Define to the version of this package. */ -#define PACKAGE_VERSION "0.5.3.15" +#define PACKAGE_VERSION "0.5.3.16-pf1-candidate21" #ifdef WIN32 @@ -251,6 +545,9 @@ struct { char *xgd1_layout_probe_report; bool xgd1_raw_id_probe; char *xgd1_raw_id_probe_report; + char *native_report_json; + char *native_report_dir; + char *firmware_modified_note; } options; @@ -830,6 +1127,16 @@ static void friidump_verify_redump_iso(dumper *dmp, disc_type type_id) { "FriiDump finalized ISO multihash"); } +static bool friidump_uses_windows_xbox_reference_path(disc *d, disc_type type_id) { +#ifdef WIN32 + return type_id == DISC_TYPE_XBOX && d && disc_is_xbox_challenge_drive(d); +#else + (void) d; + (void) type_id; + return false; +#endif +} + static void friidump_verify_redump_xbox_reference(dumper *dmp) { xbox_ref_dump_result result; @@ -875,7 +1182,490 @@ static void friidump_verify_redump_xbox_reference(dumper *dmp) { result.md5, result.sha1, result.sha256, - "GDR-8050L reference finalized full-file hashes"); + "Xbox finalized full-file hashes"); +} + + +static void friidump_native_signal_handler(int signal_number) { + (void) signal_number; + g_native_cancel_requested = 1; +} + +static bool friidump_native_cancel_callback(void *unused) { + (void) unused; + return g_native_cancel_requested != 0; +} + +static const char *friidump_native_platform(disc_type type_id) { + switch (type_id) { + case DISC_TYPE_GAMECUBE: + return "gamecube"; + case DISC_TYPE_WII: + case DISC_TYPE_WII_DL: + return "wii"; + case DISC_TYPE_XBOX: + return "xbox"; + case DISC_TYPE_DVD: + return "dvd"; + default: + return "unknown"; + } +} + +static bool friidump_native_report_operation_supported(void) { + return options.device && + !options.raw_in && + !options.allmethods && + !options.stop_unit && + !options.hlds_e7_scan && + !options.hlds_e7_subcmd_sweep && + !options.hlds_e7_memrange_sweep && + !options.xgd1_layout_probe && + !options.xgd1_raw_id_probe && + !(options.raw_out && options.iso_requested) && + (options.autodump || options.raw_out || + options.iso_requested || options.xiso_requested); +} + +static bool friidump_native_report_option_requested(void) { + return options.native_report_json || + options.native_report_dir || + options.firmware_modified_note; +} + +static void friidump_native_configure(void) { + if (!friidump_native_report_operation_supported()) + return; + + friidump_native_report_enable_default(&g_native_report); + + if (options.native_report_json) + friidump_native_report_enable_path( + &g_native_report, options.native_report_json); + if (options.native_report_dir) + friidump_native_report_enable_dir( + &g_native_report, options.native_report_dir); + +#ifdef FRIIDUMP_BUILD_COMMIT + friidump_native_report_set_build_commit(&g_native_report, FRIIDUMP_BUILD_COMMIT); +#endif + + if (options.firmware_modified_note) + friidump_native_report_set_firmware_modified( + &g_native_report, true, options.firmware_modified_note); + + if (signal(SIGINT, friidump_native_signal_handler) == SIG_ERR) { + fprintf(stderr, "WARNING: could not install the native-report Ctrl+C handler.\n"); + } +} + +static void friidump_native_set_drive(disc *d) { + if (!friidump_native_report_is_enabled(&g_native_report) || !d) + return; + + friidump_native_report_set_drive( + &g_native_report, + disc_get_drive_vendor(d), + disc_get_drive_product_id(d), + disc_get_drive_firmware_revision(d), + "ATAPI", + disc_get_device(d)); +} + +static void friidump_native_set_profile(disc *d, bool xbox_output_requested) { + (void) xbox_output_requested; + + if (!friidump_native_report_is_enabled(&g_native_report) || !d) + return; + + friidump_native_report_set_profile( + &g_native_report, + disc_get_hlds_e7_support_tier(d), + disc_get_hlds_e7_static_cdb_base(d), + disc_get_hlds_e7_static_gate(d)); +} + +static void friidump_native_set_preflight_failure(int media_rc, + int sense_key, + int asc, + int ascq) { + char note[256]; + + if (!friidump_native_report_is_enabled(&g_native_report)) + return; + + friidump_native_report_set_seed( + &g_native_report, false, "not_attempted", false, 0.0); + friidump_native_report_set_dump( + &g_native_report, false, "not_attempted", + false, 0, false, 0, false, 0.0, + NULL, false, 0, NULL); + friidump_native_report_set_reference( + &g_native_report, NULL, "not_applicable", NULL); + + if (media_rc == 0) { + friidump_native_report_set_outcome( + &g_native_report, "diagnostic_no_media", "not_applicable"); + snprintf(note, sizeof(note), + "Media preflight found no readable disc (sense %02X/%02X/%02X).", + sense_key & 0xff, asc & 0xff, ascq & 0xff); + } else { + friidump_native_report_set_outcome( + &g_native_report, "other", "fail"); + snprintf(note, sizeof(note), + "Drive or media did not become ready (sense %02X/%02X/%02X).", + sense_key & 0xff, asc & 0xff, ascq & 0xff); + } + friidump_native_report_add_note(&g_native_report, note); +} + +static void friidump_native_set_media(disc_type type_id, + const char *title, + const char *region, + const char *disc_id) { + if (!friidump_native_report_is_enabled(&g_native_report)) + return; + friidump_native_report_set_media( + &g_native_report, + friidump_native_platform(type_id), + title, + region, + disc_id); +} + +static void friidump_native_add_measurement_scope(const char *scope) { + char note[128]; + if (!friidump_native_report_is_enabled(&g_native_report) || !scope) + return; + snprintf(note, sizeof(note), "Measurement scope: %s.", scope); + friidump_native_report_add_note(&g_native_report, note); +} + +static void friidump_native_capture_output(dumper *dmp, + disc_type type_id, + bool xiso_mode, + bool success, + bool cancelled, + u_int32_t failure_sector, + u_int32_t expected_sectors) { + xbox_ref_dump_result xbox_result; + bool have_xbox_result; + const char *path; + const char *crc32; + const char *md5; + const char *sha1; + const char *sha256; + uint64_t bytes; + uint64_t sectors; + uint64_t sector_size; + bool partial; + const char *failure_stage; + + if (!friidump_native_report_is_enabled(&g_native_report) || !dmp) + return; + + g_native_report_dump_started = true; + g_native_report_resumed = dumper_get_start_sector(dmp) > 0; + + xbox_ref_dump_result_init(&xbox_result); + have_xbox_result = dumper_get_xbox_reference_result(dmp, &xbox_result); + path = NULL; + crc32 = md5 = sha1 = sha256 = NULL; + bytes = 0; + sectors = 0; + sector_size = 2048; + + if (have_xbox_result) { + if (xbox_result.cancelled) + cancelled = true; + path = xbox_result.output_path[0] ? xbox_result.output_path : NULL; + bytes = xbox_result.output_size; + sectors = xbox_result.output_sectors; + if (g_validation_summary.active) { + if (xbox_result.output_path[0]) + friidump_summary_copy( + g_validation_summary.output, + sizeof(g_validation_summary.output), + xbox_result.output_path); + if (xbox_result.title[0]) + friidump_summary_copy( + g_validation_summary.title, + sizeof(g_validation_summary.title), + xbox_result.title); + if (xbox_result.media_id[0]) + friidump_summary_copy( + g_validation_summary.game_id, + sizeof(g_validation_summary.game_id), + xbox_result.media_id); + if (xbox_result.region[0]) + friidump_summary_copy( + g_validation_summary.region, + sizeof(g_validation_summary.region), + xbox_result.region); + if (expected_sectors > 0) { + g_validation_summary.source_sectors = expected_sectors; + g_validation_summary.have_source_sectors = true; + } + if (xbox_result.output_sectors > 0) { + g_validation_summary.expected_output_sectors = + xbox_result.output_sectors; + g_validation_summary.have_expected_output_sectors = true; + } + g_validation_summary.seed_applicable = false; + g_validation_summary.have_seed_duration = false; + } + if (xbox_result.hashes_complete) { + crc32 = xbox_result.crc32; + md5 = xbox_result.md5; + sha1 = xbox_result.sha1; + sha256 = xbox_result.sha256; + } + if (xbox_result.title[0] || xbox_result.media_id[0] || xbox_result.region[0]) { + friidump_native_report_set_media( + &g_native_report, + "xbox", + xbox_result.title[0] ? xbox_result.title : + (g_native_report.have_title ? g_native_report.title : NULL), + xbox_result.region[0] ? xbox_result.region : NULL, + xbox_result.media_id[0] ? xbox_result.media_id : + (g_native_report.have_disc_id ? g_native_report.disc_id : NULL)); + } + if (xbox_result.have_game_region) { + char region_note[96]; + snprintf(region_note, sizeof(region_note), + "Xbox XBE GameRegion mask: 0x%08x.", + xbox_result.game_region); + friidump_native_report_add_note(&g_native_report, region_note); + } + if (xbox_result.elapsed_seconds > 0.0) { + g_native_report.have_dump_duration = true; + g_native_report.dump_duration_seconds = xbox_result.elapsed_seconds; + } + } else if (xiso_mode) { + path = (options.xiso_out && options.xiso_out[0]) ? options.xiso_out : NULL; + if (path) + bytes = friidump_file_size(path); + sectors = bytes / 2048; + if (!options.no_hashing) { + crc32 = dumper_get_xiso_crc32(dmp); + md5 = dumper_get_xiso_md5(dmp); + sha1 = dumper_get_xiso_sha1(dmp); + sha256 = dumper_get_xiso_sha2(dmp); + } + } else if (options.iso_out && options.iso_out[0]) { + path = options.iso_out; + bytes = friidump_file_size(path); + sectors = bytes / 2048; + if (!options.no_hashing) { + crc32 = dumper_get_iso_crc32(dmp); + md5 = dumper_get_iso_md5(dmp); + sha1 = dumper_get_iso_sha1(dmp); + sha256 = dumper_get_iso_sha2(dmp); + } + } else if (options.raw_out && options.raw_out[0]) { + path = options.raw_out; + sector_size = 2064; + bytes = friidump_file_size(path); + sectors = bytes / sector_size; + if (!options.no_hashing) { + crc32 = dumper_get_raw_crc32(dmp); + md5 = dumper_get_raw_md5(dmp); + sha1 = dumper_get_raw_sha1(dmp); + sha256 = dumper_get_raw_sha2(dmp); + } + } + + if (path && (!success || bytes == 0)) { + uint64_t observed_bytes = friidump_file_size(path); + if (observed_bytes > 0 || !success) + bytes = observed_bytes; + if (!success) + sectors = bytes / sector_size; + } + + if (success && sectors == 0 && type_id != DISC_TYPE_XBOX) { + sectors = expected_sectors; + bytes = sectors * sector_size; + } + + if (g_native_report_resumed) { + /* FriiDump's streaming hashes cover only bytes produced by this + * invocation when a file is resumed, not the complete output file. */ + crc32 = md5 = sha1 = sha256 = NULL; + } + + partial = !success && (bytes > 0 || + (failure_sector != 0 && failure_sector != 0xFFFFFFFFU)); + failure_stage = NULL; + if (!success) { + if (cancelled) + failure_stage = "user_cancelled"; + else + failure_stage = (failure_sector == 0xFFFFFFFFU) + ? "xbox_reference_dump" + : "raw_sector_read_or_output_write"; + } + + friidump_native_report_set_dump( + &g_native_report, + true, + success ? "pass" : (partial ? "partial" : "fail"), + success || sectors > 0, + sectors, + success || bytes > 0, + bytes, + g_native_report.have_dump_duration, + g_native_report.dump_duration_seconds, + failure_stage, + !success && failure_sector != 0xFFFFFFFFU, + failure_sector, + path); + + friidump_native_report_set_hashes( + &g_native_report, crc32, md5, sha1, sha256); + + if (path) { + friidump_native_report_add_artifact( + &g_native_report, + "dump_output", + path, + true, + bytes, + sha256); + } + + if (success) + friidump_native_report_set_outcome( + &g_native_report, "full_dump", "pass"); + else if (partial) + friidump_native_report_set_outcome( + &g_native_report, "partial_dump", "partial"); + else if (cancelled) + friidump_native_report_set_outcome( + &g_native_report, "other", "fail"); + else + friidump_native_report_set_outcome( + &g_native_report, "full_dump", "fail"); + + if (cancelled) + friidump_native_report_add_note( + &g_native_report, + "The invocation was cancelled by the user; only completed output bytes are represented."); + + if (g_native_report_resumed) { + friidump_native_add_measurement_scope("unknown"); + friidump_native_report_add_note( + &g_native_report, + "Resumed output: duration is not used for throughput because the output byte count includes data from an earlier invocation."); + friidump_native_report_add_note( + &g_native_report, + "Resumed output: streaming hashes were suppressed because they do not describe the complete output file."); + } else if (!success && partial) { + friidump_native_add_measurement_scope("partial_progress"); + } else if (type_id == DISC_TYPE_XBOX || xiso_mode) { + friidump_native_add_measurement_scope("assembled_output"); + friidump_native_report_add_note( + &g_native_report, + "Xbox output throughput describes the produced logical or assembled output and is not directly comparable to a full optical-payload read."); + } else if (success) { + friidump_native_add_measurement_scope("full_optical_payload"); + } else { + friidump_native_add_measurement_scope("unknown"); + } + + if (options.no_hashing) + friidump_native_report_add_note( + &g_native_report, + "Output hashing was disabled for this invocation."); +} + +static bool friidump_elapsed_from_stats(const progstats *stats, double *duration) { + double value; + suseconds_t usecs; + + if (!stats || !duration || stats->start_time.tv_sec == 0 || + stats->end_time.tv_sec == 0) + return false; + + value = (double) (stats->end_time.tv_sec - stats->start_time.tv_sec); + if (stats->end_time.tv_usec >= stats->start_time.tv_usec) { + usecs = stats->end_time.tv_usec - stats->start_time.tv_usec; + } else { + value -= 1.0; + usecs = USECS_PER_SEC + stats->end_time.tv_usec - stats->start_time.tv_usec; + } + value += (double) usecs / (double) USECS_PER_SEC; + if (value < 0.0) + return false; + *duration = value; + return true; +} + +static void friidump_native_finalize_reference(void) { + if (!friidump_native_report_is_enabled(&g_native_report)) + return; + + if (strcmp(g_native_report.run_result, "not_applicable") == 0) { + friidump_native_report_set_reference( + &g_native_report, NULL, "not_applicable", NULL); + } else if (g_redump_attempted) { + if (g_redump_result.status == REDUMP_VERIFY_MATCH) { + friidump_native_report_set_reference( + &g_native_report, + "redump", + "match", + g_redump_result.rom_name[0] + ? g_redump_result.rom_name + : g_redump_result.game_name); + } else if (g_redump_result.status == REDUMP_VERIFY_NO_MATCH) { + friidump_native_report_set_reference( + &g_native_report, "redump", "mismatch", NULL); + } else { + friidump_native_report_set_reference( + &g_native_report, "redump", "not_checked", NULL); + } + } else if (options.xiso_requested) { + friidump_native_report_set_reference( + &g_native_report, NULL, "not_applicable", NULL); + } else { + friidump_native_report_set_reference( + &g_native_report, NULL, "not_checked", NULL); + } +} + +static void friidump_native_publish(double duration, bool have_duration) { + char path[FRIIDUMP_REPORT_PATH_MAX]; + char error_text[256]; + const char *name_basis; + + if (!friidump_native_report_is_enabled(&g_native_report)) + return; + + name_basis = xbox_ref_log_path(); + if (!name_basis) + name_basis = friidump_requested_output_target(); + if (!name_basis) + name_basis = "friidump.log"; + friidump_native_report_set_name_basis(&g_native_report, name_basis); + + if (g_native_report_dump_started && !g_native_report_resumed && + !g_native_report.have_dump_duration && have_duration) { + g_native_report.have_dump_duration = true; + g_native_report.dump_duration_seconds = duration; + } + + friidump_native_finalize_reference(); + + if (friidump_native_report_write( + &g_native_report, + path, + sizeof(path), + error_text, + sizeof(error_text))) { + fprintf(stderr, "Native report.......: %s (atomic write)\n", path); + } else { + fprintf(stderr, "WARNING: native report was not written: %s\n", error_text); + } } void help (void) { @@ -965,13 +1755,20 @@ void help (void) { " --scan-dump-prefix Optional raw 0xE7 window dump prefix for --hlds-e7-scan\n" " --hlds-profile-report Write selected HLDS profile/evidence JSON\n" " --redump-dat-dir Directory containing canonical Redump DAT files\n" - " (default: executable-relative redump_dat, then current directory)\n" + " (default: bundled executable data, installed share data, then current directory)\n" + " Environment override: FRIIDUMP_REDUMP_DAT_DIR\n" " --redump-report Write atomic Redump evidence JSON\n" " --no-redump-verify Disable automatic post-dump DAT verification\n" " --xgd1-layout-probe Read-only locked/unlocked XGD1 boundary probe;\n" " writes atomic JSON and does not create an ISO\n" " --xgd1-raw-id-probe Modified-firmware cache-flushed, block-aligned raw-ID probe;\n" " maps logical LBAs to decoded physical sector IDs\n" + " Native .friidump.json reports are created by default\n" + " beside the final log using its filename\n" + " --report-json Override the complete native report pathname\n" + " --report-dir Override only the report destination directory\n" + " (may be combined with --report-json)\n" + " --firmware-modified Mark firmware modified; omission assumes stock\n" " -A, --allmethods Try all known command/method combinations until\n" " one works. Reopens the drive for each command so\n" " command-specific vendor handlers are rebound.\n" @@ -982,6 +1779,14 @@ void help (void) { " -f, --donottflush Do not call fflush() after every fwrite()\n" #endif ); +#ifndef WIN32 + fprintf (stderr, + "\nLinux raw-I/O requirement:\n" + " GC/Wii memory-dump methods and Xbox vendor-unlock paths require effective\n" + " CAP_SYS_RAWIO. Apply cap_sys_rawio=ep to the exact validated executable;\n" + " do not run FriiDump as root. Rebuilds and replacements clear capabilities.\n" + " See docs/LINUX.md and validation/friidump-linux-rawio-capability.sh.\n"); +#endif return; } @@ -1032,6 +1837,9 @@ bool optparse (int argc, char **argv) { {"no-redump-verify", 0, 0, 1008}, {"xgd1-layout-probe", 1, 0, 1009}, {"xgd1-raw-id-probe", 1, 0, 1010}, + {"report-json", 1, 0, 1011}, + {"report-dir", 1, 0, 1012}, + {"firmware-modified", 1, 0, 1013}, #ifdef DEBUG /* We don't want newbies to generate and put into circulation bad dumps, so this options are disabled for releases */ {"donottunscramble", 0, 0, 'n'}, @@ -1084,6 +1892,9 @@ bool optparse (int argc, char **argv) { options.xgd1_layout_probe_report = NULL; options.xgd1_raw_id_probe = false; options.xgd1_raw_id_probe_report = NULL; + options.native_report_json = NULL; + options.native_report_dir = NULL; + options.firmware_modified_note = NULL; do { #ifdef DEBUG @@ -1249,6 +2060,15 @@ bool optparse (int argc, char **argv) { my_strdup (options.xgd1_raw_id_probe_report, optarg); options.disctype = DISC_TYPE_XBOX; break; + case 1011: + my_strdup (options.native_report_json, optarg); + break; + case 1012: + my_strdup (options.native_report_dir, optarg); + break; + case 1013: + my_strdup (options.firmware_modified_note, optarg); + break; case -1: break; default: @@ -1286,6 +2106,28 @@ bool optparse (int argc, char **argv) { options.hlds_e7_subcmd_sweep || options.hlds_e7_memrange_sweep || (options.xgd1_layout_probe && options.xgd1_raw_id_probe))) { fprintf (stderr, "XGD1 probe modes are mutually exclusive read-only diagnostics and cannot be combined with dump, conversion, all-methods, or HLDS 0xE7 probe options.\n"); + } else if ((options.native_report_json && !options.native_report_json[0]) || + (options.native_report_dir && !options.native_report_dir[0])) { + fprintf (stderr, "Native report file and directory arguments must not be empty.\n"); + } else if ((options.native_report_json && strlen(options.native_report_json) >= FRIIDUMP_REPORT_PATH_MAX) || + (options.native_report_dir && strlen(options.native_report_dir) >= FRIIDUMP_REPORT_PATH_MAX)) { + fprintf (stderr, "Native report path is too long.\n"); + } else if (options.firmware_modified_note && + !options.firmware_modified_note[0]) { + fprintf (stderr, "--firmware-modified requires a non-empty explanation.\n"); + } else if (options.firmware_modified_note && + strlen(options.firmware_modified_note) > 1000) { + fprintf (stderr, "--firmware-modified note exceeds the 1000-byte report limit.\n"); + } else if (friidump_native_report_option_requested() && + !friidump_native_report_operation_supported()) { + fprintf (stderr, "Native compatibility report options require one normal device dump invocation with exactly one primary output and cannot be combined with conversion, all-methods, stop-only, or research probe modes.\n"); + } else if (options.device && + !options.allmethods && !options.stop_unit && + !options.hlds_e7_scan && !options.hlds_e7_subcmd_sweep && + !options.hlds_e7_memrange_sweep && !options.xgd1_layout_probe && + !options.xgd1_raw_id_probe && + options.raw_out && options.iso_requested) { + fprintf (stderr, "Native compatibility reports are created by default and require exactly one primary output; do not combine -r and -i.\n"); } else { /* Specified options seem to make sense */ out = true; @@ -1308,6 +2150,8 @@ int dologic (disc *d, progstats *stats) { xbox_forced = (options.disctype == DISC_TYPE_XBOX); xbox_output_requested = xbox_forced || options.xiso_requested || options.xgd1_layout_probe || options.xgd1_raw_id_probe; dump_attempted = false; + out = false; + g_native_cancel_requested = 0; @@ -1327,6 +2171,8 @@ int dologic (disc *d, progstats *stats) { ); if (xbox_output_requested && !disc_is_xbox_unlock_drive (d)) { + if (friidump_native_report_is_enabled(&g_native_report)) + friidump_native_report_add_note(&g_native_report, "The selected drive does not expose a supported Xbox unlock profile; dumping was not attempted."); fprintf (stderr, "Xbox/XGD output is limited to the supported Xbox unlock profiles " "currently wired into this branch: GDR-8050L, GDR-3120L, " @@ -1343,8 +2189,11 @@ int dologic (disc *d, progstats *stats) { init_range(d, options.sec_disc, options.sec_mem); - if (!(disc_set_read_method (d, options.dump_method))) - exit (2); + if (!(disc_set_read_method (d, options.dump_method))) { + if (friidump_native_report_is_enabled(&g_native_report)) + friidump_native_report_add_note(&g_native_report, "FriiDump could not select the requested read method; dumping was not attempted."); + return false; + } if (xbox_output_requested && disc_get_method(d) == 10) { fprintf (stderr, "Command............: Xbox direct MMC/SCSI path\n"); @@ -1377,6 +2226,17 @@ int dologic (disc *d, progstats *stats) { } options.dump_method=disc_get_method(d); friidump_summary_begin(d); + friidump_native_set_profile(d, xbox_output_requested); +#ifndef WIN32 + if (!friidump_linux_rawio_preflight( + d, + xbox_output_requested, + drive_supported, + options.hlds_e7_scan || + options.hlds_e7_subcmd_sweep || + options.hlds_e7_memrange_sweep)) + return false; +#endif if ((options.dump_method==0) || (options.dump_method==1) || (options.dump_method==2) || (options.dump_method==3) || (options.dump_method==4) || (options.dump_method==5) || (options.dump_method==6) @@ -1408,17 +2268,36 @@ int dologic (disc *d, progstats *stats) { " This GC/Wii 0xE7 path assumes a cross-flashed or modified GDR-8050L firmware with 0xE7 memdump support added.\n" " Stock GDR-8050L firmware is still supported for Xbox ripping, but it is not expected to dump GC/Wii discs through this path.\n"); } +#ifdef WIN32 fprintf (stderr, "\nWindows AutoPlay warning:\n" " HLDS 0xE7 GC/Wii seed reads are sensitive to Windows polling.\n" " Disable AutoPlay for this drive/media and close File Explorer or any \"insert a disc\" dialogs before dumping.\n" " FriiDump will try to lock the volume, but AutoPlay can still interfere before or during seed retrieval.\n"); fprintf (stderr, "Applying Windows volume lock guard for HLDS 0xE7 seed reads... "); +#else + fprintf (stderr, + "\nLinux media-polling warning:\n" + " HLDS 0xE7 GC/Wii seed reads can be disturbed by desktop automount and media polling.\n" + " Unmount the disc and close file managers or media players before dumping.\n"); +#endif disc_refresh_volume (d); - if (disc_lock_volume (d) < 0) - fprintf (stderr, "Warning: failed; disable AutoPlay, close File Explorer/AutoPlay dialogs for this drive, then rerun.\n"); - else - fprintf (stderr, "OK\n"); + { + int volume_lock_result = disc_lock_volume (d); +#ifdef WIN32 + if (volume_lock_result < 0) + fprintf (stderr, "Warning: failed; disable AutoPlay, close File Explorer/AutoPlay dialogs for this drive, then rerun.\n"); + else + fprintf (stderr, "OK\n"); +#else + if (volume_lock_result > 0) + fprintf (stderr, "Linux exclusive optical-device lock: unavailable; continuing without one.\n"); + else if (volume_lock_result < 0) + fprintf (stderr, "Linux exclusive optical-device lock: failed; continuing without one.\n"); + else + fprintf (stderr, "Linux exclusive optical-device lock: acquired.\n"); +#endif + } } if (options.xgd1_layout_probe || options.xgd1_raw_id_probe) { @@ -1508,6 +2387,8 @@ int dologic (disc *d, progstats *stats) { fprintf (stderr, "\nChecking for ready media before disc seed retrieval... "); media_rc = disc_media_preflight (d, 15000, &media_sense_key, &media_asc, &media_ascq); if (media_rc <= 0) { + friidump_native_set_preflight_failure( + media_rc, media_sense_key, media_asc, media_ascq); if (media_rc == 0) fprintf (stderr, "No readable disc present (sense %02X/%02X/%02X). Insert a disc, wait for spin-up, and retry.\n", media_sense_key, media_asc, media_ascq); else @@ -1516,7 +2397,7 @@ int dologic (disc *d, progstats *stats) { } fprintf (stderr, "OK\n"); - time_t seed_start, seed_end; + struct timeval seed_start, seed_end; double seed_elapsed; char seed_elapsed_buf[32]; @@ -1525,10 +2406,12 @@ int dologic (disc *d, progstats *stats) { else fprintf (stderr, "\nRetrieving disc seeds, this might take a while... "); - seed_start = time(NULL); + gettimeofday(&seed_start, NULL); if (!disc_init (d, options.disctype, options.sectors_no)) { - seed_end = time(NULL); - seed_elapsed = difftime(seed_end, seed_start); + gettimeofday(&seed_end, NULL); + seed_elapsed = (double)(seed_end.tv_sec - seed_start.tv_sec) + + (double)(seed_end.tv_usec - seed_start.tv_usec) / (double)USECS_PER_SEC; + if (seed_elapsed < 0.0) seed_elapsed = 0.0; friidump_format_hms(seed_elapsed, seed_elapsed_buf, sizeof(seed_elapsed_buf)); if (!xbox_output_requested) fprintf (stderr, "[Elapsed:%s] ", seed_elapsed_buf); @@ -1536,11 +2419,42 @@ int dologic (disc *d, progstats *stats) { g_validation_summary.have_seed_duration = true; g_validation_summary.seed_duration = seed_elapsed; } + if (friidump_native_report_is_enabled(&g_native_report)) { + if (g_native_cancel_requested) { + friidump_native_report_set_seed(&g_native_report, true, "fail", true, seed_elapsed); + friidump_native_report_set_outcome(&g_native_report, "other", "fail"); + friidump_native_report_add_note(&g_native_report, "The invocation was cancelled by the user during media initialization or seed retrieval."); + } else if (xbox_output_requested) { + friidump_native_report_set_seed(&g_native_report, false, "not_applicable", false, 0.0); + friidump_native_report_set_outcome(&g_native_report, "other", "fail"); + friidump_native_report_add_note(&g_native_report, "Xbox/media initialization failed before dumping."); + } else { + friidump_native_report_set_seed(&g_native_report, true, "fail", true, seed_elapsed); + friidump_native_report_set_outcome(&g_native_report, "seed_only", "fail"); + friidump_native_report_add_note(&g_native_report, "Disc initialization or seed retrieval failed."); + } + friidump_native_report_set_dump(&g_native_report, false, "not_attempted", false, 0, false, 0, false, 0.0, NULL, false, 0, NULL); + } + if (!xbox_output_requested) { + bool stop_ok; + friidump_capture_seed_diagnostic (d); + stop_ok = disc_stop_unit (d, false); + fprintf (stderr, "\nIssuing STOP UNIT / spin-down after seed failure... %s\n", stop_ok ? "OK" : "Failed"); + if (g_validation_summary.active) { + g_validation_summary.stop_attempted = true; + g_validation_summary.stop_ok = stop_ok; + } + if (friidump_native_report_is_enabled(&g_native_report)) + friidump_native_report_add_note(&g_native_report, + stop_ok ? "STOP UNIT after seed failure succeeded." : "STOP UNIT after seed failure failed."); + } fprintf (stderr, "Failed\n"); out = false; } else { - seed_end = time(NULL); - seed_elapsed = difftime(seed_end, seed_start); + gettimeofday(&seed_end, NULL); + seed_elapsed = (double)(seed_end.tv_sec - seed_start.tv_sec) + + (double)(seed_end.tv_usec - seed_start.tv_usec) / (double)USECS_PER_SEC; + if (seed_elapsed < 0.0) seed_elapsed = 0.0; friidump_format_hms(seed_elapsed, seed_elapsed_buf, sizeof(seed_elapsed_buf)); if (!xbox_output_requested) fprintf (stderr, "[Elapsed:%s] ", seed_elapsed_buf); @@ -1562,6 +2476,20 @@ int dologic (disc *d, progstats *stats) { friidump_summary_copy(g_validation_summary.title, sizeof(g_validation_summary.title), title); g_validation_summary.sectors = disc_get_sectors_no(d); } + friidump_native_set_media(type_id, title, + type_id == DISC_TYPE_XBOX ? NULL : region, game_id); + if (friidump_native_report_is_enabled(&g_native_report)) { + if (type_id == DISC_TYPE_GAMECUBE || type_id == DISC_TYPE_WII || type_id == DISC_TYPE_WII_DL) + friidump_native_report_set_seed(&g_native_report, true, "pass", true, seed_elapsed); + else + friidump_native_report_set_seed(&g_native_report, false, "not_applicable", false, 0.0); + } + if (g_native_cancel_requested) { + friidump_native_report_set_dump(&g_native_report, false, "not_attempted", false, 0, false, 0, false, 0.0, NULL, false, 0, NULL); + friidump_native_report_set_outcome(&g_native_report, "other", "fail"); + friidump_native_report_add_note(&g_native_report, "The invocation was cancelled by the user before dumping began."); + return false; + } fprintf (stderr, "\n" "Disc information:\n" @@ -1623,6 +2551,8 @@ int dologic (disc *d, progstats *stats) { fprintf (stderr, options.xiso_out && options.xiso_out[0] ? "Writing to file \"%s\" in Xbox XISO format\n\n" : "Writing Xbox XISO using XBE-derived filename (override with -X )\n\n", (options.xiso_out && options.xiso_out[0]) ? options.xiso_out : ""); dmp = dumper_new (d); + if (friidump_native_report_is_enabled(&g_native_report)) + dumper_set_cancel_callback(dmp, friidump_native_cancel_callback, NULL); dumper_set_hashing (dmp, !options.no_hashing); dumper_set_flushing (dmp, !options.no_flushing); @@ -1639,9 +2569,11 @@ int dologic (disc *d, progstats *stats) { dump_attempted = true; if (g_validation_summary.active) g_validation_summary.dump_attempted = true; - if (dumper_dump_xiso (dmp, ¤t_sector)) { + out = dumper_dump_xiso (dmp, ¤t_sector); + gettimeofday(&(stats -> end_time), NULL); + if (out) { fprintf (stderr, "Xbox XISO dump completed successfully!\n"); - if (!options.no_hashing && !(type_id == DISC_TYPE_XBOX && disc_is_xbox_challenge_drive (d))) + if (!options.no_hashing && !friidump_uses_windows_xbox_reference_path(d, type_id)) fprintf (stderr, "XISO image hashes:\n" "CRC32...: %s\n" @@ -1655,7 +2587,10 @@ int dologic (disc *d, progstats *stats) { if (g_validation_summary.active) g_validation_summary.dump_ok = true; } else { - fprintf (stderr, "\nXbox XISO dump failed at output sector: %u\n", current_sector); + if (dumper_was_cancelled(dmp)) + fprintf (stderr, "\nXbox XISO dump cancelled at output sector: %u\n", current_sector); + else + fprintf (stderr, "\nXbox XISO dump failed at output sector: %u\n", current_sector); out = false; if (g_validation_summary.active) { g_validation_summary.dump_ok = false; @@ -1664,6 +2599,12 @@ int dologic (disc *d, progstats *stats) { } } + if (dump_attempted) + friidump_native_capture_output(dmp, type_id, true, out, dumper_was_cancelled(dmp), current_sector, disc_get_sectors_no(d)); + else if (friidump_native_report_is_enabled(&g_native_report)) { + friidump_native_report_set_outcome(&g_native_report, "other", "fail"); + friidump_native_report_add_note(&g_native_report, "Xbox XISO output could not be prepared; dumping was not attempted."); + } dmp = dumper_destroy (dmp); } } else if (options.raw_out || options.iso_requested) { @@ -1680,6 +2621,8 @@ int dologic (disc *d, progstats *stats) { fprintf (stderr, "\n"); dmp = dumper_new (d); + if (friidump_native_report_is_enabled(&g_native_report)) + dumper_set_cancel_callback(dmp, friidump_native_cancel_callback, NULL); dumper_set_hashing (dmp, !options.no_hashing); dumper_set_flushing (dmp, !options.no_flushing); @@ -1702,7 +2645,9 @@ int dologic (disc *d, progstats *stats) { dump_attempted = true; if (g_validation_summary.active) g_validation_summary.dump_attempted = true; - if (dumper_dump (dmp, ¤t_sector)) { + out = dumper_dump (dmp, ¤t_sector); + gettimeofday(&(stats -> end_time), NULL); + if (out) { fprintf (stderr, "Dump completed successfully!\n"); if (!options.no_hashing && options.raw_out) fprintf (stderr, @@ -1716,7 +2661,7 @@ int dologic (disc *d, progstats *stats) { dumper_get_raw_crc32 (dmp), /*dumper_get_raw_md4 (dmp),*/ dumper_get_raw_md5 (dmp), dumper_get_raw_sha1 (dmp), dumper_get_raw_sha2 (dmp)/*, dumper_get_raw_ed2k (dmp)*/ ); - if (!options.no_hashing && options.iso_out && !(type_id == DISC_TYPE_XBOX && disc_is_xbox_challenge_drive (d))) + if (!options.no_hashing && options.iso_out && !friidump_uses_windows_xbox_reference_path(d, type_id)) fprintf (stderr, "ISO image hashes:\n" "CRC32...: %s\n" @@ -1742,7 +2687,9 @@ int dologic (disc *d, progstats *stats) { g_validation_summary.dump_ok = false; g_validation_summary.fail_sector = current_sector; } - if (current_sector == 0xFFFFFFFFU) + if (dumper_was_cancelled(dmp)) + fprintf (stderr, "\nDump cancelled at sector: %u\n", current_sector); + else if (current_sector == 0xFFFFFFFFU) fprintf (stderr, "\nXbox reference dumper failed; see the Xbox-specific message above.\n"); else fprintf (stderr, "\nDump failed at sectors: %u..%u\n", current_sector, current_sector+15); @@ -1751,6 +2698,12 @@ int dologic (disc *d, progstats *stats) { } } + if (dump_attempted) + friidump_native_capture_output(dmp, type_id, false, out, dumper_was_cancelled(dmp), current_sector, disc_get_sectors_no(d)); + else if (friidump_native_report_is_enabled(&g_native_report)) { + friidump_native_report_set_outcome(&g_native_report, "other", "fail"); + friidump_native_report_add_note(&g_native_report, "Dump output could not be prepared; dumping was not attempted."); + } dmp = dumper_destroy (dmp); } else { fprintf (stderr, "No output file for dumping specified, please take a look at the -i, -r, -X and -a options\n"); @@ -1799,7 +2752,7 @@ static int try_all_methods (progstats *stats) { #ifndef WIN32 fprintf (stderr, "Probably you do not have access to the DVD device. Ask the administrator\n" - "to add you to the proper group, or use 'sudo'.\n" + "to add you to the proper device-access group. Do not run FriiDump as root.\n" ); #endif continue; @@ -1826,7 +2779,7 @@ int main (int argc, char *argv[]) { disc *d; progstats stats; double duration; - suseconds_t us; + bool have_duration; int out, ret; unscrambler *u; unscrambler_progress_func pfunc; @@ -1834,6 +2787,7 @@ int main (int argc, char *argv[]) { /* First of all... */ drop_euid (); + friidump_native_report_init(&g_native_report, PACKAGE_VERSION); friidump_init_executable_dir((argc > 0) ? argv[0] : NULL); xbox_ref_log_open_for_target(NULL, 0); @@ -1844,6 +2798,7 @@ int main (int argc, char *argv[]) { out = false; ret = EXIT_FAILURE; if (optparse (argc, argv)) { + friidump_native_configure(); friidump_retarget_log_from_options(); if (options.device) { if (options.allmethods) { @@ -1857,11 +2812,12 @@ int main (int argc, char *argv[]) { #ifndef WIN32 fprintf (stderr, "Probably you do not have access to the DVD device. Ask the administrator\n" - "to add you to the proper group, or use 'sudo'.\n" + "to add you to the proper device-access group. Do not run FriiDump as root.\n" ); #endif } else { fprintf (stderr, "OK\n"); + friidump_native_set_drive(d); out = dologic (d, &stats); d = disc_destroy (d); } @@ -1885,29 +2841,25 @@ int main (int argc, char *argv[]) { MY_ASSERT (0); } - if (out) { - duration = stats.end_time.tv_sec - stats.start_time.tv_sec; - if (stats.end_time.tv_usec >= stats.start_time.tv_usec) { - us = stats.end_time.tv_usec - stats.start_time.tv_usec; - } else { - if (duration > 0) - duration--; - us = USECS_PER_SEC + stats.end_time.tv_usec - stats.start_time.tv_usec; - } - duration += ((double) us / (double) USECS_PER_SEC); - if (duration < 0) - duration = 0; - if (g_operation_duration_override_valid) - duration = g_operation_duration_override; - fprintf (stderr, "Operation took %.2f seconds\n", duration); - friidump_print_validation_summary(duration, true); + duration = 0.0; + have_duration = friidump_elapsed_from_stats(&stats, &duration); + if (g_operation_duration_override_valid) { + duration = g_operation_duration_override; + have_duration = true; + } + if (out) { + if (have_duration) + fprintf (stderr, "Operation took %.2f seconds\n", duration); + friidump_print_validation_summary(duration, have_duration); ret = EXIT_SUCCESS; } else { - friidump_print_validation_summary(0.0, false); + friidump_print_validation_summary(duration, have_duration); ret = EXIT_FAILURE; } - + + friidump_native_publish(duration, have_duration); + my_free (options.device); my_free (options.iso_out); my_free (options.xiso_out); @@ -1916,6 +2868,9 @@ int main (int argc, char *argv[]) { my_free (options.hlds_profile_report); my_free (options.xgd1_layout_probe_report); my_free (options.xgd1_raw_id_probe_report); + my_free (options.native_report_json); + my_free (options.native_report_dir); + my_free (options.firmware_modified_note); } if (xbox_ref_log_path()) diff --git a/src/linux_rawio.c b/src/linux_rawio.c new file mode 100644 index 0000000..c621096 --- /dev/null +++ b/src/linux_rawio.c @@ -0,0 +1,69 @@ +#include "linux_rawio.h" + +#include +#include +#include +#include +#include + +friidump_linux_rawio_state friidump_linux_rawio_state_from_status_file( + const char *status_path, + unsigned long long *effective_mask) { + FILE *status; + char line[256]; + unsigned long long value; + char *end; + + if (effective_mask) + *effective_mask = 0; + + if (!status_path || !status_path[0]) + return FRIIDUMP_LINUX_RAWIO_UNKNOWN; + + status = fopen(status_path, "rb"); + if (!status) + return FRIIDUMP_LINUX_RAWIO_UNKNOWN; + + while (fgets(line, sizeof(line), status)) { + if (strncmp(line, "CapEff:", 7) != 0) + continue; + + errno = 0; + value = strtoull(line + 7, &end, 16); + fclose(status); + + if (errno != 0 || end == line + 7) + return FRIIDUMP_LINUX_RAWIO_UNKNOWN; + + if (effective_mask) + *effective_mask = value; + + return (value & (1ULL << FRIIDUMP_LINUX_CAP_SYS_RAWIO)) + ? FRIIDUMP_LINUX_RAWIO_PRESENT + : FRIIDUMP_LINUX_RAWIO_MISSING; + } + + fclose(status); + return FRIIDUMP_LINUX_RAWIO_UNKNOWN; +} + +friidump_linux_rawio_state friidump_linux_rawio_effective( + unsigned long long *effective_mask) { + return friidump_linux_rawio_state_from_status_file( + "/proc/self/status", effective_mask); +} + +int friidump_linux_executable_path(char *buffer, size_t buffer_size) { + ssize_t length; + + if (!buffer || buffer_size < 2) + return 0; + + buffer[0] = '\0'; + length = readlink("/proc/self/exe", buffer, buffer_size - 1); + if (length <= 0 || (size_t) length >= buffer_size) + return 0; + + buffer[length] = '\0'; + return 1; +} diff --git a/src/linux_rawio.h b/src/linux_rawio.h new file mode 100644 index 0000000..07c610e --- /dev/null +++ b/src/linux_rawio.h @@ -0,0 +1,31 @@ +#ifndef FRIIDUMP_LINUX_RAWIO_H_INCLUDED +#define FRIIDUMP_LINUX_RAWIO_H_INCLUDED + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define FRIIDUMP_LINUX_CAP_SYS_RAWIO 17 + +typedef enum { + FRIIDUMP_LINUX_RAWIO_UNKNOWN = -1, + FRIIDUMP_LINUX_RAWIO_MISSING = 0, + FRIIDUMP_LINUX_RAWIO_PRESENT = 1 +} friidump_linux_rawio_state; + +friidump_linux_rawio_state friidump_linux_rawio_state_from_status_file( + const char *status_path, + unsigned long long *effective_mask); + +friidump_linux_rawio_state friidump_linux_rawio_effective( + unsigned long long *effective_mask); + +int friidump_linux_executable_path(char *buffer, size_t buffer_size); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/native_report.c b/src/native_report.c new file mode 100644 index 0000000..1adac1a --- /dev/null +++ b/src/native_report.c @@ -0,0 +1,1276 @@ +#include "native_report.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +#include +#include +#include +#include +#define FRIIDUMP_MKDIR(path) _mkdir(path) +#define FRIIDUMP_ACCESS(path) _access((path), 0) +#define FRIIDUMP_FILENO(file) _fileno(file) +#define FRIIDUMP_FSYNC(fd) _commit(fd) +#else +#include +#include +#include +#include +#define FRIIDUMP_MKDIR(path) mkdir((path), 0775) +#define FRIIDUMP_ACCESS(path) access((path), F_OK) +#define FRIIDUMP_FILENO(file) fileno(file) +#define FRIIDUMP_FSYNC(fd) fsync(fd) +#endif + +static void report_copy(char *dst, size_t dst_size, const char *src) { + if (!dst || dst_size == 0) + return; + if (!src) + src = ""; + snprintf(dst, dst_size, "%s", src); +} + +static void report_error(char *dst, size_t dst_size, const char *message) { + report_copy(dst, dst_size, message ? message : "Unknown report error"); +} + +static bool report_valid_hex(const char *value, size_t length) { + size_t i; + if (!value || strlen(value) != length) + return false; + for (i = 0; i < length; i++) { + if (!isxdigit((unsigned char) value[i])) + return false; + } + return true; +} + + +static bool report_string_in_set(const char *value, + const char *const *values, + size_t value_count) { + size_t i; + + if (!value) + return false; + for (i = 0; i < value_count; i++) { + if (strcmp(value, values[i]) == 0) + return true; + } + return false; +} + +static bool report_valid_uuid(const char *value) { + static const size_t hyphens[] = {8, 13, 18, 23}; + size_t i; + size_t h; + bool all_zero; + + if (!value || strlen(value) != 36) + return false; + + h = 0; + all_zero = true; + for (i = 0; i < 36; i++) { + if (h < sizeof(hyphens) / sizeof(hyphens[0]) && i == hyphens[h]) { + if (value[i] != '-') + return false; + h++; + continue; + } + if (!isxdigit((unsigned char) value[i])) + return false; + if (value[i] != '0') + all_zero = false; + } + + if (all_zero) + return false; + if (value[14] < '1' || value[14] > '5') + return false; + if (value[19] != '8' && value[19] != '9' && + value[19] != 'a' && value[19] != 'A' && + value[19] != 'b' && value[19] != 'B') + return false; + return true; +} + +static bool report_valid_utc_timestamp(const char *value) { + size_t i; + + if (!value || strlen(value) != 20) + return false; + for (i = 0; i < 20; i++) { + if (i == 4 || i == 7) { + if (value[i] != '-') + return false; + } else if (i == 10) { + if (value[i] != 'T') + return false; + } else if (i == 13 || i == 16) { + if (value[i] != ':') + return false; + } else if (i == 19) { + if (value[i] != 'Z') + return false; + } else if (!isdigit((unsigned char) value[i])) { + return false; + } + } + return true; +} + +static void report_utc_now(char *out, size_t out_size) { + time_t now; + struct tm utc_tm; + + if (!out || out_size == 0) + return; + + now = time(NULL); +#ifdef WIN32 + if (gmtime_s(&utc_tm, &now) != 0) { + out[0] = '\0'; + return; + } +#else + if (!gmtime_r(&now, &utc_tm)) { + out[0] = '\0'; + return; + } +#endif + strftime(out, out_size, "%Y-%m-%dT%H:%M:%SZ", &utc_tm); +} + +#ifndef WIN32 +static bool report_random_bytes(unsigned char *bytes, size_t length) { + int fd; + size_t total; + ssize_t got; + + fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) + return false; + + total = 0; + while (total < length) { + got = read(fd, bytes + total, length - total); + if (got <= 0) { + close(fd); + return false; + } + total += (size_t) got; + } + close(fd); + return true; +} +#endif + +static bool report_uuid(char out[37]) { +#ifdef WIN32 + GUID guid; + HRESULT status; + + status = CoCreateGuid(&guid); + if (FAILED(status)) + return false; + + snprintf(out, 37, + "%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x", + (unsigned long) guid.Data1, + (unsigned int) guid.Data2, + (unsigned int) guid.Data3, + (unsigned int) guid.Data4[0], + (unsigned int) guid.Data4[1], + (unsigned int) guid.Data4[2], + (unsigned int) guid.Data4[3], + (unsigned int) guid.Data4[4], + (unsigned int) guid.Data4[5], + (unsigned int) guid.Data4[6], + (unsigned int) guid.Data4[7]); + return true; +#else + unsigned char b[16]; + + if (!report_random_bytes(b, sizeof(b))) + return false; + + b[6] = (unsigned char) ((b[6] & 0x0fU) | 0x40U); + b[8] = (unsigned char) ((b[8] & 0x3fU) | 0x80U); + + snprintf(out, 37, + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]); + return true; +#endif +} + +static bool report_directory_exists(const char *path) { + struct stat st; + if (!path || !path[0]) + return false; + if (stat(path, &st) != 0) + return false; +#ifdef WIN32 + return (st.st_mode & _S_IFDIR) != 0; +#else + return S_ISDIR(st.st_mode); +#endif +} + +static bool report_make_directory(const char *path) { + char tmp[FRIIDUMP_REPORT_PATH_MAX]; + size_t i; + + if (!path || !path[0]) + return false; + if (report_directory_exists(path)) + return true; + + report_copy(tmp, sizeof(tmp), path); + for (i = 1; tmp[i]; i++) { + if (tmp[i] != '/' && tmp[i] != '\\') + continue; +#ifdef WIN32 + if (i == 2 && tmp[1] == ':') + continue; +#endif + { + char saved = tmp[i]; + tmp[i] = '\0'; + if (tmp[0] && !report_directory_exists(tmp)) { + if (FRIIDUMP_MKDIR(tmp) != 0 && errno != EEXIST) + return false; + } + tmp[i] = saved; + } + } + + if (!report_directory_exists(tmp)) { + if (FRIIDUMP_MKDIR(tmp) != 0 && errno != EEXIST) + return false; + } + return report_directory_exists(tmp); +} + +static char report_path_separator(const char *directory) { +#ifdef WIN32 + (void) directory; + return '\\'; +#else + (void) directory; + return '/'; +#endif +} + +static const char *report_path_leaf(const char *path) { + const char *slash; + const char *backslash; + const char *leaf; + + if (!path) + return ""; + slash = strrchr(path, '/'); + backslash = strrchr(path, '\\'); + leaf = path; + if (slash && slash + 1 > leaf) + leaf = slash + 1; + if (backslash && backslash + 1 > leaf) + leaf = backslash + 1; + return leaf; +} + +static bool report_path_parent(const char *path, + char *output, + size_t output_size) { + const char *leaf; + size_t length; + + if (!path || !path[0] || !output || output_size == 0) + return false; + + leaf = report_path_leaf(path); + if (leaf == path) + return false; + + /* Keep the final separator. This preserves POSIX root paths, Windows + * drive roots, UNC paths, and relative subdirectories without needing + * platform-specific special cases. report_join_path() accepts a trailing + * separator and will not add another one. */ + length = (size_t) (leaf - path); + if (length == 0 || length >= output_size) + return false; + + memcpy(output, path, length); + output[length] = '\0'; + return true; +} + +static bool report_ends_with_case_insensitive(const char *value, + const char *suffix) { + size_t value_length; + size_t suffix_length; + size_t i; + + if (!value || !suffix) + return false; + value_length = strlen(value); + suffix_length = strlen(suffix); + if (suffix_length > value_length) + return false; + value += value_length - suffix_length; + for (i = 0; i < suffix_length; i++) { + if (tolower((unsigned char) value[i]) != + tolower((unsigned char) suffix[i])) + return false; + } + return true; +} + +static bool report_join_path(const char *directory, + const char *leaf, + char *output, + size_t output_size) { + size_t directory_length; + char separator; + + if (!directory || !directory[0] || !leaf || !leaf[0] || + !output || output_size == 0) + return false; + + directory_length = strlen(directory); + separator = report_path_separator(directory); + return snprintf(output, output_size, + "%s%s%s", + directory, + (directory_length > 0 && + directory[directory_length - 1] != '/' && + directory[directory_length - 1] != '\\') + ? (separator == '\\' ? "\\" : "/") + : "", + leaf) < (int) output_size; +} + +static void report_sanitize_leaf(const char *input, char *output, size_t output_size) { + size_t used; + bool previous_separator; + unsigned char c; + + if (!output || output_size == 0) + return; + output[0] = '\0'; + if (!input) + return; + + used = 0; + previous_separator = false; + while (*input && used + 1 < output_size && used < 96) { + c = (unsigned char) *input++; + if (isalnum(c) || c == '-' || c == '_') { + output[used++] = (char) c; + previous_separator = false; + } else if (c == ' ' || c == '.' || c == '+' || c == '(' || c == ')' || c == '[' || c == ']') { + if (!previous_separator && used > 0) { + output[used++] = '_'; + previous_separator = true; + } + } + } + while (used > 0 && output[used - 1] == '_') + used--; + output[used] = '\0'; +} + +static bool report_derived_leaf(const friidump_native_report *report, + char *leaf, + size_t leaf_size, + char *error_text, + size_t error_text_size) { + const char *basis; + char fallback[128]; + size_t length; + + if (!report || !leaf || leaf_size == 0) + return false; + leaf[0] = '\0'; + + if (report->explicit_path) { + basis = report_path_leaf(report->requested_path); + if (!basis[0]) { + report_error(error_text, error_text_size, + "Native report pathname has no filename"); + return false; + } + if (strlen(basis) >= leaf_size) { + report_error(error_text, error_text_size, + "Native report filename is too long"); + return false; + } + report_copy(leaf, leaf_size, basis); + return true; + } + + basis = report_path_leaf(report->name_basis); + if (basis[0]) { + if (strlen(basis) >= leaf_size) { + report_error(error_text, error_text_size, + "Derived native report filename is too long"); + return false; + } + report_copy(leaf, leaf_size, basis); + if (report_ends_with_case_insensitive(leaf, ".log")) + leaf[strlen(leaf) - 4] = '\0'; + if (!leaf[0]) + report_copy(leaf, leaf_size, "friidump"); + } else { + basis = report->have_title + ? report->title + : (report->drive_known ? report->model : "friidump"); + report_sanitize_leaf(basis, fallback, sizeof(fallback)); + report_copy(leaf, leaf_size, fallback[0] ? fallback : "friidump"); + } + + length = strlen(leaf); + if (length + strlen(".friidump.json") >= leaf_size) { + report_error(error_text, error_text_size, + "Derived native report filename is too long"); + return false; + } + strcat(leaf, ".friidump.json"); + return true; +} + +static bool report_collision_path(const char *path, + const char *run_id, + char *output, + size_t output_size) { + static const char suffix[] = ".friidump.json"; + size_t path_length; + size_t stem_length; + + if (!path || !path[0] || !run_id || !run_id[0] || + !output || output_size == 0) + return false; + + path_length = strlen(path); + stem_length = path_length; + if (report_ends_with_case_insensitive(path, suffix)) + stem_length -= strlen(suffix); + + if (stem_length + 1 + strlen(run_id) + strlen(suffix) + 1 > output_size) + return false; + + memcpy(output, path, stem_length); + output[stem_length] = '\0'; + strcat(output, "-"); + strcat(output, run_id); + strcat(output, suffix); + return true; +} + +static bool report_resolve_path(friidump_native_report *report, + char *error_text, + size_t error_text_size) { + char leaf[FRIIDUMP_REPORT_PATH_MAX]; + char basis_directory[FRIIDUMP_REPORT_PATH_MAX]; + + if (!report_derived_leaf(report, leaf, sizeof(leaf), + error_text, error_text_size)) + return false; + + if (report->explicit_path && !report->directory_override) { + report_copy(report->final_path, sizeof(report->final_path), + report->requested_path); + return report->final_path[0] != '\0'; + } + + if (report->directory_override) { + if (!report_make_directory(report->report_dir)) { + report_error(error_text, error_text_size, + "Could not create or access report directory"); + return false; + } + if (!report_join_path(report->report_dir, leaf, + report->final_path, + sizeof(report->final_path))) { + report_error(error_text, error_text_size, + "Generated report path is too long"); + return false; + } + return true; + } + + /* By default, keep the report beside the final FriiDump log (or the + * output path used as its fallback naming basis). A basename-only log + * still resolves in the current working directory. */ + if (report_path_parent(report->name_basis, + basis_directory, + sizeof(basis_directory))) { + if (!report_join_path(basis_directory, leaf, + report->final_path, + sizeof(report->final_path))) { + report_error(error_text, error_text_size, + "Generated report path is too long"); + return false; + } + return true; + } + + report_copy(report->final_path, sizeof(report->final_path), leaf); + return report->final_path[0] != '\0'; +} + +static size_t report_utf8_sequence_length(const unsigned char *p) { + if (!p || p[0] == 0) + return 0; + if (p[0] < 0x80) + return 1; + if (p[0] >= 0xc2 && p[0] <= 0xdf && + p[1] >= 0x80 && p[1] <= 0xbf) + return 2; + if (p[0] == 0xe0 && + p[1] >= 0xa0 && p[1] <= 0xbf && + p[2] >= 0x80 && p[2] <= 0xbf) + return 3; + if (((p[0] >= 0xe1 && p[0] <= 0xec) || + (p[0] >= 0xee && p[0] <= 0xef)) && + p[1] >= 0x80 && p[1] <= 0xbf && + p[2] >= 0x80 && p[2] <= 0xbf) + return 3; + if (p[0] == 0xed && + p[1] >= 0x80 && p[1] <= 0x9f && + p[2] >= 0x80 && p[2] <= 0xbf) + return 3; + if (p[0] == 0xf0 && + p[1] >= 0x90 && p[1] <= 0xbf && + p[2] >= 0x80 && p[2] <= 0xbf && + p[3] >= 0x80 && p[3] <= 0xbf) + return 4; + if (p[0] >= 0xf1 && p[0] <= 0xf3 && + p[1] >= 0x80 && p[1] <= 0xbf && + p[2] >= 0x80 && p[2] <= 0xbf && + p[3] >= 0x80 && p[3] <= 0xbf) + return 4; + if (p[0] == 0xf4 && + p[1] >= 0x80 && p[1] <= 0x8f && + p[2] >= 0x80 && p[2] <= 0xbf && + p[3] >= 0x80 && p[3] <= 0xbf) + return 4; + return 0; +} + +static void report_json_string(FILE *f, const char *value) { + const unsigned char *p; + size_t utf8_length; + + fputc('"', f); + if (value) { + for (p = (const unsigned char *) value; *p; p++) { + if (*p >= 0x80) { + utf8_length = report_utf8_sequence_length(p); + if (utf8_length > 0) { + fwrite(p, 1, utf8_length, f); + p += utf8_length - 1; + } else { + fprintf(f, "\\u%04x", (unsigned int) *p); + } + continue; + } + switch (*p) { + case '\\': fputs("\\\\", f); break; + case '"': fputs("\\\"", f); break; + case '\b': fputs("\\b", f); break; + case '\f': fputs("\\f", f); break; + case '\n': fputs("\\n", f); break; + case '\r': fputs("\\r", f); break; + case '\t': fputs("\\t", f); break; + default: + if (*p < 0x20) + fprintf(f, "\\u%04x", (unsigned int) *p); + else + fputc(*p, f); + break; + } + } + } + fputc('"', f); +} + +static void report_json_nullable_string(FILE *f, const char *value, bool present) { + if (present) + report_json_string(f, value); + else + fputs("null", f); +} + +static void report_json_nullable_u64(FILE *f, uint64_t value, bool present) { + if (present) + fprintf(f, "%llu", (unsigned long long) value); + else + fputs("null", f); +} + +static void report_json_nullable_double(FILE *f, double value, bool present) { + if (present) + fprintf(f, "%.3f", value < 0.0 ? 0.0 : value); + else + fputs("null", f); +} + +static bool report_write_document(FILE *f, const friidump_native_report *r) { + size_t i; + const friidump_native_artifact *artifact; + + fputs("{\n", f); + fputs(" \"schema\": \"friidump-test-result.v1\",\n", f); + fputs(" \"format\": {\n", f); + fputs(" \"name\": \"FriiDump Interchange Format\",\n", f); + fputs(" \"generation\": 1,\n", f); + fputs(" \"serialization\": \"json\"\n", f); + fputs(" },\n", f); + fputs(" \"generated_utc\": ", f); report_json_string(f, r->generated_utc); fputs(",\n", f); + fputs(" \"generator\": {\n", f); + fputs(" \"name\": \"FriiDump\",\n", f); + fputs(" \"version\": ", f); report_json_string(f, r->generator_version); fputs(",\n", f); + fputs(" \"build_commit\": ", f); report_json_nullable_string(f, r->build_commit, r->build_commit_known); fputs("\n", f); + fputs(" },\n", f); + fputs(" \"run\": {\n", f); + fputs(" \"run_id\": ", f); report_json_string(f, r->run_id); fputs(",\n", f); + fputs(" \"started_utc\": ", f); report_json_nullable_string(f, r->started_utc, r->started_utc[0] != '\0'); fputs(",\n", f); + fputs(" \"completed_utc\": ", f); report_json_nullable_string(f, r->completed_utc, r->completed_utc[0] != '\0'); fputs(",\n", f); + fputs(" \"test_type\": ", f); report_json_string(f, r->test_type); fputs(",\n", f); + fputs(" \"result\": ", f); report_json_string(f, r->run_result); fputs("\n", f); + fputs(" },\n", f); + fputs(" \"drive\": {\n", f); + fputs(" \"vendor\": ", f); report_json_string(f, r->vendor); fputs(",\n", f); + fputs(" \"model\": ", f); report_json_string(f, r->model); fputs(",\n", f); + fputs(" \"firmware_revision\": ", f); report_json_string(f, r->firmware_revision); fputs(",\n", f); + fputs(" \"interface\": ", f); report_json_string(f, r->interface_name); fputs(",\n", f); + fputs(" \"device_path\": ", f); report_json_nullable_string(f, r->device_path, r->have_device_path); fputs(",\n", f); + fprintf(f, " \"firmware_modified\": %s,\n", r->firmware_modified ? "true" : "false"); + fputs(" \"modification_note\": ", f); report_json_nullable_string(f, r->modification_note, r->have_modification_note); fputs("\n", f); + fputs(" },\n", f); + fputs(" \"media\": {\n", f); + fputs(" \"platform\": ", f); report_json_string(f, r->platform); fputs(",\n", f); + fputs(" \"title\": ", f); report_json_nullable_string(f, r->title, r->have_title); fputs(",\n", f); + fputs(" \"region\": ", f); report_json_nullable_string(f, r->region, r->have_region); fputs(",\n", f); + fputs(" \"disc_id\": ", f); report_json_nullable_string(f, r->disc_id, r->have_disc_id); fputs("\n", f); + fputs(" },\n", f); + fputs(" \"seed\": {\n", f); + fprintf(f, " \"attempted\": %s,\n", r->seed_attempted ? "true" : "false"); + fputs(" \"result\": ", f); report_json_string(f, r->seed_result); fputs(",\n", f); + fputs(" \"duration_seconds\": ", f); report_json_nullable_double(f, r->seed_duration_seconds, r->have_seed_duration); fputs("\n", f); + fputs(" },\n", f); + fputs(" \"dump\": {\n", f); + fprintf(f, " \"attempted\": %s,\n", r->dump_attempted ? "true" : "false"); + fputs(" \"result\": ", f); report_json_string(f, r->dump_result); fputs(",\n", f); + fputs(" \"sector_count\": ", f); report_json_nullable_u64(f, r->sector_count, r->have_sector_count); fputs(",\n", f); + fputs(" \"byte_count\": ", f); report_json_nullable_u64(f, r->byte_count, r->have_byte_count); fputs(",\n", f); + fputs(" \"duration_seconds\": ", f); report_json_nullable_double(f, r->dump_duration_seconds, r->have_dump_duration); fputs(",\n", f); + fputs(" \"failure_stage\": ", f); report_json_nullable_string(f, r->failure_stage, r->have_failure_stage); fputs(",\n", f); + fputs(" \"failure_sector\": ", f); report_json_nullable_u64(f, r->failure_sector, r->have_failure_sector); fputs(",\n", f); + fputs(" \"output_path\": ", f); report_json_nullable_string(f, r->output_path, r->have_output_path); fputs("\n", f); + fputs(" },\n", f); + fputs(" \"hashes\": {\n", f); + fputs(" \"crc32\": ", f); report_json_nullable_string(f, r->crc32, report_valid_hex(r->crc32, 8)); fputs(",\n", f); + fputs(" \"md5\": ", f); report_json_nullable_string(f, r->md5, report_valid_hex(r->md5, 32)); fputs(",\n", f); + fputs(" \"sha1\": ", f); report_json_nullable_string(f, r->sha1, report_valid_hex(r->sha1, 40)); fputs(",\n", f); + fputs(" \"sha256\": ", f); report_json_nullable_string(f, r->sha256, report_valid_hex(r->sha256, 64)); fputs("\n", f); + fputs(" },\n", f); + fputs(" \"reference\": {\n", f); + fputs(" \"provider\": ", f); report_json_nullable_string(f, r->reference_provider, r->have_reference_provider); fputs(",\n", f); + fputs(" \"result\": ", f); report_json_string(f, r->reference_result); fputs(",\n", f); + fputs(" \"reference_id\": ", f); report_json_nullable_string(f, r->reference_id, r->have_reference_id); fputs("\n", f); + fputs(" },\n", f); + fputs(" \"profile\": {\n", f); + fputs(" \"support_tier\": ", f); report_json_nullable_string(f, r->support_tier, r->have_support_tier); fputs(",\n", f); + fputs(" \"cdb_offset\": ", f); report_json_nullable_string(f, r->cdb_offset, r->have_cdb_offset); fputs(",\n", f); + fputs(" \"gate_address\": ", f); report_json_nullable_string(f, r->gate_address, r->have_gate_address); fputs("\n", f); + fputs(" },\n", f); + fputs(" \"notes\": [", f); + if (r->note_count > 0) + fputc('\n', f); + for (i = 0; i < r->note_count; i++) { + fputs(" ", f); + report_json_string(f, r->notes[i]); + fputs((i + 1 < r->note_count) ? ",\n" : "\n", f); + } + fputs(r->note_count > 0 ? " ],\n" : "],\n", f); + fputs(" \"artifacts\": [", f); + if (r->artifact_count > 0) + fputc('\n', f); + for (i = 0; i < r->artifact_count; i++) { + artifact = &r->artifacts[i]; + fputs(" {\n", f); + fputs(" \"type\": ", f); report_json_string(f, artifact->type); fputs(",\n", f); + fputs(" \"path\": ", f); report_json_nullable_string(f, artifact->path, artifact->have_path); fputs(",\n", f); + fputs(" \"bytes\": ", f); report_json_nullable_u64(f, artifact->bytes, artifact->have_bytes); fputs(",\n", f); + fputs(" \"sha256\": ", f); report_json_nullable_string(f, artifact->sha256, artifact->have_sha256); fputs("\n", f); + fputs((i + 1 < r->artifact_count) ? " },\n" : " }\n", f); + } + fputs(r->artifact_count > 0 ? " ]\n" : "]\n", f); + fputs("}\n", f); + + return ferror(f) == 0; +} + +void friidump_native_report_init(friidump_native_report *report, const char *generator_version) { + if (!report) + return; + memset(report, 0, sizeof(*report)); + report_copy(report->generator_version, sizeof(report->generator_version), generator_version ? generator_version : "unknown"); + report_copy(report->test_type, sizeof(report->test_type), "other"); + report_copy(report->run_result, sizeof(report->run_result), "fail"); + report_copy(report->platform, sizeof(report->platform), "unknown"); + report_copy(report->seed_result, sizeof(report->seed_result), "not_attempted"); + report_copy(report->dump_result, sizeof(report->dump_result), "not_attempted"); + report_copy(report->reference_result, sizeof(report->reference_result), "not_checked"); + report_utc_now(report->started_utc, sizeof(report->started_utc)); + if (!report_uuid(report->run_id)) + report->run_id[0] = '\0'; +} + +bool friidump_native_report_enable_default(friidump_native_report *report) { + if (!report) + return false; + report->enabled = true; + return true; +} + +bool friidump_native_report_enable_path(friidump_native_report *report, const char *path) { + if (!report || !path || !path[0] || strlen(path) >= sizeof(report->requested_path)) + return false; + report->enabled = true; + report->explicit_path = true; + report_copy(report->requested_path, sizeof(report->requested_path), path); + return true; +} + +bool friidump_native_report_enable_dir(friidump_native_report *report, const char *directory) { + if (!report || !directory || !directory[0] || strlen(directory) >= sizeof(report->report_dir)) + return false; + report->enabled = true; + report->directory_override = true; + report_copy(report->report_dir, sizeof(report->report_dir), directory); + return true; +} + +bool friidump_native_report_set_name_basis(friidump_native_report *report, + const char *path) { + if (!report || !path || !path[0] || strlen(path) >= sizeof(report->name_basis)) + return false; + report_copy(report->name_basis, sizeof(report->name_basis), path); + return true; +} + +bool friidump_native_report_is_enabled(const friidump_native_report *report) { + return report && report->enabled; +} + +void friidump_native_report_set_build_commit(friidump_native_report *report, const char *commit) { + if (!report) + return; + report->build_commit_known = report_valid_hex(commit, 40); + report_copy(report->build_commit, sizeof(report->build_commit), report->build_commit_known ? commit : ""); +} + +void friidump_native_report_set_drive(friidump_native_report *report, + const char *vendor, + const char *model, + const char *firmware_revision, + const char *interface_name, + const char *device_path) { + if (!report) + return; + report_copy(report->vendor, sizeof(report->vendor), vendor ? vendor : ""); + report_copy(report->model, sizeof(report->model), model ? model : ""); + report_copy(report->firmware_revision, sizeof(report->firmware_revision), firmware_revision ? firmware_revision : ""); + report_copy(report->interface_name, sizeof(report->interface_name), interface_name ? interface_name : ""); + report->have_device_path = device_path && device_path[0]; + report_copy(report->device_path, sizeof(report->device_path), report->have_device_path ? device_path : ""); + report->drive_known = report->model[0] != '\0' && report->firmware_revision[0] != '\0'; +} + +void friidump_native_report_set_firmware_modified(friidump_native_report *report, + bool modified, + const char *note) { + if (!report) + return; + report->firmware_modified = modified; + report->have_modification_note = note && note[0] && strlen(note) <= 1000; + report_copy(report->modification_note, sizeof(report->modification_note), report->have_modification_note ? note : ""); +} + +void friidump_native_report_set_media(friidump_native_report *report, + const char *platform, + const char *title, + const char *region, + const char *disc_id) { + if (!report) + return; + report_copy(report->platform, sizeof(report->platform), platform && platform[0] ? platform : "unknown"); + report->have_title = title && title[0]; + report_copy(report->title, sizeof(report->title), report->have_title ? title : ""); + report->have_region = region && region[0]; + report_copy(report->region, sizeof(report->region), report->have_region ? region : ""); + report->have_disc_id = disc_id && disc_id[0]; + report_copy(report->disc_id, sizeof(report->disc_id), report->have_disc_id ? disc_id : ""); +} + +void friidump_native_report_set_profile(friidump_native_report *report, + const char *support_tier, + uint32_t cdb_offset, + uint32_t gate_address) { + if (!report) + return; + report->have_support_tier = support_tier && support_tier[0] && strcmp(support_tier, "none") != 0; + report_copy(report->support_tier, sizeof(report->support_tier), report->have_support_tier ? support_tier : ""); + report->have_cdb_offset = cdb_offset != 0; + report->have_gate_address = gate_address != 0; + if (report->have_cdb_offset) + snprintf(report->cdb_offset, sizeof(report->cdb_offset), "0x%x", (unsigned int) cdb_offset); + if (report->have_gate_address) + snprintf(report->gate_address, sizeof(report->gate_address), "0x%x", (unsigned int) gate_address); +} + +void friidump_native_report_set_seed(friidump_native_report *report, + bool attempted, + const char *result, + bool have_duration, + double duration_seconds) { + if (!report) + return; + report->seed_attempted = attempted; + report_copy(report->seed_result, sizeof(report->seed_result), result ? result : "not_attempted"); + report->have_seed_duration = have_duration; + report->seed_duration_seconds = duration_seconds; +} + +void friidump_native_report_set_dump(friidump_native_report *report, + bool attempted, + const char *result, + bool have_sector_count, + uint64_t sector_count, + bool have_byte_count, + uint64_t byte_count, + bool have_duration, + double duration_seconds, + const char *failure_stage, + bool have_failure_sector, + uint64_t failure_sector, + const char *output_path) { + if (!report) + return; + report->dump_attempted = attempted; + report_copy(report->dump_result, sizeof(report->dump_result), result ? result : "not_attempted"); + report->have_sector_count = have_sector_count; + report->sector_count = sector_count; + report->have_byte_count = have_byte_count; + report->byte_count = byte_count; + report->have_dump_duration = have_duration; + report->dump_duration_seconds = duration_seconds; + report->have_failure_stage = failure_stage && failure_stage[0]; + report_copy(report->failure_stage, sizeof(report->failure_stage), report->have_failure_stage ? failure_stage : ""); + report->have_failure_sector = have_failure_sector; + report->failure_sector = failure_sector; + report->have_output_path = output_path && output_path[0]; + report_copy(report->output_path, sizeof(report->output_path), report->have_output_path ? output_path : ""); +} + +void friidump_native_report_set_hashes(friidump_native_report *report, + const char *crc32, + const char *md5, + const char *sha1, + const char *sha256) { + if (!report) + return; + report_copy(report->crc32, sizeof(report->crc32), crc32 ? crc32 : ""); + report_copy(report->md5, sizeof(report->md5), md5 ? md5 : ""); + report_copy(report->sha1, sizeof(report->sha1), sha1 ? sha1 : ""); + report_copy(report->sha256, sizeof(report->sha256), sha256 ? sha256 : ""); +} + +void friidump_native_report_set_reference(friidump_native_report *report, + const char *provider, + const char *result, + const char *reference_id) { + if (!report) + return; + report->have_reference_provider = provider && provider[0]; + report_copy(report->reference_provider, sizeof(report->reference_provider), report->have_reference_provider ? provider : ""); + report_copy(report->reference_result, sizeof(report->reference_result), result ? result : "not_checked"); + report->have_reference_id = reference_id && reference_id[0]; + report_copy(report->reference_id, sizeof(report->reference_id), report->have_reference_id ? reference_id : ""); +} + +void friidump_native_report_set_outcome(friidump_native_report *report, + const char *test_type, + const char *run_result) { + if (!report) + return; + report_copy(report->test_type, sizeof(report->test_type), test_type ? test_type : "other"); + report_copy(report->run_result, sizeof(report->run_result), run_result ? run_result : "fail"); +} + +bool friidump_native_report_add_note(friidump_native_report *report, const char *note) { + if (!report || !note || !note[0] || + strlen(note) > FRIIDUMP_REPORT_TEXT_MAX || + report->note_count >= FRIIDUMP_REPORT_NOTE_MAX) + return false; + report_copy(report->notes[report->note_count], sizeof(report->notes[report->note_count]), note); + report->note_count++; + return true; +} + +bool friidump_native_report_add_artifact(friidump_native_report *report, + const char *type, + const char *path, + bool have_bytes, + uint64_t bytes, + const char *sha256) { + friidump_native_artifact *artifact; + + if (!report || !type || !type[0] || strlen(type) > 80 || + report->artifact_count >= FRIIDUMP_REPORT_ARTIFACT_MAX) + return false; + if (path && strlen(path) > FRIIDUMP_REPORT_PATH_MAX) + return false; + if (sha256 && sha256[0] && !report_valid_hex(sha256, 64)) + return false; + + artifact = &report->artifacts[report->artifact_count]; + memset(artifact, 0, sizeof(*artifact)); + report_copy(artifact->type, sizeof(artifact->type), type); + artifact->have_path = path && path[0]; + report_copy(artifact->path, sizeof(artifact->path), artifact->have_path ? path : ""); + artifact->have_bytes = have_bytes; + artifact->bytes = bytes; + artifact->have_sha256 = sha256 && sha256[0]; + report_copy(artifact->sha256, sizeof(artifact->sha256), artifact->have_sha256 ? sha256 : ""); + report->artifact_count++; + return true; +} + + +static bool report_validate_for_write(const friidump_native_report *report, + char *error_text, + size_t error_text_size) { + static const char *const platforms[] = { + "gamecube", "wii", "xbox", "dvd", "unknown" + }; + static const char *const test_types[] = { + "full_dump", "seed_only", "partial_dump", "diagnostic_no_media", + "diagnostic_wrong_media", "media_transition", "other" + }; + static const char *const run_results[] = { + "pass", "partial", "fail", "not_applicable" + }; + static const char *const step_results[] = { + "pass", "partial", "fail", "not_attempted", "not_applicable" + }; + static const char *const reference_results[] = { + "match", "mismatch", "not_checked", "not_applicable" + }; + + if (!report->generator_version[0]) { + report_error(error_text, error_text_size, "Generator version is empty"); + return false; + } + if (!report_valid_uuid(report->run_id)) { + report_error(error_text, error_text_size, "Run UUID is missing or invalid"); + return false; + } + if (!report_valid_utc_timestamp(report->started_utc) || + !report_valid_utc_timestamp(report->completed_utc) || + !report_valid_utc_timestamp(report->generated_utc)) { + report_error(error_text, error_text_size, "Report timestamps are missing or invalid"); + return false; + } + if (strcmp(report->completed_utc, report->started_utc) < 0) { + report_error(error_text, error_text_size, "Completion timestamp precedes start timestamp"); + return false; + } + if (!report->drive_known) { + report_error(error_text, error_text_size, + "Drive identity is incomplete; no database-compatible report was written"); + return false; + } + if (report->firmware_modified && !report->have_modification_note) { + report_error(error_text, error_text_size, + "Modified firmware requires a modification note"); + return false; + } + if (!report_string_in_set(report->platform, platforms, + sizeof(platforms) / sizeof(platforms[0])) || + !report_string_in_set(report->test_type, test_types, + sizeof(test_types) / sizeof(test_types[0])) || + !report_string_in_set(report->run_result, run_results, + sizeof(run_results) / sizeof(run_results[0])) || + !report_string_in_set(report->seed_result, step_results, + sizeof(step_results) / sizeof(step_results[0])) || + !report_string_in_set(report->dump_result, step_results, + sizeof(step_results) / sizeof(step_results[0])) || + !report_string_in_set(report->reference_result, reference_results, + sizeof(reference_results) / sizeof(reference_results[0]))) { + report_error(error_text, error_text_size, "One or more report enum values are invalid"); + return false; + } + if ((report->have_seed_duration && + (!isfinite(report->seed_duration_seconds) || report->seed_duration_seconds < 0.0)) || + (report->have_dump_duration && + (!isfinite(report->dump_duration_seconds) || report->dump_duration_seconds < 0.0))) { + report_error(error_text, error_text_size, "A report duration is negative or non-finite"); + return false; + } + if (report->note_count > FRIIDUMP_REPORT_NOTE_MAX || + report->artifact_count > FRIIDUMP_REPORT_ARTIFACT_MAX) { + report_error(error_text, error_text_size, + "Report note or artifact count exceeds the implementation limit"); + return false; + } + { + size_t i; + for (i = 0; i < report->note_count; i++) { + if (!report->notes[i][0] || + strlen(report->notes[i]) > FRIIDUMP_REPORT_TEXT_MAX) { + report_error(error_text, error_text_size, + "One or more report notes are invalid"); + return false; + } + } + } + if (report->seed_attempted) { + if (strcmp(report->seed_result, "pass") != 0 && + strcmp(report->seed_result, "partial") != 0 && + strcmp(report->seed_result, "fail") != 0) { + report_error(error_text, error_text_size, + "An attempted seed stage requires pass, partial, or fail"); + return false; + } + } else { + if ((strcmp(report->seed_result, "not_attempted") != 0 && + strcmp(report->seed_result, "not_applicable") != 0) || + report->have_seed_duration) { + report_error(error_text, error_text_size, + "A non-attempted seed stage cannot claim a result or duration"); + return false; + } + } + if (report->dump_attempted) { + if (strcmp(report->dump_result, "pass") != 0 && + strcmp(report->dump_result, "partial") != 0 && + strcmp(report->dump_result, "fail") != 0) { + report_error(error_text, error_text_size, + "An attempted dump requires pass, partial, or fail"); + return false; + } + } else { + if ((strcmp(report->dump_result, "not_attempted") != 0 && + strcmp(report->dump_result, "not_applicable") != 0) || + report->have_sector_count || report->have_byte_count || + report->have_dump_duration || report->have_failure_stage || + report->have_failure_sector || report->have_output_path) { + report_error(error_text, error_text_size, + "A non-attempted dump cannot claim output, progress, failure, or duration"); + return false; + } + } + if (strcmp(report->dump_result, "pass") == 0 && + (strcmp(report->test_type, "full_dump") != 0 || + strcmp(report->run_result, "pass") != 0)) { + report_error(error_text, error_text_size, + "A successful dump requires a full_dump/pass run outcome"); + return false; + } + if (strcmp(report->dump_result, "partial") == 0 && + (strcmp(report->test_type, "partial_dump") != 0 || + strcmp(report->run_result, "partial") != 0)) { + report_error(error_text, error_text_size, + "A partial dump requires a partial_dump/partial run outcome"); + return false; + } + if (strcmp(report->dump_result, "fail") == 0 && + strcmp(report->run_result, "fail") != 0) { + report_error(error_text, error_text_size, + "A failed dump requires a failed run outcome"); + return false; + } + if (strcmp(report->test_type, "seed_only") == 0 && + (!report->seed_attempted || report->dump_attempted)) { + report_error(error_text, error_text_size, + "A seed_only run requires an attempted seed stage and no dump"); + return false; + } + if ((strcmp(report->test_type, "diagnostic_no_media") == 0 || + strcmp(report->test_type, "diagnostic_wrong_media") == 0 || + strcmp(report->test_type, "media_transition") == 0) && + (strcmp(report->run_result, "not_applicable") != 0 || + report->dump_attempted)) { + report_error(error_text, error_text_size, + "Diagnostic and media-transition runs must be not_applicable and must not dump"); + return false; + } + if (strcmp(report->dump_result, "pass") == 0 && + (!report->have_sector_count || !report->have_byte_count)) { + report_error(error_text, error_text_size, + "A successful dump requires sector and byte counts"); + return false; + } + if (strcmp(report->dump_result, "fail") == 0 && !report->have_failure_stage) { + report_error(error_text, error_text_size, + "A failed dump requires a failure stage"); + return false; + } + if (strcmp(report->reference_result, "match") == 0 && + !report->have_reference_provider) { + report_error(error_text, error_text_size, + "A reference match requires a provider"); + return false; + } + { + size_t i; + size_t dump_output_count = 0; + const friidump_native_artifact *artifact; + for (i = 0; i < report->artifact_count; i++) { + artifact = &report->artifacts[i]; + if (!artifact->type[0] || strlen(artifact->type) > 80 || + (artifact->have_path && strlen(artifact->path) > FRIIDUMP_REPORT_PATH_MAX) || + (artifact->have_sha256 && !report_valid_hex(artifact->sha256, 64))) { + report_error(error_text, error_text_size, + "One or more artifact records are invalid"); + return false; + } + if (strcmp(artifact->type, "dump_output") == 0) { + dump_output_count++; + if (!artifact->have_path || !report->have_output_path || + strcmp(artifact->path, report->output_path) != 0 || + (report->have_byte_count && + (!artifact->have_bytes || artifact->bytes != report->byte_count)) || + (report_valid_hex(report->sha256, 64) && + (!artifact->have_sha256 || + strcmp(artifact->sha256, report->sha256) != 0))) { + report_error(error_text, error_text_size, + "The dump_output artifact does not match the reported output"); + return false; + } + } + } + if (report->dump_attempted && report->have_output_path && + dump_output_count != 1) { + report_error(error_text, error_text_size, + "A dump with an output path requires exactly one dump_output artifact"); + return false; + } + } + return true; +} + +bool friidump_native_report_write(friidump_native_report *report, + char *written_path, + size_t written_path_size, + char *error_text, + size_t error_text_size) { + FILE *f; + char temporary_path[FRIIDUMP_REPORT_PATH_MAX + 64]; + int close_status; + + if (written_path && written_path_size > 0) + written_path[0] = '\0'; + if (error_text && error_text_size > 0) + error_text[0] = '\0'; + + if (!report || !report->enabled) { + report_error(error_text, error_text_size, "Native report is not enabled"); + return false; + } + report_utc_now(report->completed_utc, sizeof(report->completed_utc)); + report_utc_now(report->generated_utc, sizeof(report->generated_utc)); + + if (!report_validate_for_write(report, error_text, error_text_size)) + return false; + if (!report_resolve_path(report, error_text, error_text_size)) + return false; + if (FRIIDUMP_ACCESS(report->final_path) == 0) { + if (report->explicit_path) { + report_error(error_text, error_text_size, + "Refusing to overwrite an existing native report"); + return false; + } + if (!report_collision_path(report->final_path, + report->run_id, + report->final_path, + sizeof(report->final_path))) { + report_error(error_text, error_text_size, + "Could not generate a collision-safe native report path"); + return false; + } + if (FRIIDUMP_ACCESS(report->final_path) == 0) { + report_error(error_text, error_text_size, + "Collision-safe native report path already exists"); + return false; + } + } + + if (snprintf(temporary_path, sizeof(temporary_path), "%s.tmp-%s", + report->final_path, report->run_id) >= (int) sizeof(temporary_path)) { + report_error(error_text, error_text_size, "Temporary report path is too long"); + return false; + } + + if (FRIIDUMP_ACCESS(temporary_path) == 0 && remove(temporary_path) != 0) { + report_error(error_text, error_text_size, "Could not remove a stale temporary report"); + return false; + } + + f = fopen(temporary_path, "wb"); + if (!f) { + report_error(error_text, error_text_size, "Could not open temporary native report"); + return false; + } + + if (!report_write_document(f, report) || fflush(f) != 0 || FRIIDUMP_FSYNC(FRIIDUMP_FILENO(f)) != 0) { + fclose(f); + remove(temporary_path); + report_error(error_text, error_text_size, "Could not write or flush native report"); + return false; + } + + close_status = fclose(f); + if (close_status != 0) { + remove(temporary_path); + report_error(error_text, error_text_size, "Could not close temporary native report"); + return false; + } + +#ifdef WIN32 + if (!MoveFileExA(temporary_path, report->final_path, MOVEFILE_WRITE_THROUGH)) { + remove(temporary_path); + report_error(error_text, error_text_size, "Could not atomically publish native report"); + return false; + } +#else + if (link(temporary_path, report->final_path) != 0) { + remove(temporary_path); + if (errno == EEXIST) + report_error(error_text, error_text_size, "Refusing to overwrite an existing native report"); + else + report_error(error_text, error_text_size, "Could not atomically publish native report"); + return false; + } + if (unlink(temporary_path) != 0) { + remove(report->final_path); + report_error(error_text, error_text_size, "Could not finalize the atomic native report publication"); + return false; + } +#endif + + report_copy(written_path, written_path_size, report->final_path); + return true; +} diff --git a/src/native_report.h b/src/native_report.h new file mode 100644 index 0000000..2cdfa9d --- /dev/null +++ b/src/native_report.h @@ -0,0 +1,183 @@ +#ifndef FRIIDUMP_NATIVE_REPORT_H_INCLUDED +#define FRIIDUMP_NATIVE_REPORT_H_INCLUDED + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define FRIIDUMP_REPORT_PATH_MAX 1024 +#define FRIIDUMP_REPORT_TEXT_MAX 2000 +#define FRIIDUMP_REPORT_NOTE_MAX 16 +#define FRIIDUMP_REPORT_ARTIFACT_MAX 8 + +typedef struct friidump_native_artifact_s { + char type[81]; + char path[FRIIDUMP_REPORT_PATH_MAX + 1]; + bool have_path; + uint64_t bytes; + bool have_bytes; + char sha256[65]; + bool have_sha256; +} friidump_native_artifact; + +typedef struct friidump_native_report_s { + bool enabled; + bool explicit_path; + bool directory_override; + char requested_path[FRIIDUMP_REPORT_PATH_MAX]; + char report_dir[FRIIDUMP_REPORT_PATH_MAX]; + char name_basis[FRIIDUMP_REPORT_PATH_MAX]; + char final_path[FRIIDUMP_REPORT_PATH_MAX]; + + char generator_version[81]; + char build_commit[41]; + bool build_commit_known; + + char run_id[37]; + char started_utc[32]; + char completed_utc[32]; + char generated_utc[32]; + char test_type[40]; + char run_result[24]; + + char vendor[65]; + char model[121]; + char firmware_revision[65]; + char interface_name[41]; + char device_path[256]; + bool have_device_path; + bool firmware_modified; + char modification_note[1001]; + bool have_modification_note; + bool drive_known; + + char platform[16]; + char title[256]; + bool have_title; + char region[81]; + bool have_region; + char disc_id[121]; + bool have_disc_id; + + bool seed_attempted; + char seed_result[24]; + double seed_duration_seconds; + bool have_seed_duration; + + bool dump_attempted; + char dump_result[24]; + uint64_t sector_count; + bool have_sector_count; + uint64_t byte_count; + bool have_byte_count; + double dump_duration_seconds; + bool have_dump_duration; + char failure_stage[161]; + bool have_failure_stage; + uint64_t failure_sector; + bool have_failure_sector; + char output_path[FRIIDUMP_REPORT_PATH_MAX]; + bool have_output_path; + + char crc32[9]; + char md5[33]; + char sha1[41]; + char sha256[65]; + + char reference_provider[81]; + bool have_reference_provider; + char reference_result[24]; + char reference_id[161]; + bool have_reference_id; + + char support_tier[121]; + bool have_support_tier; + char cdb_offset[32]; + bool have_cdb_offset; + char gate_address[32]; + bool have_gate_address; + + char notes[FRIIDUMP_REPORT_NOTE_MAX][FRIIDUMP_REPORT_TEXT_MAX + 1]; + size_t note_count; + + friidump_native_artifact artifacts[FRIIDUMP_REPORT_ARTIFACT_MAX]; + size_t artifact_count; +} friidump_native_report; + +void friidump_native_report_init(friidump_native_report *report, const char *generator_version); +bool friidump_native_report_enable_default(friidump_native_report *report); +bool friidump_native_report_enable_path(friidump_native_report *report, const char *path); +bool friidump_native_report_enable_dir(friidump_native_report *report, const char *directory); +bool friidump_native_report_set_name_basis(friidump_native_report *report, const char *path); +bool friidump_native_report_is_enabled(const friidump_native_report *report); +void friidump_native_report_set_build_commit(friidump_native_report *report, const char *commit); +void friidump_native_report_set_drive(friidump_native_report *report, + const char *vendor, + const char *model, + const char *firmware_revision, + const char *interface_name, + const char *device_path); +void friidump_native_report_set_firmware_modified(friidump_native_report *report, + bool modified, + const char *note); +void friidump_native_report_set_media(friidump_native_report *report, + const char *platform, + const char *title, + const char *region, + const char *disc_id); +void friidump_native_report_set_profile(friidump_native_report *report, + const char *support_tier, + uint32_t cdb_offset, + uint32_t gate_address); +void friidump_native_report_set_seed(friidump_native_report *report, + bool attempted, + const char *result, + bool have_duration, + double duration_seconds); +void friidump_native_report_set_dump(friidump_native_report *report, + bool attempted, + const char *result, + bool have_sector_count, + uint64_t sector_count, + bool have_byte_count, + uint64_t byte_count, + bool have_duration, + double duration_seconds, + const char *failure_stage, + bool have_failure_sector, + uint64_t failure_sector, + const char *output_path); +void friidump_native_report_set_hashes(friidump_native_report *report, + const char *crc32, + const char *md5, + const char *sha1, + const char *sha256); +void friidump_native_report_set_reference(friidump_native_report *report, + const char *provider, + const char *result, + const char *reference_id); +void friidump_native_report_set_outcome(friidump_native_report *report, + const char *test_type, + const char *run_result); +bool friidump_native_report_add_note(friidump_native_report *report, const char *note); +bool friidump_native_report_add_artifact(friidump_native_report *report, + const char *type, + const char *path, + bool have_bytes, + uint64_t bytes, + const char *sha256); +bool friidump_native_report_write(friidump_native_report *report, + char *written_path, + size_t written_path_size, + char *error_text, + size_t error_text_size); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/redump_dat.c b/src/redump_dat.c index 028aa13..d2aba7c 100644 --- a/src/redump_dat.c +++ b/src/redump_dat.c @@ -51,6 +51,7 @@ int redump_resolve_dat_path( char *out_path, size_t out_path_size ) { + const char *environment_dir; char candidate[1024]; if (!basename || !basename[0] || !out_path || out_path_size == 0) @@ -58,6 +59,9 @@ int redump_resolve_dat_path( out_path[0] = '\0'; + /* An explicit CLI directory is authoritative. Do not silently fall back + * when the operator supplied a path, because that would make evidence + * provenance ambiguous. */ if (explicit_dir && explicit_dir[0]) { if (!redump_join_path(explicit_dir, NULL, basename, out_path, out_path_size)) @@ -65,6 +69,17 @@ int redump_resolve_dat_path( return redump_path_exists(out_path); } + /* Environment override is useful for packaged and read-only installs. + * Like the CLI override, it is authoritative when set. */ + environment_dir = getenv("FRIIDUMP_REDUMP_DAT_DIR"); + if (environment_dir && environment_dir[0]) { + if (!redump_join_path(environment_dir, NULL, basename, + out_path, out_path_size)) + return 0; + return redump_path_exists(out_path); + } + + /* Portable/source-build layout: DAT directory beside the executable. */ if (executable_dir && executable_dir[0] && redump_join_path(executable_dir, "redump_dat", basename, candidate, sizeof(candidate))) { @@ -72,19 +87,27 @@ int redump_resolve_dat_path( snprintf(out_path, out_path_size, "%s", candidate); return 1; } - - /* Retain the executable-relative path as the preferred diagnostic - * candidate if neither automatic location exists. */ snprintf(out_path, out_path_size, "%s", candidate); } + /* Installed Unix layout: /bin/friidump with data under + * /share/friidump/redump_dat. */ + if (executable_dir && executable_dir[0] && + redump_join_path(executable_dir, "../share/friidump/redump_dat", + basename, candidate, sizeof(candidate))) { + if (redump_path_exists(candidate)) { + snprintf(out_path, out_path_size, "%s", candidate); + return 1; + } + } + + /* Development convenience for direct source-tree execution. */ if (redump_join_path(".", "redump_dat", basename, candidate, sizeof(candidate))) { if (redump_path_exists(candidate)) { snprintf(out_path, out_path_size, "%s", candidate); return 1; } - if (!out_path[0]) snprintf(out_path, out_path_size, "%s", candidate); } diff --git a/src/redump_dat.h b/src/redump_dat.h index 475f5c2..b3b2c83 100644 --- a/src/redump_dat.h +++ b/src/redump_dat.h @@ -54,12 +54,16 @@ const char *redump_verify_match_kind_string(const redump_verify_result *result); * Resolve a DAT path predictably. * * Resolution order: - * 1. explicit_dir, when non-empty; - * 2. executable_dir/redump_dat; - * 3. current-working-directory redump_dat. + * 1. explicit_dir, when non-empty (authoritative); + * 2. FRIIDUMP_REDUMP_DAT_DIR, when set (authoritative); + * 3. executable_dir/redump_dat (portable and CMake build layout); + * 4. executable_dir/../share/friidump/redump_dat (installed Unix layout); + * 5. current-working-directory redump_dat. * - * The selected candidate is always written to out_path when arguments are - * valid. The return value is nonzero only when that candidate exists. + * CMake copies the bundled DAT files beside the build executable and installs + * them under the platform-appropriate data directory. The selected candidate + * is always written to out_path when arguments are valid. The return value is + * nonzero only when that candidate exists. */ int redump_resolve_dat_path( const char *explicit_dir, diff --git a/tests/contracts/README.md b/tests/contracts/README.md new file mode 100644 index 0000000..5802f15 --- /dev/null +++ b/tests/contracts/README.md @@ -0,0 +1,12 @@ +# Native report contract fixture + +`friidump-test-result.v1.schema.json` is the exact JSON Schema contract shipped +by FriiDump Compatibility Database v1.2.0. It is copied here only for native +report regression testing. The database remains authoritative for submission +and semantic validation. + +Expected schema SHA-256: + +```text +d1e97cb3e88e0f5a94ccf9b9edf921554fc15278662e69420b8c1e564c600411 +``` diff --git a/tests/contracts/friidump-test-result.v1.schema.json b/tests/contracts/friidump-test-result.v1.schema.json new file mode 100644 index 0000000..cef6358 --- /dev/null +++ b/tests/contracts/friidump-test-result.v1.schema.json @@ -0,0 +1,476 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://releases.jtryba.com/friidump/schemas/friidump-test-result.v1.schema.json", + "title": "FriiDump single-run test result", + "description": "One FriiDump execution using one drive and one disc.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "format", + "generated_utc", + "generator", + "run", + "drive", + "media", + "seed", + "dump", + "hashes", + "reference", + "profile", + "notes", + "artifacts" + ], + "properties": { + "schema": { + "const": "friidump-test-result.v1" + }, + "format": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "generation", + "serialization" + ], + "properties": { + "name": { + "const": "FriiDump Interchange Format" + }, + "generation": { + "const": 1 + }, + "serialization": { + "const": "json" + } + } + }, + "generated_utc": { + "type": "string", + "format": "date-time" + }, + "generator": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "version", + "build_commit" + ], + "properties": { + "name": { + "const": "FriiDump" + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "build_commit": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9a-fA-F]{40}$" + } + } + }, + "run": { + "type": "object", + "additionalProperties": false, + "required": [ + "run_id", + "started_utc", + "completed_utc", + "test_type", + "result" + ], + "properties": { + "run_id": { + "type": "string", + "format": "uuid" + }, + "started_utc": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "completed_utc": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "test_type": { + "enum": [ + "full_dump", + "seed_only", + "partial_dump", + "diagnostic_no_media", + "diagnostic_wrong_media", + "media_transition", + "other" + ] + }, + "result": { + "enum": [ + "pass", + "partial", + "fail", + "not_applicable" + ] + } + } + }, + "drive": { + "type": "object", + "additionalProperties": false, + "required": [ + "vendor", + "model", + "firmware_revision", + "interface", + "device_path", + "firmware_modified", + "modification_note" + ], + "properties": { + "vendor": { + "type": "string", + "maxLength": 64 + }, + "model": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "firmware_revision": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "interface": { + "type": "string", + "maxLength": 40 + }, + "device_path": { + "type": [ + "string", + "null" + ], + "maxLength": 255 + }, + "firmware_modified": { + "type": "boolean" + }, + "modification_note": { + "type": [ + "string", + "null" + ], + "maxLength": 1000 + } + } + }, + "media": { + "type": "object", + "additionalProperties": false, + "required": [ + "platform", + "title", + "region", + "disc_id" + ], + "properties": { + "platform": { + "enum": [ + "gamecube", + "wii", + "xbox", + "dvd", + "unknown" + ] + }, + "title": { + "type": [ + "string", + "null" + ], + "maxLength": 255 + }, + "region": { + "type": [ + "string", + "null" + ], + "maxLength": 80 + }, + "disc_id": { + "type": [ + "string", + "null" + ], + "maxLength": 120 + } + } + }, + "seed": { + "type": "object", + "additionalProperties": false, + "required": [ + "attempted", + "result", + "duration_seconds" + ], + "properties": { + "attempted": { + "type": "boolean" + }, + "result": { + "enum": [ + "pass", + "partial", + "fail", + "not_attempted", + "not_applicable" + ] + }, + "duration_seconds": { + "type": [ + "number", + "null" + ], + "minimum": 0 + } + } + }, + "dump": { + "type": "object", + "additionalProperties": false, + "required": [ + "attempted", + "result", + "sector_count", + "byte_count", + "duration_seconds", + "failure_stage", + "failure_sector", + "output_path" + ], + "properties": { + "attempted": { + "type": "boolean" + }, + "result": { + "enum": [ + "pass", + "partial", + "fail", + "not_attempted", + "not_applicable" + ] + }, + "sector_count": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "byte_count": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "duration_seconds": { + "type": [ + "number", + "null" + ], + "minimum": 0 + }, + "failure_stage": { + "type": [ + "string", + "null" + ], + "maxLength": 160 + }, + "failure_sector": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "output_path": { + "type": [ + "string", + "null" + ], + "maxLength": 1024 + } + } + }, + "hashes": { + "type": "object", + "additionalProperties": false, + "required": [ + "crc32", + "md5", + "sha1", + "sha256" + ], + "properties": { + "crc32": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9a-fA-F]{8}$" + }, + "md5": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9a-fA-F]{32}$" + }, + "sha1": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9a-fA-F]{40}$" + }, + "sha256": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9a-fA-F]{64}$" + } + } + }, + "reference": { + "type": "object", + "additionalProperties": false, + "required": [ + "provider", + "result", + "reference_id" + ], + "properties": { + "provider": { + "type": [ + "string", + "null" + ], + "maxLength": 80 + }, + "result": { + "enum": [ + "match", + "mismatch", + "not_checked", + "not_applicable" + ] + }, + "reference_id": { + "type": [ + "string", + "null" + ], + "maxLength": 160 + } + } + }, + "profile": { + "type": "object", + "additionalProperties": false, + "required": [ + "support_tier", + "cdb_offset", + "gate_address" + ], + "properties": { + "support_tier": { + "type": [ + "string", + "null" + ], + "maxLength": 120 + }, + "cdb_offset": { + "type": [ + "string", + "null" + ], + "pattern": "^0x[0-9a-fA-F]+$" + }, + "gate_address": { + "type": [ + "string", + "null" + ], + "pattern": "^0x[0-9a-fA-F]+$" + } + } + }, + "notes": { + "type": "array", + "items": { + "type": "string", + "maxLength": 2000 + }, + "maxItems": 100 + }, + "artifacts": { + "type": "array", + "maxItems": 100, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "path", + "bytes", + "sha256" + ], + "properties": { + "type": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "path": { + "type": [ + "string", + "null" + ], + "maxLength": 1024 + }, + "bytes": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "sha256": { + "type": [ + "string", + "null" + ], + "pattern": "^[0-9a-fA-F]{64}$" + } + } + } + } + } +} diff --git a/tests/test_linux_rawio.c b/tests/test_linux_rawio.c new file mode 100644 index 0000000..cbe7556 --- /dev/null +++ b/tests/test_linux_rawio.c @@ -0,0 +1,102 @@ +#include "linux_rawio.h" + +#include +#include +#include +#include + +static int create_status(char *path, size_t path_size, const char *cap_eff) { + int fd; + FILE *f; + + if (path_size < sizeof("/tmp/friidump-rawio-XXXXXX")) + return 0; + + snprintf(path, path_size, "/tmp/friidump-rawio-XXXXXX"); + fd = mkstemp(path); + if (fd < 0) + return 0; + + f = fdopen(fd, "wb"); + if (!f) { + close(fd); + remove(path); + return 0; + } + + fprintf(f, "Name:\ttest\nCapInh:\t0000000000000000\nCapEff:\t%s\n", cap_eff); + fclose(f); + return 1; +} + +static int expect_state( + const char *label, + const char *path, + friidump_linux_rawio_state expected, + unsigned long long expected_mask) { + unsigned long long mask = 0; + friidump_linux_rawio_state actual = + friidump_linux_rawio_state_from_status_file(path, &mask); + + if (actual != expected || mask != expected_mask) { + fprintf(stderr, + "FAIL: %s state=%d expected=%d mask=0x%llx expected=0x%llx\n", + label, (int) actual, (int) expected, + mask, expected_mask); + return 0; + } + + printf("PASS: %s\n", label); + return 1; +} + +int main(void) { + char present_path[64]; + char missing_path[64]; + char malformed_path[64]; + unsigned long long rawio_mask = 1ULL << FRIIDUMP_LINUX_CAP_SYS_RAWIO; + char present_value[32]; + int ok = 1; + + snprintf(present_value, sizeof(present_value), "%016llx", rawio_mask); + + ok &= create_status(present_path, sizeof(present_path), present_value); + ok &= create_status(missing_path, sizeof(missing_path), "0000000000000000"); + ok &= create_status(malformed_path, sizeof(malformed_path), "not-hex"); + + if (!ok) { + fprintf(stderr, "FAIL: fixture creation\n"); + return 1; + } + + ok &= expect_state( + "CAP_SYS_RAWIO present", + present_path, + FRIIDUMP_LINUX_RAWIO_PRESENT, + rawio_mask); + ok &= expect_state( + "CAP_SYS_RAWIO missing", + missing_path, + FRIIDUMP_LINUX_RAWIO_MISSING, + 0); + ok &= expect_state( + "malformed CapEff", + malformed_path, + FRIIDUMP_LINUX_RAWIO_UNKNOWN, + 0); + ok &= expect_state( + "missing status file", + "/definitely/not/a/real/status/file", + FRIIDUMP_LINUX_RAWIO_UNKNOWN, + 0); + + remove(present_path); + remove(missing_path); + remove(malformed_path); + + if (!ok) + return 1; + + printf("LINUX RAW-I/O CAPABILITY PARSER: PASS\n"); + return 0; +} diff --git a/tests/test_linux_rawio_contract.py b/tests/test_linux_rawio_contract.py new file mode 100644 index 0000000..1798af9 --- /dev/null +++ b/tests/test_linux_rawio_contract.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +from pathlib import Path +import sys + +root = Path(__file__).resolve().parents[1] +main_c = (root / "src" / "friidump.c").read_text(encoding="utf-8") +rawio_c = (root / "src" / "linux_rawio.c").read_text(encoding="utf-8") +dvd_drive_c = (root / "libfriidump" / "dvd_drive.c").read_text(encoding="utf-8") +dumper_c = (root / "libfriidump" / "dumper.c").read_text(encoding="utf-8") +windows_utils_c = (root / "libfriidump" / "xbox_ref" / "utils.c").read_text(encoding="utf-8") +cmake = (root / "CMakeLists.txt").read_text(encoding="utf-8") +linux_doc = (root / "docs" / "LINUX.md").read_text(encoding="utf-8") +helper = (root / "validation" / "friidump-linux-rawio-capability.sh").read_text(encoding="utf-8") +validator = ( + root + / "validation" + / "friidump-v0.5.3.16-pf1-candidate21-linux-live-validation.sh" +).read_text(encoding="utf-8") + +checks = { + "runtime reads CapEff": 'strncmp(line, "CapEff:", 7)' in rawio_c, + "CAP_SYS_RAWIO bit is explicit": "FRIIDUMP_LINUX_CAP_SYS_RAWIO 17" in (root / "src" / "linux_rawio.h").read_text(encoding="utf-8"), + "vendor path has runtime preflight": "friidump_linux_rawio_preflight" in main_c, + "root execution is refused": "refuses this Linux vendor-command path while running as root" in main_c, + "missing capability is explained": "Linux vendor-command authorization is unavailable" in main_c, + "build emits raw-I/O warning": "Linux vendor-command paths require CAP_SYS_RAWIO" in cmake, + "helper installs exact file capability": "sudo setcap cap_sys_rawio=ep" in helper, + "helper verifies capability": "FRIIDUMP LINUX RAW-I/O CAPABILITY INSTALL: PASS" in helper, + "helper warns rebuild clears capability": "rebuilding, replacing, copying, or re-extracting" in helper, + "validator has capability phase": "capability_phase" in validator, + "validator normalizes future timestamps": "Future source timestamp preflight" in validator, + "preflight note buffer covers long executable paths": "char note[1536]" in main_c, + "fallback executable path is bounded without snprintf": "fallback_dir" in main_c and "suffix_len" in main_c, + "Linux documentation names CAP_SYS_RAWIO": "CAP_SYS_RAWIO" in linux_doc, + "Linux documentation forbids whole-process sudo": "Do not run FriiDump itself with `sudo`" in linux_doc, + "Linux media cycle releases door lock": "CDROM_LOCKDOOR, locked ? 1 : 0" in dvd_drive_c and "dvd_set_door_lock (dvd, false)" in dvd_drive_c, + "Linux media cycle restores door lock": "dvd_set_door_lock (dvd, true)" in dvd_drive_c, + "Linux media-cycle failure remains recoverable": "the door remains unlocked for recovery" in dvd_drive_c, + "Linux documentation explains software tray cycle": "CDROM_LOCKDOOR" in linux_doc and "software tray cycle" in linux_doc, + "Linux Xbox handshake uses explicit SG_IO": "#include " in dvd_drive_c and "io.interface_id = 'S'" in dvd_drive_c and "ioctl (dvd -> fd, SG_IO, &io)" in dvd_drive_c, + "Linux Xbox handshake preserves explicit CDB lengths": "io.cmd_len = (unsigned char) cdb_len" in dvd_drive_c and '"9-mode-select-sticky-descrambling"' in dvd_drive_c, + "Linux Xbox handshake preserves Windows timeouts": "120000" in dvd_drive_c and "10000" in dvd_drive_c, + "Linux Xbox handshake emits transport diagnostics": "[XBOX-SGIO] step=%s" in dvd_drive_c and "host=0x%04X" in dvd_drive_c and "driver=0x%04X" in dvd_drive_c, + "Linux media-cycle evidence reaches persistent log": 'xbox_ref_log_fprintf (stderr, "[XBOX] Linux media-cycle released' in dvd_drive_c and 'xbox_ref_log_fprintf (stderr, "[XBOX] Linux media-cycle restored' in dvd_drive_c, + "generic RecoveryKick removed": "RecoveryKick(" not in windows_utils_c and "recovery_kick" not in dvd_drive_c and "recovery_kick" not in dumper_c, + "Windows state sequence ported": "dvd_xbox_prepare_game_view" in dvd_drive_c and "Entry state appears locked/video; attempting the full handshake directly without a media transition" in dvd_drive_c, + "Windows refresh cadence exact": "dvd_refresh_volume (dvd);" in dvd_drive_c and "dvd_sleep_ms (2000);" in dvd_drive_c and "dvd_wait_ready (dvd, 30000)" in dvd_drive_c, + "single tray-cycle recovery": "performing one tray-cycle recovery and retry" in dvd_drive_c and "dvd_media_cycle (dvd, NULL)" in dvd_drive_c, + "portable dumper uses shared Windows sequence": dumper_c.count("disc_xbox_prepare_game_view") >= 2 and "Windows sequence 1/4" in dumper_c and "Windows sequence 4/4" in dumper_c, +} + +failed = [] +for label, ok in checks.items(): + print(f"{'PASS' if ok else 'FAIL'}: {label}") + if not ok: + failed.append(label) + +if failed: + raise SystemExit("Linux raw-I/O contract failed: " + ", ".join(failed)) + +print("LINUX RAW-I/O SOURCE/DOCUMENTATION CONTRACT: PASS") diff --git a/tests/test_linux_xbox_portable_contract.py b/tests/test_linux_xbox_portable_contract.py new file mode 100644 index 0000000..7377055 --- /dev/null +++ b/tests/test_linux_xbox_portable_contract.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +from pathlib import Path +import re + +root = Path(__file__).resolve().parents[1] +dumper = (root / "libfriidump" / "dumper.c").read_text(encoding="utf-8") +portable = (root / "libfriidump" / "xbox_portable.c").read_text(encoding="utf-8") +cmake = (root / "libfriidump" / "CMakeLists.txt").read_text(encoding="utf-8") +rsp = (root / "msvc32_friidump.rsp").read_text(encoding="utf-8") + +checks = [ + ("portable source is compiled by CMake", "xbox_portable.c" in cmake), + ("portable source is compiled by MSVC response file", r"libfriidump\xbox_portable.c" in rsp), + ("portable XDFS/XBE metadata parser exists", "xbox_portable_read_metadata" in portable), + ("portable UTF-16 title conversion exists", "xbox_decode_title" in portable), + ("portable DMI media-ID extraction exists", "xbox_parse_media_id" in portable), + ("portable result handoff begins for ISO", "xbox_portable_result_begin (dmp, dmp -> outfile_iso, '1')" in dumper), + ("portable result handoff begins for XISO", "xbox_portable_result_begin (dmp, dmp -> outfile_xiso, '2')" in dumper), + ("portable result handoff captures metadata", "xbox_portable_result_capture_metadata (dmp)" in dumper), + ("portable result handoff finalizes hashes", "result -> hashes_complete = 1" in dumper), + ("Linux default XBE-derived filename is resolved", "xbox_portable_resolve_output_path" in dumper), + ("Windows challenge-drive copied path remains guarded", bool(re.search(r"#ifdef WIN32\s+if \(disc_is_xbox_challenge_drive", dumper))), +] + +failed = False +for label, ok in checks: + print(f"{'PASS' if ok else 'FAIL'}: {label}") + failed |= not ok + +if failed: + raise SystemExit(1) + +print("LINUX XBOX PORTABLE CONTRACT: PASS") diff --git a/tests/test_native_report.c b/tests/test_native_report.c new file mode 100644 index 0000000..b0954d0 --- /dev/null +++ b/tests/test_native_report.c @@ -0,0 +1,691 @@ +#include "native_report.h" +#include "nintendo_disc_header.h" +#include "xbox_region.h" + +#include +#include +#include + +#ifdef WIN32 +#include +#define PATH_SEP "\\" +#define TEST_GETCWD _getcwd +#define TEST_CHDIR _chdir +#else +#include +#define PATH_SEP "/" +#define TEST_GETCWD getcwd +#define TEST_CHDIR chdir +#endif + +static int nintendo_disc_header_tests(void) { + unsigned char gamecube_header[0x20] = {0}; + unsigned char wii_header[0x20] = {0}; + unsigned char unknown_header[0x20] = {0}; + + memcpy(gamecube_header, "GALE01", 6); + gamecube_header[0x1c] = 0xc2; + gamecube_header[0x1d] = 0x33; + gamecube_header[0x1e] = 0x9f; + gamecube_header[0x1f] = 0x3d; + + memcpy(wii_header, "RSBE01", 6); + wii_header[0x18] = 0x5d; + wii_header[0x19] = 0x1c; + wii_header[0x1a] = 0x9e; + wii_header[0x1b] = 0xa3; + + if (nintendo_disc_header_detect( + gamecube_header, + sizeof(gamecube_header)) != NINTENDO_DISC_HEADER_GAMECUBE) { + fprintf(stderr, "GALE01 GameCube header classification failed\n"); + return 0; + } + + if (nintendo_disc_header_detect( + wii_header, + sizeof(wii_header)) != NINTENDO_DISC_HEADER_WII) { + fprintf(stderr, "RSBE01 Wii header classification failed\n"); + return 0; + } + + if (nintendo_disc_header_detect( + unknown_header, + sizeof(unknown_header)) != NINTENDO_DISC_HEADER_UNKNOWN) { + fprintf(stderr, "Unknown Nintendo header classification failed\n"); + return 0; + } + + if (nintendo_disc_header_detect( + gamecube_header, + 0x1f) != NINTENDO_DISC_HEADER_UNKNOWN) { + fprintf(stderr, "Truncated Nintendo header classification failed\n"); + return 0; + } + + printf("Nintendo disc-header classification: PASS\n"); + return 1; +} + +static int xbox_region_tests(void) { + char region[128]; + + if (!xbox_region_format(XB_REGION_US_CANADA, region, sizeof(region)) || + strcmp(region, "North America") != 0) { + fprintf(stderr, "Xbox North America region formatting failed: %s\n", region); + return 0; + } + + if (!xbox_region_format(XB_REGION_JAPAN, region, sizeof(region)) || + strcmp(region, "Japan") != 0) { + fprintf(stderr, "Xbox Japan region formatting failed: %s\n", region); + return 0; + } + + if (!xbox_region_format(XB_REGION_EUROPE_AU_NZ, region, sizeof(region)) || + strcmp(region, "Europe/Australia/New Zealand") != 0) { + fprintf(stderr, "Xbox Europe/Australia/New Zealand formatting failed: %s\n", region); + return 0; + } + + if (!xbox_region_format( + XB_REGION_US_CANADA | XB_REGION_JAPAN, + region, sizeof(region)) || + strcmp(region, "North America; Japan") != 0) { + fprintf(stderr, "Xbox multi-region formatting failed: %s\n", region); + return 0; + } + + if (!xbox_region_format(0U, region, sizeof(region)) || + strcmp(region, "None (Locked)") != 0) { + fprintf(stderr, "Xbox locked-region formatting failed: %s\n", region); + return 0; + } + + if (!xbox_region_format(0x7fffffffU, region, sizeof(region)) || + strcmp(region, "Region Free") != 0) { + fprintf(stderr, "Xbox region-free formatting failed: %s\n", region); + return 0; + } + + printf("Xbox XBE GameRegion formatting: PASS\n"); + return 1; +} + +static int write_report(friidump_native_report *report, + const char *directory, + const char *filename) { + char path[FRIIDUMP_REPORT_PATH_MAX]; + char written[FRIIDUMP_REPORT_PATH_MAX]; + char error_text[256]; + + snprintf(path, sizeof(path), "%s%s%s", directory, PATH_SEP, filename); + if (!friidump_native_report_enable_path(report, path)) { + fprintf(stderr, "Could not configure fixture path: %s\n", path); + return 0; + } + if (!friidump_native_report_write(report, written, sizeof(written), + error_text, sizeof(error_text))) { + fprintf(stderr, "Could not write fixture %s: %s\n", path, error_text); + return 0; + } + printf("fixture: %s\n", written); + return 1; +} + +static void base_drive(friidump_native_report *r, + const char *model, + const char *firmware) { + friidump_native_report_set_drive( + r, "HL-DT-ST", model, firmware, "ATAPI", "fixture-device"); + friidump_native_report_set_profile( + r, "known_supported_profile_hardening", 0x5e0, 0x90024ff7); +} + +static int success_gamecube(const char *dir) { + friidump_native_report r; + friidump_native_report_init(&r, "0.5.3.16-native-report-test"); + base_drive(&r, "DVD-ROM GDR8163B", "0M26"); + friidump_native_report_set_media( + &r, "gamecube", "Sonic Mega Collection", "USA", "GUPP8P"); + friidump_native_report_set_seed(&r, true, "pass", true, 6.0); + friidump_native_report_set_dump( + &r, true, "pass", true, 712880, true, 1459978240ULL, + true, 7241.31, NULL, false, 0, "Sonic Mega Collection.iso"); + friidump_native_report_set_hashes( + &r, + "01b52739", + "85a525df1481d0ad67d8761f832dca12", + "06eb6d15b4d7f90ec0fed9cea9a77db41358d74ed", + "2c0da8b5cb1f615d68562ae0f731f28bc82ac59136c70eb57aecbb4fe4b63882"); + friidump_native_report_add_artifact( + &r, "dump_output", "Sonic Mega Collection.iso", true, + 1459978240ULL, + "2c0da8b5cb1f615d68562ae0f731f28bc82ac59136c70eb57aecbb4fe4b63882"); + friidump_native_report_set_reference( + &r, "redump", "match", "Sonic Mega Collection (USA).iso"); + friidump_native_report_set_outcome(&r, "full_dump", "pass"); + friidump_native_report_add_note(&r, "Measurement scope: full_optical_payload."); + return write_report(&r, dir, "success-gamecube.friidump.json"); +} + +static int success_xbox(const char *dir) { + friidump_native_report r; + friidump_native_report_init(&r, "0.5.3.16-native-report-test"); + base_drive(&r, "DVD-ROM GDR8050L", "0012"); + friidump_native_report_set_firmware_modified( + &r, true, "Modified firmware with HIT 0xE7 memdump support."); + friidump_native_report_set_media( + &r, "xbox", "Red Faction II", "North America", "TQ00501A"); + friidump_native_report_set_seed(&r, false, "not_applicable", false, 0.0); + friidump_native_report_set_dump( + &r, true, "pass", true, 3820880, true, 7825162240ULL, + true, 14000.0, NULL, false, 0, "Red Faction II [TQ00501A].iso"); + friidump_native_report_set_hashes( + &r, + "12345678", + "0123456789abcdef0123456789abcdef", + "0123456789abcdef0123456789abcdef01234567", + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + friidump_native_report_add_artifact( + &r, "dump_output", "Red Faction II [TQ00501A].iso", true, + 7825162240ULL, + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + friidump_native_report_set_reference(&r, "redump", "mismatch", NULL); + friidump_native_report_set_outcome(&r, "full_dump", "pass"); + friidump_native_report_add_note(&r, "Measurement scope: assembled_output."); + friidump_native_report_add_note( + &r, + "Xbox output throughput describes the produced assembled output and is not directly comparable to a full optical-payload read."); + return write_report(&r, dir, "success-xbox-assembled.friidump.json"); +} + +static int partial_gamecube(const char *dir) { + friidump_native_report r; + friidump_native_report_init(&r, "0.5.3.16-native-report-test"); + base_drive(&r, "DVD-ROM GDR8163B", "0M26"); + friidump_native_report_set_media( + &r, "gamecube", "Sonic Mega Collection", "USA", "GUPP8P"); + friidump_native_report_set_seed(&r, true, "pass", true, 6.0); + friidump_native_report_set_dump( + &r, true, "partial", true, 100000, true, 204800000ULL, + true, 1000.0, "raw_sector_read_or_output_write", true, 100000, + "Sonic Mega Collection.partial.iso"); + friidump_native_report_set_hashes(&r, NULL, NULL, NULL, NULL); + friidump_native_report_add_artifact( + &r, "dump_output", "Sonic Mega Collection.partial.iso", true, + 204800000ULL, NULL); + friidump_native_report_set_reference(&r, NULL, "not_checked", NULL); + friidump_native_report_set_outcome(&r, "partial_dump", "partial"); + friidump_native_report_add_note(&r, "Measurement scope: partial_progress."); + return write_report(&r, dir, "partial-gamecube.friidump.json"); +} + +static int partial_cancelled(const char *dir) { + friidump_native_report r; + friidump_native_report_init(&r, "0.5.3.16-native-report-test"); + base_drive(&r, "DVD-ROM GDR8163B", "0M26"); + friidump_native_report_set_media( + &r, "gamecube", "Sonic Mega Collection", "USA", "GUPP8P"); + friidump_native_report_set_seed(&r, true, "pass", true, 6.0); + friidump_native_report_set_dump( + &r, true, "partial", true, 25000, true, 51200000ULL, + true, 250.0, "user_cancelled", true, 25000, + "Sonic Mega Collection.cancelled.iso"); + friidump_native_report_set_hashes(&r, NULL, NULL, NULL, NULL); + friidump_native_report_add_artifact( + &r, "dump_output", "Sonic Mega Collection.cancelled.iso", true, + 51200000ULL, NULL); + friidump_native_report_set_reference(&r, NULL, "not_checked", NULL); + friidump_native_report_set_outcome(&r, "partial_dump", "partial"); + friidump_native_report_add_note(&r, "Measurement scope: partial_progress."); + friidump_native_report_add_note( + &r, + "The invocation was cancelled by the user; only completed output bytes are represented."); + return write_report(&r, dir, "partial-cancelled.friidump.json"); +} + +static int partial_xbox_cancelled(const char *dir) { + friidump_native_report r; + friidump_native_report_init(&r, "0.5.3.16-native-report-test"); + base_drive(&r, "DVD-ROM GDR8050L", "0012"); + friidump_native_report_set_firmware_modified( + &r, true, + "GDR-8163B cross-flashed with modified GDR-8050L firmware; patched to allow 0xE7 memory dumps"); + friidump_native_report_set_media( + &r, "xbox", "Red Faction II", "North America", "TQ00501A"); + friidump_native_report_set_seed( + &r, false, "not_applicable", false, 0.0); + friidump_native_report_set_dump( + &r, true, "partial", true, 22273, true, 45615104ULL, + true, 14.0, "user_cancelled", true, 22273, + "Red Faction II [TQ00501A].partial.xiso"); + friidump_native_report_set_hashes( + &r, + "89abcdef", + "0123456789abcdef0123456789abcdef", + "0123456789abcdef0123456789abcdef01234567", + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + friidump_native_report_add_artifact( + &r, "dump_output", + "Red Faction II [TQ00501A].partial.xiso", true, + 45615104ULL, + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + friidump_native_report_set_reference( + &r, NULL, "not_checked", NULL); + friidump_native_report_set_outcome( + &r, "partial_dump", "partial"); + friidump_native_report_add_note( + &r, "Measurement scope: partial_progress."); + friidump_native_report_add_note( + &r, + "The invocation was cancelled by the user; only completed output bytes are represented."); + friidump_native_report_add_note( + &r, "Xbox XBE GameRegion mask: 0x00000001."); + return write_report( + &r, dir, "partial-xbox-cancelled.friidump.json"); +} + +static int seed_failure(const char *dir) { + friidump_native_report r; + friidump_native_report_init(&r, "0.5.3.16-native-report-test"); + base_drive(&r, "DVD-ROM GDR8163B", "0M26"); + friidump_native_report_set_media(&r, "gamecube", NULL, NULL, NULL); + friidump_native_report_set_seed(&r, true, "fail", true, 15.0); + friidump_native_report_set_dump( + &r, false, "not_attempted", false, 0, false, 0, + false, 0.0, NULL, false, 0, NULL); + friidump_native_report_set_reference(&r, NULL, "not_checked", NULL); + friidump_native_report_set_outcome(&r, "seed_only", "fail"); + friidump_native_report_add_note(&r, "Disc initialization or seed retrieval failed."); + return write_report(&r, dir, "seed-failure.friidump.json"); +} + +static int no_media(const char *dir) { + friidump_native_report r; + friidump_native_report_init(&r, "0.5.3.16-native-report-test"); + base_drive(&r, "DVD-ROM GDR8163B", "0M26"); + friidump_native_report_set_media(&r, "unknown", NULL, NULL, NULL); + friidump_native_report_set_seed(&r, false, "not_attempted", false, 0.0); + friidump_native_report_set_dump( + &r, false, "not_attempted", false, 0, false, 0, + false, 0.0, NULL, false, 0, NULL); + friidump_native_report_set_reference(&r, NULL, "not_applicable", NULL); + friidump_native_report_set_outcome( + &r, "diagnostic_no_media", "not_applicable"); + friidump_native_report_add_note( + &r, "Media preflight found no readable disc (sense 02/3A/00)."); + return write_report(&r, dir, "no-media.friidump.json"); +} + +static int modified_firmware(const char *dir) { + friidump_native_report r; + friidump_native_report_init(&r, "0.5.3.16-native-report-test"); + base_drive(&r, "DVD-ROM GDR8050L", "0L23"); + friidump_native_report_set_firmware_modified( + &r, true, "Cross-flashed firmware with added HIT 0xE7 memdump support."); + friidump_native_report_set_media( + &r, "gamecube", "Sonic Mega Collection", "USA", "GUPP8P"); + friidump_native_report_set_seed(&r, true, "pass", true, 6.0); + friidump_native_report_set_dump( + &r, true, "pass", true, 712880, true, 1459978240ULL, + true, 9000.0, NULL, false, 0, "modified-drive-test.iso"); + friidump_native_report_set_hashes( + &r, + "01b52739", + "85a525df1481d0ad67d8761f832dca12", + "06eb6d15b4d7f90ec0fed9cea9a77db41358d74ed", + NULL); + friidump_native_report_add_artifact( + &r, "dump_output", "modified-drive-test.iso", true, + 1459978240ULL, NULL); + friidump_native_report_set_reference(&r, "redump", "match", "fixture"); + friidump_native_report_set_outcome(&r, "full_dump", "pass"); + friidump_native_report_add_note(&r, "Measurement scope: full_optical_payload."); + friidump_native_report_add_note( + &r, "UTF-8 preservation fixture: Pok\xc3\xa9mon."); + return write_report(&r, dir, "modified-firmware.friidump.json"); +} + +static int identity_tests(const char *dir) { + friidump_native_report a; + friidump_native_report b; + char path[FRIIDUMP_REPORT_PATH_MAX]; + char written[FRIIDUMP_REPORT_PATH_MAX]; + char error_text[256]; + + friidump_native_report_init(&a, "0.5.3.16-native-report-test"); + friidump_native_report_init(&b, "0.5.3.16-native-report-test"); + if (strcmp(a.run_id, b.run_id) == 0) { + fprintf(stderr, "Retry identity test failed: UUIDs are equal\n"); + return 0; + } + + base_drive(&a, "DVD-ROM GDR8163B", "0M26"); + friidump_native_report_set_media(&a, "unknown", NULL, NULL, NULL); + friidump_native_report_set_outcome(&a, "diagnostic_no_media", "not_applicable"); + friidump_native_report_set_reference(&a, NULL, "not_applicable", NULL); + snprintf(path, sizeof(path), "%s%sidentity-no-overwrite.friidump.json", dir, PATH_SEP); + if (!friidump_native_report_enable_path(&a, path)) + return 0; + if (!friidump_native_report_write(&a, written, sizeof(written), error_text, sizeof(error_text))) + return 0; + if (friidump_native_report_write(&a, written, sizeof(written), error_text, sizeof(error_text))) { + fprintf(stderr, "No-overwrite test failed: second publish succeeded\n"); + return 0; + } + if (strstr(error_text, "overwrite") == NULL) { + fprintf(stderr, "No-overwrite test failed with unexpected error: %s\n", error_text); + return 0; + } + printf("identity: unique retry UUIDs and no-overwrite policy PASS\n"); + return 1; +} + +static int report_directory_test(const char *dir) { + friidump_native_report r; + char nested[FRIIDUMP_REPORT_PATH_MAX]; + char written[FRIIDUMP_REPORT_PATH_MAX]; + char error_text[256]; + FILE *f; + + snprintf(nested, sizeof(nested), "%s%snested%sreports", dir, PATH_SEP, PATH_SEP); + friidump_native_report_init(&r, "0.5.3.16-native-report-test"); + base_drive(&r, "DVD-ROM GDR8163B", "0M26"); + friidump_native_report_set_media( + &r, "gamecube", "Directory Fixture", "USA", "DIR001"); + friidump_native_report_set_seed(&r, false, "not_applicable", false, 0.0); + friidump_native_report_set_dump( + &r, false, "not_attempted", false, 0, false, 0, + false, 0.0, NULL, false, 0, NULL); + friidump_native_report_set_reference(&r, NULL, "not_applicable", NULL); + friidump_native_report_set_outcome(&r, "other", "not_applicable"); + + if (!friidump_native_report_enable_default(&r) || + !friidump_native_report_set_name_basis( + &r, "logs/directory-fixture.log") || + !friidump_native_report_enable_dir(&r, nested)) { + fprintf(stderr, "Could not configure nested report directory\n"); + return 0; + } + if (!friidump_native_report_write( + &r, written, sizeof(written), error_text, sizeof(error_text))) { + fprintf(stderr, "Nested report-directory write failed: %s\n", error_text); + return 0; + } + if (strstr(written, "directory-fixture.friidump.json") == NULL || + strstr(written, r.run_id) != NULL) { + fprintf(stderr, "Generated report path did not retain the log-derived name: %s\n", written); + return 0; + } + f = fopen(written, "rb"); + if (!f) { + fprintf(stderr, "Generated report path is not readable: %s\n", written); + return 0; + } + fclose(f); + printf("report directory: nested creation and log-derived filename PASS\n"); + return 1; +} + +static int expect_write_failure(friidump_native_report *r, + const char *dir, + const char *filename, + const char *expected_error) { + char path[FRIIDUMP_REPORT_PATH_MAX]; + char written[FRIIDUMP_REPORT_PATH_MAX]; + char error_text[256]; + FILE *f; + + snprintf(path, sizeof(path), "%s%s%s", dir, PATH_SEP, filename); + remove(path); + if (!friidump_native_report_enable_path(r, path)) + return 0; + if (friidump_native_report_write( + r, written, sizeof(written), error_text, sizeof(error_text))) { + fprintf(stderr, "Invalid report unexpectedly wrote %s\n", path); + remove(path); + return 0; + } + if (!strstr(error_text, expected_error)) { + fprintf(stderr, "Invalid report error mismatch: expected '%s', got '%s'\n", + expected_error, error_text); + return 0; + } + f = fopen(path, "rb"); + if (f) { + fclose(f); + fprintf(stderr, "Invalid report left a published file: %s\n", path); + remove(path); + return 0; + } + return 1; +} + +static void valid_no_media_report(friidump_native_report *r) { + friidump_native_report_init(r, "0.5.3.16-native-report-test"); + base_drive(r, "DVD-ROM GDR8163B", "0M26"); + friidump_native_report_set_media(r, "unknown", NULL, NULL, NULL); + friidump_native_report_set_seed(r, false, "not_attempted", false, 0.0); + friidump_native_report_set_dump( + r, false, "not_attempted", false, 0, false, 0, + false, 0.0, NULL, false, 0, NULL); + friidump_native_report_set_reference(r, NULL, "not_applicable", NULL); + friidump_native_report_set_outcome( + r, "diagnostic_no_media", "not_applicable"); +} + +static int path_override_tests(const char *dir) { + friidump_native_report first; + friidump_native_report second; + friidump_native_report working; + friidump_native_report combined; + char nested[FRIIDUMP_REPORT_PATH_MAX]; + char explicit_path[FRIIDUMP_REPORT_PATH_MAX]; + char expected_path[FRIIDUMP_REPORT_PATH_MAX]; + char written[FRIIDUMP_REPORT_PATH_MAX]; + char error_text[256]; + FILE *f; + + snprintf(nested, sizeof(nested), "%s%spath-overrides", dir, PATH_SEP); + + valid_no_media_report(&first); + if (!friidump_native_report_enable_default(&first) || + !friidump_native_report_set_name_basis( + &first, "logs/default-session.log") || + !friidump_native_report_enable_dir(&first, nested)) { + fprintf(stderr, "Could not configure derived path fixture\n"); + return 0; + } + if (!friidump_native_report_write( + &first, written, sizeof(written), error_text, sizeof(error_text))) { + fprintf(stderr, "Could not write derived path fixture: %s\n", error_text); + return 0; + } + snprintf(expected_path, sizeof(expected_path), + "%s%sdefault-session.friidump.json", nested, PATH_SEP); + if (strcmp(written, expected_path) != 0) { + fprintf(stderr, "Derived path mismatch: expected %s, got %s\n", + expected_path, written); + return 0; + } + + valid_no_media_report(&second); + if (!friidump_native_report_enable_default(&second) || + !friidump_native_report_set_name_basis( + &second, "logs/default-session.log") || + !friidump_native_report_enable_dir(&second, nested)) { + fprintf(stderr, "Could not configure collision fixture\n"); + return 0; + } + if (!friidump_native_report_write( + &second, written, sizeof(written), error_text, sizeof(error_text))) { + fprintf(stderr, "Could not write collision fixture: %s\n", error_text); + return 0; + } + if (strstr(written, second.run_id) == NULL || + strstr(written, "default-session-") == NULL) { + fprintf(stderr, "Collision-safe path did not use the run UUID: %s\n", written); + return 0; + } + + valid_no_media_report(&working); + snprintf(explicit_path, sizeof(explicit_path), + "%s%sworking-directory.log", nested, PATH_SEP); + if (!friidump_native_report_enable_default(&working) || + !friidump_native_report_set_name_basis(&working, explicit_path)) { + fprintf(stderr, "Could not configure default beside-log fixture\n"); + return 0; + } + if (!friidump_native_report_write( + &working, written, sizeof(written), error_text, sizeof(error_text))) { + fprintf(stderr, "Could not write default beside-log fixture: %s\n", + error_text); + return 0; + } + snprintf(expected_path, sizeof(expected_path), + "%s%sworking-directory.friidump.json", nested, PATH_SEP); + if (strcmp(written, expected_path) != 0) { + fprintf(stderr, "Beside-log path mismatch: expected %s, got %s\n", + expected_path, written); + return 0; + } + f = fopen(written, "rb"); + if (!f) { + fprintf(stderr, "Default beside-log report is not readable: %s\n", written); + return 0; + } + fclose(f); + + valid_no_media_report(&combined); + snprintf(explicit_path, sizeof(explicit_path), + "ignored%sparent%scustom-evidence.json", PATH_SEP, PATH_SEP); + if (!friidump_native_report_enable_default(&combined) || + !friidump_native_report_enable_path(&combined, explicit_path) || + !friidump_native_report_enable_dir(&combined, nested)) { + fprintf(stderr, "Could not configure combined pathname/directory fixture\n"); + return 0; + } + if (!friidump_native_report_write( + &combined, written, sizeof(written), error_text, sizeof(error_text))) { + fprintf(stderr, "Could not write combined override fixture: %s\n", error_text); + return 0; + } + snprintf(expected_path, sizeof(expected_path), + "%s%scustom-evidence.json", nested, PATH_SEP); + if (strcmp(written, expected_path) != 0) { + fprintf(stderr, "Combined override mismatch: expected %s, got %s\n", + expected_path, written); + return 0; + } + f = fopen(written, "rb"); + if (!f) { + fprintf(stderr, "Combined override report is not readable: %s\n", written); + return 0; + } + fclose(f); + + printf("path overrides: default beside-log, derived name, collision handling, and combined overrides PASS\n"); + return 1; +} + +static int invalid_report_tests(const char *dir) { + friidump_native_report r; + + valid_no_media_report(&r); + friidump_native_report_set_firmware_modified(&r, true, NULL); + if (!expect_write_failure( + &r, dir, "invalid-modified-without-note.friidump.json", + "Modified firmware requires")) + return 0; + + valid_no_media_report(&r); + snprintf(r.run_id, sizeof(r.run_id), + "00000000-0000-0000-0000-000000000000"); + if (!expect_write_failure( + &r, dir, "invalid-zero-uuid.friidump.json", + "Run UUID")) + return 0; + + valid_no_media_report(&r); + friidump_native_report_set_dump( + &r, true, "pass", false, 0, false, 0, + true, 1.0, NULL, false, 0, "invalid.iso"); + friidump_native_report_set_outcome(&r, "full_dump", "pass"); + if (!expect_write_failure( + &r, dir, "invalid-success-without-counts.friidump.json", + "requires sector and byte counts")) + return 0; + + valid_no_media_report(&r); + friidump_native_report_set_seed(&r, false, "pass", false, 0.0); + if (!expect_write_failure( + &r, dir, "invalid-nonattempted-seed-pass.friidump.json", + "non-attempted seed stage")) + return 0; + + valid_no_media_report(&r); + friidump_native_report_set_media( + &r, "gamecube", "Artifact Fixture", "USA", "ART001"); + friidump_native_report_set_seed(&r, true, "pass", true, 1.0); + friidump_native_report_set_dump( + &r, true, "pass", true, 1, true, 2048, + true, 1.0, NULL, false, 0, "missing-artifact.iso"); + friidump_native_report_set_hashes( + &r, NULL, NULL, NULL, + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + friidump_native_report_set_outcome(&r, "full_dump", "pass"); + friidump_native_report_add_note(&r, "Measurement scope: full_optical_payload."); + if (!expect_write_failure( + &r, dir, "invalid-missing-output-artifact.friidump.json", + "exactly one dump_output artifact")) + return 0; + + valid_no_media_report(&r); + friidump_native_report_set_media( + &r, "gamecube", "Artifact Fixture", "USA", "ART002"); + friidump_native_report_set_seed(&r, true, "pass", true, 1.0); + friidump_native_report_set_dump( + &r, true, "pass", true, 1, true, 2048, + true, 1.0, NULL, false, 0, "artifact-mismatch.iso"); + friidump_native_report_set_hashes( + &r, NULL, NULL, NULL, + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + friidump_native_report_add_artifact( + &r, "dump_output", "artifact-mismatch.iso", true, 4096, + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + friidump_native_report_set_outcome(&r, "full_dump", "pass"); + friidump_native_report_add_note(&r, "Measurement scope: full_optical_payload."); + if (!expect_write_failure( + &r, dir, "invalid-output-artifact-mismatch.friidump.json", + "does not match")) + return 0; + + printf("invalid reports: semantic rejection and no publication PASS\n"); + return 1; +} + +int main(int argc, char **argv) { + int ok; + if (argc != 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 2; + } + + ok = nintendo_disc_header_tests() && + xbox_region_tests() && + success_gamecube(argv[1]) && + success_xbox(argv[1]) && + partial_gamecube(argv[1]) && + partial_cancelled(argv[1]) && + partial_xbox_cancelled(argv[1]) && + seed_failure(argv[1]) && + no_media(argv[1]) && + modified_firmware(argv[1]) && + identity_tests(argv[1]) && + report_directory_test(argv[1]) && + path_override_tests(argv[1]) && + invalid_report_tests(argv[1]); + + return ok ? 0 : 1; +} diff --git a/tests/test_redump_dat_verify.c b/tests/test_redump_dat_verify.c new file mode 100644 index 0000000..7a7d598 --- /dev/null +++ b/tests/test_redump_dat_verify.c @@ -0,0 +1,45 @@ +#include "redump_dat.h" + +#include +#include + +int main(int argc, char **argv) { + redump_verify_result result; + int ok; + + if (argc != 2) { + fprintf(stderr, "Usage: %s \n", + argc > 0 ? argv[0] : "test_redump_dat_verify"); + return 64; + } + + redump_verify_result_init(&result); + ok = redump_verify_dat_file( + argv[1], + UINT64_C(1459978240), + "01b52739", + "85a525df1481d0ad67d8761f832dca12", + "06eb6d15b4d7f90ec0fed9ce9a77db41358d74ed", + &result + ); + + printf("status=%s\n", redump_verify_status_string(result.status)); + printf("entries_scanned=%lu\n", result.entries_scanned); + printf("exact_matches=%lu\n", result.exact_matches); + printf("game_name=%s\n", result.game_name); + printf("rom_name=%s\n", result.rom_name); + + if (!ok || result.status != REDUMP_VERIFY_MATCH) + return 1; + if (result.exact_matches < 1) + return 2; + if (strcmp(result.expected_crc32, "01b52739") != 0) + return 3; + if (strcmp(result.expected_md5, "85a525df1481d0ad67d8761f832dca12") != 0) + return 4; + if (strcmp(result.expected_sha1, + "06eb6d15b4d7f90ec0fed9ce9a77db41358d74ed") != 0) + return 5; + + return 0; +} diff --git a/tests/test_seed_diagnostics_contract.py b/tests/test_seed_diagnostics_contract.py new file mode 100644 index 0000000..a0647cd --- /dev/null +++ b/tests/test_seed_diagnostics_contract.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DISC_C = (ROOT / "libfriidump" / "disc.c").read_text(encoding="utf-8") +DISC_H = (ROOT / "libfriidump" / "disc.h").read_text(encoding="utf-8") +DVD_C = (ROOT / "libfriidump" / "dvd_drive.c").read_text(encoding="utf-8") +DVD_H = (ROOT / "libfriidump" / "dvd_drive.h").read_text(encoding="utf-8") +MAIN_C = (ROOT / "src" / "friidump.c").read_text(encoding="utf-8") + +checks = { + "always-visible seed diagnostic prefix": '[SEED-DIAG]' in DISC_C, + "seed block and sector evidence": 'seed_block_index' in DISC_H and 'sector_start' in DISC_H and 'sector_end' in DISC_H, + "retry evidence": 'int retry;' in DISC_H, + "prefetch stage": 'stage=prefetch' in DISC_C, + "streaming stage": '"streaming-read"' in DISC_C, + "header memdump stage": '"header-memdump"' in DISC_C, + "EDC memdump stage": '"edc-memdump"' in DISC_C, + "EDC validation stage": '"edc-unscramble-validation"' in DISC_C, + "split recovery stages": '"split-streaming-read"' in DISC_C and '"split-header-memdump"' in DISC_C and '"split-edc-memdump"' in DISC_C, + "transport command snapshot type": 'dvd_command_diagnostic' in DVD_H, + "transport result capture": 'transport_result' in DVD_C, + "Linux errno and SCSI evidence": 'saved_errno' in DVD_C and 'cgc.stat' in DVD_C and 'sense.sense_key' in DVD_C, + "Windows status and sense evidence": 'sptd -> ScsiStatus' in DVD_C and 'win_error' in DVD_C, + "public seed diagnostic getter": 'disc_get_seed_diagnostic' in DISC_H and 'disc_get_seed_diagnostic' in DISC_C, + "final seed diagnostic summary": 'Seed diagnostic final failure:' in MAIN_C, + "native report diagnostic note": 'Seed diagnostic: block %u/20' in MAIN_C, + "STOP UNIT after seed failure": 'STOP UNIT / spin-down after seed failure' in MAIN_C, + "STOP UNIT result note": 'STOP UNIT after seed failure succeeded.' in MAIN_C, +} + +failed = [] +for name, ok in checks.items(): + print(f"{'PASS' if ok else 'FAIL'}: {name}") + if not ok: + failed.append(name) + +if failed: + raise SystemExit("SEED DIAGNOSTICS CONTRACT: FAIL: " + ", ".join(failed)) + +print("SEED DIAGNOSTICS CONTRACT: PASS") diff --git a/tests/test_xbox_cancel_contract.py b/tests/test_xbox_cancel_contract.py new file mode 100644 index 0000000..961f3af --- /dev/null +++ b/tests/test_xbox_cancel_contract.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +import argparse +import re +from pathlib import Path + + +def require(text: str, token: str, label: str) -> None: + if token not in text: + raise SystemExit(f"FAIL: {label}: missing {token!r}") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--source-root", required=True) + args = parser.parse_args() + root = Path(args.source_root) + + bridge_h = (root / "libfriidump/xbox_ref_bridge.h").read_text() + bridge_c = (root / "libfriidump/xbox_ref_bridge.c").read_text() + dumper_c = (root / "libfriidump/dumper.c").read_text() + utils_c = (root / "libfriidump/xbox_ref/utils.c").read_text() + friidump_c = (root / "src/friidump.c").read_text() + msvc_rsp = (root / "msvc32_friidump.rsp").read_text() + + # Candidate18 removed the authRecovery parameter from the copied Windows + # helper but left three callers with a stale eighth argument. MSVC only + # warned (C4020), so enforce the exact seven-argument contract in source + # and make any future C4020 a production-build error. + call_matches = list( + re.finditer(r"DumpSectorRangeWithRetry\s*\((.*?)\)", utils_c, re.S) + ) + if len(call_matches) < 2: + raise SystemExit("FAIL: DumpSectorRangeWithRetry declaration/calls not found") + for index, match in enumerate(call_matches[1:], start=1): + argument_count = match.group(1).count(",") + 1 + if argument_count != 7: + raise SystemExit( + f"FAIL: DumpSectorRangeWithRetry call {index} has " + f"{argument_count} arguments; expected 7" + ) + require(msvc_rsp, "/we4020", "MSVC excess-argument warning is fatal") + + require(bridge_h, "typedef int (*xbox_ref_cancel_func)", "bridge callback type") + require(bridge_h, "int cancelled;", "result cancellation state") + require(bridge_h, "uint32_t completed_sectors;", "result partial progress") + require(bridge_c, "cancel, cancel_data, result", "bridge callback handoff") + require(dumper_c, "dumper_xbox_ref_cancel_requested", "dumper adapter") + if dumper_c.count("dumper_xbox_ref_cancel_requested, dmp") < 2: + raise SystemExit("FAIL: callback is not passed to both copied Xbox ISO and XISO paths") + if utils_c.count("XboxDumpCancelRequested()") < 5: + raise SystemExit("FAIL: copied Xbox long-running paths do not contain enough cancellation checks") + require(utils_c, "result->completed_sectors =", "partial sector finalization") + require( + utils_c, + "[HASH] Finalizing controlled partial Xbox output hashes", + "partial output hashing", + ) + require( + utils_c, + "result->cancelled &&", + "partial hash cancellation guard", + ) + require( + utils_c, + "CalculateFileHashes(filename", + "partial full-file hash calculation", + ) + require(utils_c, "g_xbox_dump_cancel = NULL;", "callback cleanup") + require(friidump_c, "if (xbox_result.cancelled)", "native-report cancellation merge") + require( + friidump_c, + "g_validation_summary.expected_output_sectors", + "Xbox expected-output geometry summary", + ) + require( + friidump_c, + "g_validation_summary.source_sectors", + "Xbox source geometry summary", + ) + require( + friidump_c, + "g_validation_summary.region", + "Xbox region summary", + ) + require( + friidump_c, + "xbox_result.title", + "Xbox title summary handoff", + ) + require( + friidump_c, + "xbox_result.media_id", + "Xbox media-ID summary handoff", + ) + + print("Xbox copied-dumper cancellation, hashing, and summary contract: PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_xbox_dvd_structure_contract.py b/tests/test_xbox_dvd_structure_contract.py new file mode 100755 index 0000000..79d0a7f --- /dev/null +++ b/tests/test_xbox_dvd_structure_contract.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Contract for the standard MMC READ DVD STRUCTURE CDB boundary.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + + +def require(text: str, token: str, label: str) -> None: + if token not in text: + raise SystemExit(f"FAIL: missing {label}: {token}") + print(f"PASS: {label}") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "--source-root", + type=Path, + default=Path(__file__).resolve().parents[1], + ) + args = parser.parse_args() + + source_root = args.source_root.resolve() + dvd_drive = ( + source_root / "libfriidump" / "dvd_drive.c" + ).read_text(encoding="utf-8") + windows_utils = ( + source_root / "libfriidump" / "xbox_ref" / "utils.c" + ).read_text(encoding="utf-8") + dumper = ( + source_root / "libfriidump" / "dumper.c" + ).read_text(encoding="utf-8") + + start = dvd_drive.index("int dvd_read_dvd_structure") + end = dvd_drive.index("\n}\n", start) + 3 + function = dvd_drive[start:end] + + require( + function, + "mmc.cmd[6] = layer;", + "READ DVD STRUCTURE layer is CDB byte 6", + ) + require( + function, + "mmc.cmd[7] = format;", + "READ DVD STRUCTURE format is CDB byte 7", + ) + require( + function, + "mmc.cmd[8] = (u_int8_t) ((extbufsize & 0xFF00) >> 8);", + "allocation length high byte is CDB byte 8", + ) + require( + function, + "mmc.cmd[9] = (u_int8_t) (extbufsize & 0x00FF);", + "allocation length low byte is CDB byte 9", + ) + require( + function, + "mmc.cmdlen = 12;", + "READ DVD STRUCTURE uses a 12-byte CDB", + ) + + if "mmc.cmd[11] = format;" in function: + raise SystemExit( + "FAIL: READ DVD STRUCTURE format is still written to control byte 11" + ) + print("PASS: READ DVD STRUCTURE control byte 11 is not used as Format") + + require( + windows_utils, + "sptd.Cdb[7] = 0x04;", + "copied Windows DMI request uses format byte 7", + ) + require( + dumper, + "[XBOX-DVD-STRUCTURE] format=0x04 name=DMI cdb_format_byte=7 result=%s", + "persistent DMI capture diagnostic", + ) + + print("XBOX READ DVD STRUCTURE CDB CONTRACT: PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_xbox_portable_metadata.c b/tests/test_xbox_portable_metadata.c new file mode 100644 index 0000000..5c1bf69 --- /dev/null +++ b/tests/test_xbox_portable_metadata.c @@ -0,0 +1,148 @@ +#include "xbox_portable.h" +#include "xbox_region.h" + +#include +#include +#include + +#define SECTOR_SIZE 2048U +#define MAX_SECTORS 512U + +static unsigned char g_sectors[MAX_SECTORS][SECTOR_SIZE]; +static unsigned char g_dmi[SECTOR_SIZE]; + +static void put_le16(unsigned char *p, unsigned int value) +{ + p[0] = (unsigned char)(value & 0xffU); + p[1] = (unsigned char)((value >> 8) & 0xffU); +} + +static void put_le32(unsigned char *p, unsigned long value) +{ + p[0] = (unsigned char)(value & 0xffUL); + p[1] = (unsigned char)((value >> 8) & 0xffUL); + p[2] = (unsigned char)((value >> 16) & 0xffUL); + p[3] = (unsigned char)((value >> 24) & 0xffUL); +} + +static int fake_read10(void *context, + uint32_t lba, + uint32_t sectors, + unsigned char *buffer, + size_t buffer_size) +{ + uint32_t index; + (void)context; + + if (!buffer || sectors == 0 || lba + sectors > MAX_SECTORS) + return -1; + if (buffer_size < (size_t)sectors * SECTOR_SIZE) + return -1; + + for (index = 0; index < sectors; ++index) + memcpy(buffer + ((size_t)index * SECTOR_SIZE), + g_sectors[lba + index], + SECTOR_SIZE); + return 0; +} + +static int fake_read_structure(void *context, + uint8_t format, + uint8_t layer, + unsigned char *buffer, + size_t buffer_size) +{ + (void)context; + (void)layer; + + if (format != 0x04U || !buffer || buffer_size < sizeof(g_dmi)) + return -1; + memcpy(buffer, g_dmi, sizeof(g_dmi)); + return 0; +} + +static void require_true(int condition, const char *message) +{ + if (!condition) { + fprintf(stderr, "FAIL: %s\n", message); + exit(1); + } + printf("PASS: %s\n", message); +} + +static void write_utf16_title(unsigned char *certificate, const unsigned short *title) +{ + size_t index; + for (index = 0; index < 40U && title[index] != 0; ++index) + put_le16(certificate + 0x00CU + (index * 2U), title[index]); +} + +int main(void) +{ + static const unsigned short title[] = { + 'R','e','d',' ','F','a','c','t','i','o','n',0x00AE,' ','I','I',0 + }; + xbox_portable_metadata metadata; + unsigned char *volume; + unsigned char *root; + unsigned char *xbe; + unsigned char *certificate; + + memset(g_sectors, 0, sizeof(g_sectors)); + memset(g_dmi, 0, sizeof(g_dmi)); + + volume = g_sectors[32]; + memcpy(volume, "MICROSOFT*XBOX*MEDIA", 20); + put_le32(volume + 0x14U, 100U); + put_le32(volume + 0x18U, SECTOR_SIZE); + + root = g_sectors[100]; + put_le16(root, 0U); + put_le16(root + 2U, 0U); + put_le32(root + 4U, 200U); + put_le32(root + 8U, 2314240U); + root[12U] = 0U; + root[13U] = 11U; + memcpy(root + 14U, "default.xbe", 11U); + root[28U] = 0U; + + xbe = g_sectors[200]; + put_le32(xbe, 0x48454258UL); + put_le32(xbe + 0x104U, 0x00010000UL); + put_le32(xbe + 0x118U, 0x00010800UL); + + certificate = g_sectors[201]; + put_le32(certificate + 0x008U, 0x54510005UL); + write_utf16_title(certificate, title); + put_le32(certificate + 0x09CU, 0x00000020UL); + put_le32(certificate + 0x0A0U, XB_REGION_US_CANADA); + put_le32(certificate + 0x0A4U, 0x00000001UL); + put_le32(certificate + 0x0A8U, 0U); + put_le32(certificate + 0x0ACU, 1U); + + memcpy(g_dmi + 8U, "TQ00501A", 8U); + + require_true( + xbox_portable_read_metadata(fake_read10, + fake_read_structure, + NULL, + &metadata) == 1, + "portable Xbox metadata parser accepts a valid XDFS/XBE fixture"); + require_true(metadata.valid == 1, + "portable Xbox metadata result is marked valid"); + require_true(metadata.xdfs_volume_lba == 32U, + "standard Xbox game-view XDFS volume LBA is retained"); + require_true(strcmp(metadata.title, "Red Faction\xC2\xAE II") == 0, + "UTF-16 XBE title is converted to UTF-8"); + require_true(strcmp(metadata.media_id, "TQ00501A") == 0, + "DMI media ID is retained"); + require_true(metadata.title_id == 0x54510005UL, + "XBE title ID is retained"); + require_true(metadata.game_region == XB_REGION_US_CANADA, + "raw XBE GameRegion mask is retained"); + require_true(strcmp(metadata.region, "North America") == 0, + "XBE GameRegion is formatted consistently"); + + printf("XBOX PORTABLE METADATA CONTRACT: PASS\n"); + return 0; +} diff --git a/tests/validate_native_reports.py b/tests/validate_native_reports.py new file mode 100644 index 0000000..88628b8 --- /dev/null +++ b/tests/validate_native_reports.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""Validate FriiDump native report fixtures against the v1 contract.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +import uuid +from pathlib import Path +from typing import Any + + +def load_json(path: Path) -> Any: + try: + return json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as exc: + raise RuntimeError(f"Could not decode {path}: {exc}") from exc + + +def require(condition: bool, message: str) -> None: + if not condition: + raise RuntimeError(message) + + +def semantic_fixture_checks(path: Path, report: dict[str, Any]) -> None: + run = report["run"] + dump = report["dump"] + notes = report["notes"] + + parsed_uuid = uuid.UUID(run["run_id"]) + require(parsed_uuid.int != 0, f"{path.name}: nil run UUID") + + scope_notes = [ + note for note in notes + if isinstance(note, str) and note.lower().startswith("measurement scope:") + ] + + if dump["attempted"]: + require(len(scope_notes) == 1, + f"{path.name}: attempted dump must have exactly one measurement scope") + + artifacts = report["artifacts"] + dump_artifacts = [ + artifact for artifact in artifacts + if isinstance(artifact, dict) and artifact.get("type") == "dump_output" + ] + + if dump["attempted"] and dump.get("output_path"): + require(len(dump_artifacts) == 1, + f"{path.name}: attempted dump with output must have one dump_output artifact") + artifact = dump_artifacts[0] + require(artifact["path"] == dump["output_path"], + f"{path.name}: dump_output path mismatch") + if dump.get("byte_count") is not None: + require(artifact["bytes"] == dump["byte_count"], + f"{path.name}: dump_output byte count mismatch") + if report["hashes"].get("sha256") is not None: + require(artifact["sha256"] == report["hashes"]["sha256"], + f"{path.name}: dump_output SHA-256 mismatch") + + if path.name == "success-gamecube.friidump.json": + require(run == {**run, "test_type": "full_dump", "result": "pass"}, + f"{path.name}: success outcome mismatch") + require("Measurement scope: full_optical_payload." in notes, + f"{path.name}: optical scope missing") + + if path.name == "success-xbox-assembled.friidump.json": + require("Measurement scope: assembled_output." in notes, + f"{path.name}: assembled-output scope missing") + require(report["drive"]["firmware_modified"] is True, + f"{path.name}: modified-firmware identity missing") + require(report["media"]["region"] == "North America", + f"{path.name}: Xbox XBE region mismatch") + + if path.name == "modified-firmware.friidump.json": + require("UTF-8 preservation fixture: Pokémon." in notes, + f"{path.name}: UTF-8 text was not preserved") + + if path.name == "partial-cancelled.friidump.json": + require(run["test_type"] == "partial_dump" and run["result"] == "partial", + f"{path.name}: cancellation outcome mismatch") + require(dump["result"] == "partial" and + dump["failure_stage"] == "user_cancelled", + f"{path.name}: cancellation dump state mismatch") + require("Measurement scope: partial_progress." in notes, + f"{path.name}: cancellation scope missing") + + if path.name == "seed-failure.friidump.json": + require(run["test_type"] == "seed_only" and run["result"] == "fail", + f"{path.name}: seed-failure outcome mismatch") + require(dump["result"] == "not_attempted", + f"{path.name}: seed failure must not claim a dump") + + if path.name == "no-media.friidump.json": + require(run["test_type"] == "diagnostic_no_media" and + run["result"] == "not_applicable", + f"{path.name}: no-media outcome mismatch") + + +def run_php_validator(php_validator: Path, report_path: Path) -> tuple[int, int]: + result = subprocess.run( + ["php", str(php_validator), str(report_path)], + check=False, + capture_output=True, + text=True, + ) + if result.returncode not in (0, 1): + raise RuntimeError( + f"PHP validator failed for {report_path}:\n{result.stdout}\n{result.stderr}" + ) + try: + decoded = json.loads(result.stdout) + except json.JSONDecodeError as exc: + raise RuntimeError( + f"PHP validator returned invalid JSON for {report_path}: {exc}" + ) from exc + if not decoded.get("valid"): + raise RuntimeError( + f"PHP validator rejected {report_path}: {decoded.get('errors')}" + ) + return len(decoded.get("errors", [])), len(decoded.get("warnings", [])) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--fixtures", type=Path, required=True) + parser.add_argument( + "--schema", + type=Path, + default=Path(__file__).parent / "contracts" / + "friidump-test-result.v1.schema.json", + ) + parser.add_argument("--php-validator", type=Path) + parser.add_argument( + "--allow-arbitrary", + action="store_true", + help="Validate any report set without requiring the built-in fixture filenames", + ) + args = parser.parse_args() + + try: + from jsonschema import Draft202012Validator + except ImportError as exc: + raise RuntimeError( + "The Python 'jsonschema' package is required for strict contract validation" + ) from exc + + schema = load_json(args.schema) + validator = Draft202012Validator(schema) + reports = sorted(args.fixtures.rglob("*.friidump.json")) + require(bool(reports), f"No .friidump.json fixtures found under {args.fixtures}") + + seen_run_ids: set[str] = set() + php_warning_total = 0 + + for path in reports: + report = load_json(path) + errors = sorted(validator.iter_errors(report), key=lambda item: list(item.path)) + if errors: + rendered = "\n".join( + f" {list(error.path)}: {error.message}" for error in errors + ) + raise RuntimeError(f"JSON Schema rejected {path}:\n{rendered}") + + run_id = report["run"]["run_id"] + require(run_id not in seen_run_ids, f"Duplicate fixture run UUID: {run_id}") + seen_run_ids.add(run_id) + semantic_fixture_checks(path, report) + + php_summary = "" + if args.php_validator: + errors_count, warnings_count = run_php_validator(args.php_validator, path) + php_warning_total += warnings_count + php_summary = f", php errors={errors_count}, warnings={warnings_count}" + + print(f"PASS {path.relative_to(args.fixtures)}: schema + fixture semantics{php_summary}") + + if not args.allow_arbitrary: + expected_names = { + "success-gamecube.friidump.json", + "success-xbox-assembled.friidump.json", + "partial-gamecube.friidump.json", + "partial-cancelled.friidump.json", + "seed-failure.friidump.json", + "no-media.friidump.json", + "modified-firmware.friidump.json", + "identity-no-overwrite.friidump.json", + } + observed_names = {path.name for path in reports} + missing = expected_names - observed_names + require(not missing, f"Required fixture reports are missing: {sorted(missing)}") + + print() + print(f"Native report validation: PASS ({len(reports)} reports)") + if args.php_validator: + print(f"Database semantic validator warnings: {php_warning_total}") + print("The single expected warning is the honest seed-failure fixture without a disc title.") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except RuntimeError as exc: + print(f"FAIL: {exc}", file=sys.stderr) + raise SystemExit(1) diff --git a/validation/README.md b/validation/README.md new file mode 100644 index 0000000..87d0316 --- /dev/null +++ b/validation/README.md @@ -0,0 +1,75 @@ +# FriiDump 0.5.3.16-pf1-candidate21 validation + +Candidate21 preserves Candidate13's seed diagnostics and Candidate12's DAT and +Linux-lock corrections while making the Linux raw-I/O requirement explicit. + +## Linux build + +Run from the source root: + +```bash +bash ./validation/friidump-v0.5.3.16-pf1-candidate21-linux-live-validation.sh build +``` + +The build gate compiles FriiDump and the native-report fixtures, validates all +13 report fixtures, exercises Redump DAT discovery/matching, runs the Linux Xbox +portable contract, seed-diagnostics contract, raw-I/O parser test, and the +raw-I/O source/documentation contract. + +The build deliberately does not apply file capabilities. It prints a warning +that vendor-command hardware runs require a separate capability phase. + +Expected final line: + +```text +FRIIDUMP CANDIDATE21 LINUX BUILD: PASS +``` + +## Linux raw-I/O capability + +After the exact executable has been built and reviewed: + +```bash +bash ./validation/friidump-v0.5.3.16-pf1-candidate21-linux-live-validation.sh capability +``` + +This invokes the maintained helper, applies only `cap_sys_rawio=ep`, verifies +it, and prints the executable SHA-256 and capability state. + +Expected final line: + +```text +FRIIDUMP CANDIDATE21 LINUX CAPABILITY: PASS +``` + +Rebuilding or replacing the executable clears this capability. + +## Windows + +Run the Candidate21 Windows validator from Windows PowerShell 5.1 with Visual +Studio 2019 Build Tools x86 available: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass ` + -File .\validation\friidump-v0.5.3.16-pf1-candidate21-windows-live-validation.ps1 ` + -Phase Build ` + -SourceRoot (Get-Location).Path +``` + +Expected final line: + +```text +FRIIDUMP CANDIDATE21 WINDOWS BUILD: PASS +``` + +## Live gate + +A Linux vendor-command live test is valid only when its evidence records: + +- normal-user identity; +- exact executable SHA-256; +- `cap_sys_rawio=ep` on that executable; +- runtime raw-I/O preflight PASS; +- selected optical device identity; +- dump or diagnostic result; +- STOP UNIT result. diff --git a/validation/friidump-linux-rawio-capability.sh b/validation/friidump-linux-rawio-capability.sh new file mode 100644 index 0000000..0954249 --- /dev/null +++ b/validation/friidump-linux-rawio-capability.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +action="${1:-status}" +source_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +default_binary="$source_root/build-candidate21-linux/src/friidump" +binary="${2:-$default_binary}" +expected_version="0.5.3.16-pf1-candidate21" + +fail() { + printf 'ERROR: %s\n' "$*" >&2 + exit 1 +} + +require_command() { + local name="$1" + local package="$2" + + command -v "$name" >/dev/null 2>&1 || { + printf 'Missing required command: %s\n' "$name" >&2 + printf 'Ubuntu package hint: sudo apt-get install -y %s\n' "$package" >&2 + exit 1 + } +} + +resolve_binary() { + local resolved help_output help_status + + [[ -f "$binary" ]] || fail "FriiDump executable does not exist: $binary" + [[ -x "$binary" ]] || fail "FriiDump executable is not executable: $binary" + [[ ! -L "$binary" ]] || fail "Refusing a symbolic-link executable: $binary" + + resolved="$(readlink -f -- "$binary")" + [[ -n "$resolved" && -f "$resolved" ]] || fail "Unable to resolve executable: $binary" + + set +e + help_output="$("$resolved" --help 2>&1)" + help_status=$? + set -e + + [[ "$help_status" -eq 1 ]] || fail "Unexpected --help exit status from $resolved: $help_status" + grep -Fq "FriiDump $expected_version" <<<"$help_output" || + fail "Executable version mismatch: $resolved" + + printf '%s\n' "$resolved" +} + +capability_text() { + getcap -n -- "$1" 2>/dev/null || true +} + +capability_present() { + local text + text="$(capability_text "$1")" + [[ "$text" =~ cap_sys_rawio(\+|=)(ep|eip|p|ip) ]] || + [[ "$text" == *"cap_sys_rawio=ep"* ]] +} + +print_identity() { + local resolved="$1" + printf 'Executable: %s\n' "$resolved" + printf 'SHA-256: %s\n' "$(sha256sum "$resolved" | awk '{print $1}')" + printf 'File mode: ' + stat -c '%A (%a) owner=%U:%G' "$resolved" + printf 'Capability: %s\n' "$(capability_text "$resolved")" +} + +install_capability() { + local resolved="$1" + local before_hash after_hash + + before_hash="$(sha256sum "$resolved" | awk '{print $1}')" + + printf '\nInstalling the minimum Linux authority required for vendor SCSI commands.\n' + printf 'Only the exact executable receives CAP_SYS_RAWIO; FriiDump remains a normal-user process.\n' + printf 'Do not run FriiDump itself with sudo.\n\n' + + sudo setcap cap_sys_rawio=ep "$resolved" + + after_hash="$(sha256sum "$resolved" | awk '{print $1}')" + [[ "$after_hash" == "$before_hash" ]] || + fail "Executable bytes changed while applying the file capability." + + capability_present "$resolved" || { + print_identity "$resolved" + fail "CAP_SYS_RAWIO verification failed." + } + + print_identity "$resolved" + printf '\nFRIIDUMP LINUX RAW-I/O CAPABILITY INSTALL: PASS\n' + printf 'WARNING: rebuilding, replacing, copying, or re-extracting this executable clears the capability.\n' +} + +remove_capability() { + local resolved="$1" + + if ! capability_present "$resolved"; then + print_identity "$resolved" + printf '\nFRIIDUMP LINUX RAW-I/O CAPABILITY REMOVE: NOT PRESENT\n' + return 0 + fi + + sudo setcap -r "$resolved" + + if capability_present "$resolved"; then + print_identity "$resolved" + fail "CAP_SYS_RAWIO remains present after removal." + fi + + print_identity "$resolved" + printf '\nFRIIDUMP LINUX RAW-I/O CAPABILITY REMOVE: PASS\n' +} + +require_command getcap libcap2-bin +require_command sha256sum coreutils +require_command readlink coreutils +require_command stat coreutils + +resolved_binary="$(resolve_binary)" + +case "$action" in + status) + print_identity "$resolved_binary" + if capability_present "$resolved_binary"; then + printf '\nFRIIDUMP LINUX RAW-I/O CAPABILITY STATUS: PRESENT\n' + else + printf '\nFRIIDUMP LINUX RAW-I/O CAPABILITY STATUS: MISSING\n' + printf 'Vendor-command operations will be refused before seed retrieval or unlock.\n' + printf 'Install with:\n bash %q install %q\n' "$0" "$resolved_binary" + exit 1 + fi + ;; + install) + require_command setcap libcap2-bin + require_command sudo sudo + install_capability "$resolved_binary" + ;; + remove) + require_command setcap libcap2-bin + require_command sudo sudo + remove_capability "$resolved_binary" + ;; + *) + printf 'Usage: %s {status|install|remove} [friidump-executable]\n' "$0" >&2 + exit 2 + ;; +esac diff --git a/validation/friidump-v0.5.3.16-pf1-candidate10-windows-live-validation.ps1 b/validation/friidump-v0.5.3.16-pf1-candidate10-windows-live-validation.ps1 new file mode 100644 index 0000000..ff27c79 --- /dev/null +++ b/validation/friidump-v0.5.3.16-pf1-candidate10-windows-live-validation.ps1 @@ -0,0 +1,856 @@ +[CmdletBinding()] +param( + [ValidateSet( + 'Build', + 'NoMedia', + 'GameCube', + 'XboxIso', + 'XboxXiso', + 'XboxCancel', + 'PrepareCancellation', + 'ValidateReports', + 'Collect' + )] + [string]$Phase = 'Build', + + [string]$SourceRoot = (Get-Location).Path, + + [string]$Drive, + + [string]$ResultsRoot, + + [string]$OutputName, + + [string]$FirmwareModifiedNote, + + [bool]$UseMethod8 = $true, + + [string[]]$ExtraFriiDumpArgs = @(), + + [string]$PhpValidator +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$ExpectedVersion = '0.5.3.16-pf1-candidate10' +$ExpectedCommit = 'b03b227673ee00aec3c29d34038b3c86ee2d8b9a' +$ExpectedSchemaSha256 = 'd1e97cb3e88e0f5a94ccf9b9edf921554fc15278662e69420b8c1e564c600411' + +$SourceRoot = (Resolve-Path -LiteralPath $SourceRoot).Path +if ([string]::IsNullOrWhiteSpace($ResultsRoot)) { + $ResultsRoot = Join-Path $SourceRoot '_native-report-live-validation' +} +$ResultsRoot = [System.IO.Path]::GetFullPath($ResultsRoot) + +$Exe = Join-Path $SourceRoot 'friidump.exe' +$BuildScript = Join-Path $SourceRoot 'build_msvc32.cmd' +$FixtureBuildScript = Join-Path $SourceRoot 'build_msvc32_native_report_tests.cmd' +$Validator = Join-Path $SourceRoot 'tests\validate_native_reports.py' +$Schema = Join-Path $SourceRoot 'tests\contracts\friidump-test-result.v1.schema.json' +$SourceManifest = Join-Path $SourceRoot 'SOURCE_SHA256SUMS.txt' +$MainResponseFile = Join-Path $SourceRoot 'msvc32_friidump.rsp' +$FixtureResponseFile = Join-Path $SourceRoot 'msvc32_native_report_tests.rsp' + +function Assert-True { + param( + [Parameter(Mandatory)] + [bool]$Condition, + [Parameter(Mandatory)] + [string]$Message + ) + + if (-not $Condition) { + throw $Message + } +} + +function Assert-LastExitCode { + param( + [Parameter(Mandatory)] + [string]$Operation + ) + + if ($LASTEXITCODE -ne 0) { + throw "$Operation failed with exit code $LASTEXITCODE." + } +} + +function Get-Sha256 { + param([Parameter(Mandatory)][string]$Path) + + return (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant() +} + +function Test-SourceManifest { + if (-not (Test-Path -LiteralPath $SourceManifest -PathType Leaf)) { + Write-Host '[INFO] SOURCE_SHA256SUMS.txt is not present; package-manifest validation skipped.' + return + } + + $checked = 0 + foreach ($line in Get-Content -LiteralPath $SourceManifest) { + if ([string]::IsNullOrWhiteSpace($line)) { + continue + } + if ($line -notmatch '^([0-9a-fA-F]{64}) (.+)$') { + throw "Malformed source-manifest line: $line" + } + + $expected = $Matches[1].ToLowerInvariant() + $relative = $Matches[2] -replace '/', '\' + $path = Join-Path $SourceRoot $relative + Assert-True (Test-Path -LiteralPath $path -PathType Leaf) "Manifest file is missing: $relative" + $actual = Get-Sha256 $path + Assert-True ($actual -eq $expected) "Manifest SHA-256 mismatch: $relative" + $checked++ + } + + Write-Host "Source manifest: PASS ($checked files)" +} + +function Test-MsvcSplitLinkContract { + param( + [Parameter(Mandatory)][string]$ResponsePath, + [Parameter(Mandatory)][string]$BuildScriptPath, + [Parameter(Mandatory)][string]$ExpectedInvocation, + [Parameter(Mandatory)][string]$Label + ) + + Assert-True (Test-Path -LiteralPath $ResponsePath -PathType Leaf) "$Label response file is missing: $ResponsePath" + Assert-True (Test-Path -LiteralPath $BuildScriptPath -PathType Leaf) "$Label build script is missing: $BuildScriptPath" + + $lines = @( + Get-Content -LiteralPath $ResponsePath | + ForEach-Object { $_.Trim() } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + + Assert-True (-not ($lines -icontains '/link')) "$Label response file must not contain /link. The linker boundary must be on the cl.exe command line." + Assert-True (-not ($lines -icontains 'ole32.lib')) "$Label response file must not contain ole32.lib. The library must follow /link on the cl.exe command line." + + $buildText = Get-Content -Raw -LiteralPath $BuildScriptPath + Assert-True ($buildText.Contains($ExpectedInvocation)) "$Label build script is missing the required split compile/link invocation: $ExpectedInvocation" + + Write-Host "$Label MSVC command-line linker boundary: PASS" +} + +function Test-PythonValidatorAvailable { + & py -3 -c 'import jsonschema' 2>$null + if ($LASTEXITCODE -ne 0) { + throw 'Python 3 with jsonschema is required. Install it with: py -3 -m pip install jsonschema' + } +} + +function Invoke-ReportValidation { + param([Parameter(Mandatory)][string]$ReportRoot) + + Assert-True (Test-Path -LiteralPath $Validator -PathType Leaf) "Validator is missing: $Validator" + Assert-True (Test-Path -LiteralPath $Schema -PathType Leaf) "Schema is missing: $Schema" + Assert-True ((Get-Sha256 $Schema) -eq $ExpectedSchemaSha256) 'Bundled schema SHA-256 mismatch.' + + Test-PythonValidatorAvailable + + $arguments = @( + '-3', + $Validator, + '--fixtures', $ReportRoot, + '--schema', $Schema, + '--allow-arbitrary' + ) + + if (-not [string]::IsNullOrWhiteSpace($PhpValidator)) { + Assert-True (Test-Path -LiteralPath $PhpValidator -PathType Leaf) "PHP validator is missing: $PhpValidator" + $arguments += @('--php-validator', $PhpValidator) + } + + & py @arguments + Assert-LastExitCode 'Strict native-report validation' +} + +function Get-NewReport { + param( + [Parameter(Mandatory)][string]$ReportDirectory, + [Parameter(Mandatory)][datetime]$Started + ) + + $reports = @( + Get-ChildItem -LiteralPath $ReportDirectory -Recurse -Filter '*.friidump.json' -File | + Where-Object { $_.LastWriteTimeUtc -ge $Started.ToUniversalTime().AddSeconds(-2) } | + Sort-Object LastWriteTimeUtc + ) + + Assert-True ($reports.Count -eq 1) "Expected exactly one new report; found $($reports.Count)." + return $reports[0] +} + +function Read-Report { + param([Parameter(Mandatory)][string]$Path) + + return Get-Content -Raw -LiteralPath $Path | ConvertFrom-Json +} + +function Test-CommonReportIdentity { + param( + [Parameter(Mandatory)]$Report, + [Parameter(Mandatory)][string]$ReportPath + ) + + Assert-True ($Report.schema -eq 'friidump-test-result.v1') "${ReportPath}: schema mismatch." + Assert-True ($Report.generator.name -eq 'FriiDump') "${ReportPath}: generator name mismatch." + Assert-True ($Report.generator.version -eq $ExpectedVersion) "${ReportPath}: generator version mismatch." + Assert-True ($Report.generator.build_commit -eq $ExpectedCommit) "${ReportPath}: build commit mismatch." + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.run.run_id)) "${ReportPath}: run UUID missing." + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.drive.model)) "${ReportPath}: drive model missing." + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.drive.firmware_revision)) "${ReportPath}: firmware revision missing." + + if ([string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) { + Assert-True ($Report.drive.firmware_modified -eq $false) "${ReportPath}: omitted --firmware-modified must assume stock firmware." + Assert-True ($null -eq $Report.drive.modification_note) "${ReportPath}: stock firmware must not have a modification note." + } + else { + Assert-True ($Report.drive.firmware_modified -eq $true) "${ReportPath}: modified-firmware flag was not recorded." + Assert-True ([string]$Report.drive.modification_note -eq $FirmwareModifiedNote) "${ReportPath}: modified-firmware explanation mismatch." + } +} + +function Test-DumpOutputIdentity { + param( + [Parameter(Mandatory)]$Report, + [Parameter(Mandatory)][string]$ExpectedOutput + ) + + $resolvedOutput = [System.IO.Path]::GetFullPath($ExpectedOutput) + Assert-True (Test-Path -LiteralPath $resolvedOutput -PathType Leaf) "Dump output is missing: $resolvedOutput" + + $actualBytes = (Get-Item -LiteralPath $resolvedOutput).Length + Assert-True ([int64]$Report.dump.byte_count -eq $actualBytes) 'Report byte count does not match the output file.' + Assert-True ([string]$Report.dump.output_path -eq $resolvedOutput) 'Report output path does not match the requested path.' + + $artifacts = @($Report.artifacts | Where-Object { $_.type -eq 'dump_output' }) + Assert-True ($artifacts.Count -eq 1) 'Exactly one dump_output artifact is required.' + Assert-True ([string]$artifacts[0].path -eq $resolvedOutput) 'dump_output artifact path mismatch.' + Assert-True ([int64]$artifacts[0].bytes -eq $actualBytes) 'dump_output artifact byte count mismatch.' + + if ($null -ne $Report.hashes.sha256) { + $actualSha = Get-Sha256 $resolvedOutput + Assert-True ([string]$Report.hashes.sha256 -eq $actualSha) 'Report SHA-256 does not match the output file.' + Assert-True ([string]$artifacts[0].sha256 -eq $actualSha) 'Artifact SHA-256 does not match the output file.' + } +} + +function New-RunDirectory { + param([Parameter(Mandatory)][string]$Name) + + $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ') + $path = Join-Path $ResultsRoot "$stamp-$Name" + New-Item -ItemType Directory -Force -Path $path | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $path 'reports') | Out-Null + return $path +} + +function Invoke-FriiDumpRun { + param( + [Parameter(Mandatory)][string[]]$Arguments, + [Parameter(Mandatory)][string]$RunDirectory, + [Parameter(Mandatory)][bool]$ExpectSuccess + ) + + $started = (Get-Date).ToUniversalTime() + + Write-Host "Working directory: $RunDirectory" + Write-Host "Command: $Exe $($Arguments -join ' ')" + Write-Host 'Console mode: direct live FriiDump output; FriiDump native .log is the evidence log.' + + # Do not route an interactive FriiDump run through a PowerShell + # pipeline. Tee-Object buffers carriage-return progress updates, turns + # native stderr into PowerShell error records, and also contaminates a + # function's return stream with every line emitted by the child. + # + # Start FriiDump with inherited console handles instead. This preserves + # real-time progress output and leaves this function with exactly one + # return object. + function ConvertTo-WindowsCommandLineArgument { + param([AllowEmptyString()][string]$Value) + + if ($Value.Length -gt 0 -and $Value -notmatch '[\s"]') { + return $Value + } + + $builder = New-Object System.Text.StringBuilder + [void]$builder.Append('"') + $backslashes = 0 + + foreach ($character in $Value.ToCharArray()) { + if ($character -eq '\') { + $backslashes++ + continue + } + + if ($character -eq '"') { + [void]$builder.Append(('\' * (($backslashes * 2) + 1))) + [void]$builder.Append('"') + $backslashes = 0 + continue + } + + if ($backslashes -gt 0) { + [void]$builder.Append(('\' * $backslashes)) + $backslashes = 0 + } + + [void]$builder.Append($character) + } + + if ($backslashes -gt 0) { + [void]$builder.Append(('\' * ($backslashes * 2))) + } + + [void]$builder.Append('"') + return $builder.ToString() + } + + $argumentLine = ( + $Arguments | + ForEach-Object { + ConvertTo-WindowsCommandLineArgument ([string]$_) + } + ) -join ' ' + + $startInfo = New-Object System.Diagnostics.ProcessStartInfo + $startInfo.FileName = $Exe + $startInfo.Arguments = $argumentLine + $startInfo.WorkingDirectory = $RunDirectory + $startInfo.UseShellExecute = $false + $startInfo.CreateNoWindow = $false + $startInfo.RedirectStandardOutput = $false + $startInfo.RedirectStandardError = $false + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $startInfo + + try { + [void]$process.Start() + $process.WaitForExit() + $exitCode = $process.ExitCode + } + finally { + $process.Dispose() + } + + Set-Content -LiteralPath (Join-Path $RunDirectory 'exit-code.txt') -Value $exitCode -Encoding ASCII + + if ($ExpectSuccess) { + Assert-True ($exitCode -eq 0) "FriiDump was expected to succeed but exited with $exitCode." + } + + return [pscustomobject]@{ + Started = $started + ExitCode = [int]$exitCode + Log = $null + } +} + +function Add-ModifiedFirmwareArgument { + param([string[]]$Arguments) + + if (-not [string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) { + return $Arguments + @('--firmware-modified', $FirmwareModifiedNote) + } + return $Arguments +} + +New-Item -ItemType Directory -Force -Path $ResultsRoot | Out-Null + +switch ($Phase) { + 'Build' { + Assert-True (Test-Path -LiteralPath $BuildScript -PathType Leaf) "Build script is missing: $BuildScript" + Assert-True (Test-Path -LiteralPath $FixtureBuildScript -PathType Leaf) "Fixture build script is missing: $FixtureBuildScript" + Test-SourceManifest + + $XboxRegionHeader = Join-Path $SourceRoot 'libfriidump\xbox_region.h' + $XboxLegacyUtilsHeader = Join-Path $SourceRoot 'libfriidump\xbox_ref\utils.h' + $XboxRegionHeaderText = Get-Content -Raw -LiteralPath $XboxRegionHeader + $XboxLegacyUtilsText = Get-Content -Raw -LiteralPath $XboxLegacyUtilsHeader + + Assert-True (-not $XboxRegionHeaderText.Contains('')) ` + 'xbox_region.h must not introduce stdbool.h into the copied Xbox header graph.' + Assert-True ($XboxRegionHeaderText.Contains('int xbox_region_format(')) ` + 'xbox_region.h must expose the MSVC-safe int return type.' + Assert-True ($XboxLegacyUtilsText.Contains('typedef int bool;')) ` + 'The copied Xbox legacy bool declaration changed unexpectedly.' + Write-Host 'Xbox region/MSVC bool isolation: PASS' + + $XboxCancelContract = Join-Path $SourceRoot 'tests\test_xbox_cancel_contract.py' + Assert-True (Test-Path -LiteralPath $XboxCancelContract -PathType Leaf) ` + 'Xbox copied-dumper cancellation contract test is missing.' + & py -3 $XboxCancelContract --source-root $SourceRoot + Assert-LastExitCode 'Xbox copied-dumper cancellation contract' + + Test-MsvcSplitLinkContract ` + -ResponsePath $MainResponseFile ` + -BuildScriptPath $BuildScript ` + -ExpectedInvocation 'cl @"%EFFECTIVE_RSP%" /link ole32.lib' ` + -Label 'FriiDump executable' + + Test-MsvcSplitLinkContract ` + -ResponsePath $FixtureResponseFile ` + -BuildScriptPath $FixtureBuildScript ` + -ExpectedInvocation 'cl @"%~dp0msvc32_native_report_tests.rsp" /link ole32.lib' ` + -Label 'Native-report fixtures' + + $env:FRIIDUMP_BUILD_COMMIT = $ExpectedCommit + + & $BuildScript clean + Assert-LastExitCode 'MSVC clean' + + & $BuildScript + Assert-LastExitCode 'MSVC candidate build' + + & $FixtureBuildScript + Assert-LastExitCode 'MSVC native-report fixture validation' + + Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'friidump.exe was not produced.' + + # Windows PowerShell 5.1 converts native stderr records into + # NativeCommandError objects when $ErrorActionPreference is Stop. + # FriiDump intentionally writes part of its startup/help text to + # stderr, so capture both streams through ProcessStartInfo instead + # of invoking the executable directly through the PowerShell + # pipeline. + $helpStartInfo = New-Object System.Diagnostics.ProcessStartInfo + $helpStartInfo.FileName = $Exe + $helpStartInfo.Arguments = '--help' + $helpStartInfo.UseShellExecute = $false + $helpStartInfo.RedirectStandardOutput = $true + $helpStartInfo.RedirectStandardError = $true + $helpStartInfo.CreateNoWindow = $true + + $helpProcess = New-Object System.Diagnostics.Process + $helpProcess.StartInfo = $helpStartInfo + + try { + [void]$helpProcess.Start() + + # Drain stdout and stderr concurrently. Reading either redirected + # stream synchronously before the other can deadlock when the + # second pipe fills while the process is still running. + $helpStdoutTask = $helpProcess.StandardOutput.ReadToEndAsync() + $helpStderrTask = $helpProcess.StandardError.ReadToEndAsync() + + $helpProcess.WaitForExit() + + $helpStdout = $helpStdoutTask.GetAwaiter().GetResult() + $helpStderr = $helpStderrTask.GetAwaiter().GetResult() + $helpExitCode = $helpProcess.ExitCode + } + finally { + $helpProcess.Dispose() + } + + $help = @( + $helpStdout, + $helpStderr + ) -join [Environment]::NewLine + + Assert-True ($helpExitCode -eq 1) "Candidate help command returned unexpected exit code $helpExitCode; FriiDump historically returns 1 for --help." + Assert-True ($help.Contains("FriiDump $ExpectedVersion")) 'Candidate version is missing from help output.' + foreach ($option in @('--report-json', '--report-dir', '--firmware-modified')) { + Assert-True ($help.Contains($option)) "Candidate help is missing $option." + } + Assert-True ($help.Contains('Native .friidump.json reports are created by default')) 'Default native-report behavior is missing from help output.' + Assert-True ($help.Contains('beside the final log')) 'Default beside-log report placement is missing from help output.' + Assert-True ($help.Contains('(may be combined with --report-json)')) 'Combined --report-json/--report-dir behavior is missing from help output.' + Assert-True ($help.Contains('omission assumes stock')) 'Stock-firmware default is missing from help output.' + + $buildRecord = @( + "Generated UTC: $((Get-Date).ToUniversalTime().ToString('o'))", + "Version: $ExpectedVersion", + "Commit: $ExpectedCommit", + "Executable: $Exe", + "Executable SHA-256: $(Get-Sha256 $Exe)", + 'MSVC build: PASS', + 'MSVC native-report fixtures: PASS', + 'Schema and semantic validation: PASS (13 reports)', + 'Help/version/options/default-location smoke: PASS', + 'Xbox copied-dumper cancellation, hashing, and summary contract: PASS' + ) + $recordPath = Join-Path $ResultsRoot 'build-validation.txt' + Set-Content -LiteralPath $recordPath -Value $buildRecord -Encoding UTF8 + + Write-Host '' + Write-Host 'FRIIDUMP CANDIDATE WINDOWS BUILD: PASS' + Write-Host "Build record: $recordPath" + break + } + + 'NoMedia' { + Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' + Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' + + Read-Host "Remove all media from $Drive, wait for the drive to settle, then press Enter" + $runDir = New-RunDirectory 'no-media-default-report' + $output = Join-Path $runDir 'no-media-placeholder.iso' + $args = @('-d', $Drive, '-i', $output) + $args = Add-ModifiedFirmwareArgument $args + $args += $ExtraFriiDumpArgs + + $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $false + $reportFile = Get-NewReport -ReportDirectory $runDir -Started $run.Started + Assert-True ($reportFile.Name -eq 'no-media-placeholder.iso.friidump.json') "Default report filename mismatch: $($reportFile.Name)" + Assert-True ($reportFile.DirectoryName -eq $runDir) "Default no-media report was not placed beside the final log/output path." + Invoke-ReportValidation $runDir + $report = Read-Report $reportFile.FullName + Test-CommonReportIdentity $report $reportFile.FullName + + Assert-True ($report.run.test_type -eq 'diagnostic_no_media') 'No-media test_type mismatch.' + Assert-True ($report.run.result -eq 'not_applicable') 'No-media run result mismatch.' + Assert-True ($report.dump.attempted -eq $false) 'No-media report must not claim a dump.' + Assert-True (-not (Test-Path -LiteralPath $output)) 'No-media test unexpectedly created an output image.' + + Write-Host '' + Write-Host 'NO-MEDIA DEFAULT NATIVE REPORT: PASS' + Write-Host "Report: $($reportFile.FullName)" + break + } + + 'GameCube' { + Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' + Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' + + $runDir = New-RunDirectory 'gamecube-full-dump' + if ([string]::IsNullOrWhiteSpace($OutputName)) { + $OutputName = 'Sonic Mega Collection.iso' + } + $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) + $args = @('-d', $Drive) + if ($UseMethod8) { + $args += '-8' + } + $args += @('-i', $output, '--report-dir', $reportDir) + $args = Add-ModifiedFirmwareArgument $args + $args += $ExtraFriiDumpArgs + + $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true + $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started + Invoke-ReportValidation $reportDir + $report = Read-Report $reportFile.FullName + Test-CommonReportIdentity $report $reportFile.FullName + Test-DumpOutputIdentity $report $output + + Assert-True ($report.media.platform -eq 'gamecube') 'Expected a GameCube report.' + Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'GameCube run outcome mismatch.' + Assert-True ($report.dump.result -eq 'pass') 'GameCube dump result mismatch.' + Assert-True (@($report.notes) -contains 'Measurement scope: full_optical_payload.') 'GameCube measurement scope mismatch.' + + Write-Host '' + Write-Host 'GAMECUBE FULL-DUMP NATIVE REPORT: PASS' + Write-Host "Report: $($reportFile.FullName)" + break + } + + 'XboxIso' { + Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' + Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' + + $runDir = New-RunDirectory 'xbox-redump-iso' + $reportDir = Join-Path $runDir 'reports' + if ([string]::IsNullOrWhiteSpace($OutputName)) { + $OutputName = 'Red Faction II [TQ00501A].iso' + } + $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) + $args = @('-d', $Drive, '-T', '4', '-i', $output, '--report-dir', $reportDir) + $args = Add-ModifiedFirmwareArgument $args + $args += $ExtraFriiDumpArgs + + $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true + $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started + Invoke-ReportValidation $reportDir + $report = Read-Report $reportFile.FullName + Test-CommonReportIdentity $report $reportFile.FullName + Test-DumpOutputIdentity $report $output + + Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.' + Assert-True ($report.media.region -eq 'North America') 'Expected the XBE GameRegion to report North America.' + Assert-True (@($report.notes) -contains 'Xbox XBE GameRegion mask: 0x00000001.') 'Expected the raw XBE GameRegion mask note.' + Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'Xbox ISO run outcome mismatch.' + Assert-True (@($report.notes) -contains 'Measurement scope: assembled_output.') 'Xbox ISO measurement scope mismatch.' + + Write-Host '' + Write-Host 'XBOX REDUMP-STYLE ISO NATIVE REPORT: PASS' + Write-Host "Report: $($reportFile.FullName)" + break + } + + 'XboxXiso' { + Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' + Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' + + $runDir = New-RunDirectory 'xbox-xiso' + $reportDir = Join-Path $runDir 'reports' + if ([string]::IsNullOrWhiteSpace($OutputName)) { + $OutputName = 'Red Faction II [TQ00501A].xiso' + } + $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) + $args = @('-d', $Drive, '-T', '4', '-X', $output, '--report-dir', $reportDir) + $args = Add-ModifiedFirmwareArgument $args + $args += $ExtraFriiDumpArgs + + $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true + $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started + Invoke-ReportValidation $reportDir + $report = Read-Report $reportFile.FullName + Test-CommonReportIdentity $report $reportFile.FullName + Test-DumpOutputIdentity $report $output + + Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.' + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.media.region)) 'Expected an XBE-derived Xbox region.' + Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'Xbox XISO run outcome mismatch.' + Assert-True (@($report.notes) -contains 'Measurement scope: assembled_output.') 'Xbox XISO measurement scope mismatch.' + + Write-Host '' + Write-Host 'XBOX XISO NATIVE REPORT: PASS' + Write-Host "Report: $($reportFile.FullName)" + break + } + + 'XboxCancel' { + Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' + Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' + + $runDir = New-RunDirectory 'xbox-controlled-cancellation' + $reportDir = Join-Path $runDir 'reports' + if ([string]::IsNullOrWhiteSpace($OutputName)) { + $OutputName = 'Red Faction II [TQ00501A].xiso' + } + $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) + $args = @('-d', $Drive, '-T', '4', '-X', $output, '--report-dir', $reportDir) + $args = Add-ModifiedFirmwareArgument $args + $args += $ExtraFriiDumpArgs + + Write-Host '' + Write-Host 'After Regions: North America appears and XISO progress begins, press Ctrl+C once.' + Write-Host '' + + $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $false + $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started + Invoke-ReportValidation $reportDir + $report = Read-Report $reportFile.FullName + Test-CommonReportIdentity $report $reportFile.FullName + Test-DumpOutputIdentity $report $output + + Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.' + Assert-True ($report.media.region -eq 'North America') 'Expected the XBE GameRegion to report North America.' + Assert-True (@($report.notes) -contains 'Xbox XBE GameRegion mask: 0x00000001.') 'Expected the raw XBE GameRegion mask note.' + Assert-True ($report.run.test_type -eq 'partial_dump' -and $report.run.result -eq 'partial') 'Xbox cancellation run outcome mismatch.' + Assert-True ($report.dump.result -eq 'partial') 'Xbox cancellation dump result mismatch.' + Assert-True ($report.dump.failure_stage -eq 'user_cancelled') 'Xbox cancellation failure stage mismatch.' + Assert-True ([int64]$report.dump.sector_count -gt 32) 'Xbox cancellation did not record meaningful partial progress.' + Assert-True ([int64]$report.dump.byte_count -eq ([int64]$report.dump.sector_count * 2048)) 'Xbox cancellation byte/sector accounting mismatch.' + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.crc32)) 'Partial Xbox CRC32 is missing.' + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.md5)) 'Partial Xbox MD5 is missing.' + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.sha1)) 'Partial Xbox SHA-1 is missing.' + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.sha256)) 'Partial Xbox SHA-256 is missing.' + + $nativeLog = "$output.log" + Assert-True (Test-Path -LiteralPath $nativeLog -PathType Leaf) "Xbox native log is missing: $nativeLog" + $logText = Get-Content -Raw -LiteralPath $nativeLog + foreach ($expectedText in @( + 'Game/Media ID.....: TQ00501A', + 'Title.............: Red Faction', + 'Region............: North America', + 'Seed read.........: N/A (Xbox reference auth path)', + 'Source sectors....: 3431264', + 'Expected output...: 1913920 sectors', + '[OK] Controlled partial Xbox output hashes captured.' + )) { + Assert-True ($logText.Contains($expectedText)) "Xbox summary/log is missing: $expectedText" + } + + Write-Host '' + Write-Host 'XBOX REGION, SUMMARY, GEOMETRY, CANCELLATION, AND HASHING: PASS' + Write-Host "Report: $($reportFile.FullName)" + break + } + + 'PrepareCancellation' { + Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' + Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' + + $runDir = New-RunDirectory 'controlled-cancellation' + if ([string]::IsNullOrWhiteSpace($OutputName)) { + $OutputName = 'controlled-cancellation.partial.iso' + } + $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) + $wrapper = Join-Path $runDir 'run-controlled-cancellation.ps1' + + $argLines = @("'-d'", "'$($Drive.Replace("'", "''"))'") + if ($UseMethod8) { + $argLines += "'-8'" + } + $argLines += @( + "'-i'", "'$($output.Replace("'", "''"))'" + ) + if (-not [string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) { + $argLines += @("'--firmware-modified'", "'$($FirmwareModifiedNote.Replace("'", "''"))'") + } + foreach ($arg in $ExtraFriiDumpArgs) { + $argLines += "'$($arg.Replace("'", "''"))'" + } + + $wrapperText = @" +`$ErrorActionPreference = 'Stop' +`$exe = '$($Exe.Replace("'", "''"))' +`$arguments = @( + $($argLines -join ",`r`n ") +) +Write-Host 'Allow data to be written, then press Ctrl+C once.' +& `$exe @arguments +exit `$LASTEXITCODE +"@ + Set-Content -LiteralPath $wrapper -Value $wrapperText -Encoding UTF8 + + Write-Host '' + Write-Host 'CONTROLLED CANCELLATION TEST PREPARED' + Write-Host 'Run this wrapper in a separate PowerShell window:' + Write-Host "powershell -NoProfile -ExecutionPolicy Bypass -File `"$wrapper`"" + Write-Host '' + Write-Host 'After the dump has written data, press Ctrl+C once. Then validate all generated reports with:' + Write-Host "powershell -NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" -Phase ValidateReports -SourceRoot `"$SourceRoot`" -ResultsRoot `"$ResultsRoot`"" + Write-Host '' + Write-Host 'Expected cancellation report: beside the final .log, partial_dump / partial, failure_stage=user_cancelled, scope=partial_progress.' + Write-Host 'The copied GDR-8050L Windows reference-dumper path is not yet an approved cancellation target.' + break + } + + 'ValidateReports' { + $reports = @(Get-ChildItem -LiteralPath $ResultsRoot -Recurse -Filter '*.friidump.json' -File) + Assert-True ($reports.Count -gt 0) "No native reports were found under $ResultsRoot." + Invoke-ReportValidation $ResultsRoot + + $runIds = @{} + foreach ($file in $reports) { + $report = Read-Report $file.FullName + Test-CommonReportIdentity $report $file.FullName + $runId = [string]$report.run.run_id + Assert-True (-not $runIds.ContainsKey($runId)) "Duplicate run UUID: $runId" + $runIds[$runId] = $true + + if ($report.dump.attempted -and $null -ne $report.dump.output_path) { + Test-DumpOutputIdentity $report ([string]$report.dump.output_path) + } + } + + $cancelReports = @( + $reports | Where-Object { + $document = Read-Report $_.FullName + $document.dump.failure_stage -eq 'user_cancelled' + } + ) + foreach ($file in $cancelReports) { + $report = Read-Report $file.FullName + Assert-True ($report.run.test_type -eq 'partial_dump') "$($file.FullName): cancellation test_type mismatch." + Assert-True ($report.run.result -eq 'partial') "$($file.FullName): cancellation run result mismatch." + Assert-True ($report.dump.result -eq 'partial') "$($file.FullName): cancellation dump result mismatch." + Assert-True (@($report.notes) -contains 'Measurement scope: partial_progress.') "$($file.FullName): cancellation scope mismatch." + + $outputPath = [System.IO.Path]::GetFullPath([string]$report.dump.output_path) + $finalLogPath = "$outputPath.log" + $reportParent = [System.IO.Path]::GetFullPath($file.DirectoryName).TrimEnd([char[]]"\/") + $logParent = [System.IO.Path]::GetFullPath((Split-Path -Parent $finalLogPath)).TrimEnd([char[]]"\/") + + Assert-True ($reportParent -eq $logParent) "$($file.FullName): default report is not beside the final FriiDump log." + Assert-True (Test-Path -LiteralPath $finalLogPath -PathType Leaf) "$($file.FullName): final FriiDump log is missing: $finalLogPath" + + $logText = Get-Content -Raw -LiteralPath $finalLogPath + Assert-True ($logText.Contains('Dump status.......: CANCELLED (PARTIAL)')) "${finalLogPath}: corrected cancellation status is missing." + Assert-True ($logText.Contains("Cancelled at......: sector $($report.dump.failure_sector)")) "${finalLogPath}: corrected cancellation-sector line is missing." + Assert-True ($logText.Contains("Completed sectors.: $($report.dump.sector_count)")) "${finalLogPath}: completed-sector line is missing or inconsistent." + Assert-True ($logText.Contains("Completed bytes...: $($report.dump.byte_count)")) "${finalLogPath}: completed-byte line is missing or inconsistent." + Assert-True (-not $logText.Contains('MiB ISO payload')) "${finalLogPath}: obsolete full-payload throughput wording remains." + + $averageMatch = [regex]::Match( + $logText, + 'Observed average\.\.: ([0-9]+(?:\.[0-9]+)?) MiB/h over ([0-9]+(?:\.[0-9]+)?) MiB completed output' + ) + Assert-True ($averageMatch.Success) "${finalLogPath}: corrected completed-output throughput line is missing." + + $culture = [System.Globalization.CultureInfo]::InvariantCulture + $observedRate = [double]::Parse($averageMatch.Groups[1].Value, $culture) + $observedMiB = [double]::Parse($averageMatch.Groups[2].Value, $culture) + $expectedMiB = [double]$report.dump.byte_count / 1MB + $expectedRate = $expectedMiB / [double]$report.dump.duration_seconds * 3600.0 + + Assert-True ([Math]::Abs($observedMiB - $expectedMiB) -lt 0.01) "${finalLogPath}: completed MiB does not match the report byte count." + Assert-True ([Math]::Abs($observedRate - $expectedRate) -lt 0.05) "${finalLogPath}: observed average does not use completed bytes and report duration." + + # Verify that the native report and human-readable summary agree + # with the authoritative Nintendo platform magic in sector 0. + # This catches HLDS drives that accept out-of-range dummy reads + # and therefore defeat the legacy capacity-only classifier. + $header = New-Object byte[] 32 + $stream = [System.IO.File]::OpenRead($outputPath) + + try { + $headerBytes = $stream.Read($header, 0, $header.Length) + } + finally { + $stream.Dispose() + } + + if ($headerBytes -ge 32) { + $wiiMagic = ( + ([uint32]$header[0x18] -shl 24) -bor + ([uint32]$header[0x19] -shl 16) -bor + ([uint32]$header[0x1a] -shl 8) -bor + [uint32]$header[0x1b] + ) + + $gameCubeMagic = ( + ([uint32]$header[0x1c] -shl 24) -bor + ([uint32]$header[0x1d] -shl 16) -bor + ([uint32]$header[0x1e] -shl 8) -bor + [uint32]$header[0x1f] + ) + + if ($wiiMagic -eq [uint32]0x5d1c9ea3) { + Assert-True ( + $report.media.platform -eq 'wii' + ) "${finalLogPath}: Wii header magic does not match the native-report platform." + } + elseif ($gameCubeMagic -eq [uint32]0xc2339f3d) { + Assert-True ( + $report.media.platform -eq 'gamecube' + ) "${finalLogPath}: GameCube header magic does not match the native-report platform." + + Assert-True ( + $logText.Contains('Disc type..........: GameCube') + ) "${finalLogPath}: GameCube header magic does not match the human-readable disc type." + + Assert-True ( + $logText.Contains('Expected sectors..: 712880') + ) "${finalLogPath}: GameCube header magic does not match the expected-sector geometry." + } + } + } + + Write-Host '' + Write-Host "LIVE NATIVE REPORT VALIDATION: PASS ($($reports.Count) reports)" + Write-Host "Controlled cancellation reports: $($cancelReports.Count)" + break + } + + 'Collect' { + Assert-True (Test-Path -LiteralPath $ResultsRoot -PathType Container) "Results directory is missing: $ResultsRoot" + $destination = "$ResultsRoot-results.zip" + if (Test-Path -LiteralPath $destination) { + Remove-Item -LiteralPath $destination -Force + } + Compress-Archive -Path (Join-Path $ResultsRoot '*') -DestinationPath $destination -CompressionLevel Optimal + Write-Host 'VALIDATION RESULT COLLECTION: PASS' + Write-Host "Archive: $destination" + Write-Host "Bytes: $((Get-Item -LiteralPath $destination).Length)" + Write-Host "SHA-256: $(Get-Sha256 $destination)" + break + } +} diff --git a/validation/friidump-v0.5.3.16-pf1-candidate21-linux-live-validation.sh b/validation/friidump-v0.5.3.16-pf1-candidate21-linux-live-validation.sh new file mode 100644 index 0000000..4c59e90 --- /dev/null +++ b/validation/friidump-v0.5.3.16-pf1-candidate21-linux-live-validation.sh @@ -0,0 +1,397 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +phase="${1:-build}" +device="${2:-/dev/sr0}" +firmware_note="${3:-${FRIIDUMP_FIRMWARE_MODIFIED_NOTE:-}}" + +source_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +build_root="$source_root/build-candidate21-linux" +expected_version="0.5.3.16-pf1-candidate21" +expected_commit="$( + python3 - "$source_root/CANDIDATE.json" <<'PYEXPECTED' +import json +import re +import sys +from pathlib import Path + +try: + value = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))["candidate_implementation"]["commit"] +except (OSError, KeyError, TypeError, ValueError): + value = "" + +print(value.lower() if re.fullmatch(r"[0-9a-fA-F]{40}", value) else "") +PYEXPECTED +)" + +if [[ ! "$expected_commit" =~ ^[0-9a-f]{40}$ ]]; then + echo "Unable to resolve candidate21 implementation commit from CANDIDATE.json." >&2 + exit 1 +fi + +require_command() { + local command_name="$1" + local package_hint="$2" + + if ! command -v "$command_name" >/dev/null 2>&1; then + echo "Missing required command: $command_name" >&2 + echo "Ubuntu package hint: sudo apt-get install -y $package_hint" >&2 + exit 1 + fi +} + +require_build_dependencies() { + require_command cmake cmake + require_command cc build-essential + require_command python3 python3 + + if ! python3 -c 'import jsonschema' >/dev/null 2>&1; then + echo "Missing Python module: jsonschema" >&2 + echo "Ubuntu package hint: sudo apt-get install -y python3-jsonschema" >&2 + exit 1 + fi +} + +resolve_commit() { + local git_root git_commit + + git_root="$(git -C "$source_root" rev-parse --show-toplevel 2>/dev/null || true)" + if [[ -n "$git_root" && "$(cd -- "$git_root" && pwd -P)" == "$(cd -- "$source_root" && pwd -P)" ]]; then + git_commit="$(git -C "$source_root" rev-parse HEAD 2>/dev/null || true)" + if [[ ! "$git_commit" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "Unable to resolve HEAD from the source-root Git repository." >&2 + exit 1 + fi + if [[ "${git_commit,,}" != "$expected_commit" ]]; then + echo "Candidate implementation commit mismatch." >&2 + echo "Expected: $expected_commit" >&2 + echo "Actual: ${git_commit,,}" >&2 + exit 1 + fi + fi + + printf '%s\n' "$expected_commit" +} + +validate_report_directory() { + local report_root="$1" + + python3 "$source_root/tests/validate_native_reports.py" \ + --fixtures "$report_root" \ + --schema "$source_root/tests/contracts/friidump-test-result.v1.schema.json" \ + --allow-arbitrary +} + +normalize_future_source_timestamps() { + python3 - "$source_root" <<'PYTIMESTAMPS' +import os +import sys +import time +from pathlib import Path + +root = Path(sys.argv[1]) +now = time.time() +normalized = 0 + +for path in root.rglob("*"): + if not path.is_file(): + continue + stat = path.stat() + if stat.st_mtime > now + 1.0: + os.utime(path, (stat.st_atime, now)) + normalized += 1 + +print(f"Future source timestamp preflight: PASS (normalized={normalized})") +PYTIMESTAMPS +} + +build_phase() { + local commit fixtures + + require_build_dependencies + commit="$(resolve_commit)" + normalize_future_source_timestamps + + rm -rf -- "$build_root" + + cmake -S "$source_root" -B "$build_root" \ + -DBUILD_STATIC_BINARY=ON \ + -DBUILD_NATIVE_REPORT_TESTS=ON \ + -DFRIIDUMP_BUILD_COMMIT="$commit" + + cmake --build "$build_root" --parallel + + "$build_root/src/friidump-xbox-portable-metadata-test" + python3 "$source_root/tests/test_linux_xbox_portable_contract.py" + python3 "$source_root/tests/test_seed_diagnostics_contract.py" + "$build_root/src/friidump-linux-rawio-test" + python3 "$source_root/tests/test_linux_rawio_contract.py" + python3 "$source_root/tests/test_xbox_dvd_structure_contract.py" --source-root "$source_root" + + fixtures="$(mktemp -d)" + trap 'rm -rf -- "$fixtures"' RETURN + + "$build_root/src/friidump-report-fixtures" "$fixtures" + validate_report_directory "$fixtures" + + test -f "$build_root/src/redump_dat/Nintendo - GameCube.dat" + test -f "$build_root/src/redump_dat/Nintendo - Wii.dat" + test -f "$build_root/src/redump_dat/Microsoft - Xbox.dat" + + "$build_root/src/friidump-redump-dat-resolver-test" \ + - \ + "$build_root/src" \ + "Nintendo - GameCube.dat" \ + "$build_root/src/redump_dat/Nintendo - GameCube.dat" \ + 1 + + "$build_root/src/friidump-redump-dat-verify-test" \ + "$build_root/src/redump_dat/Nintendo - GameCube.dat" + + resolver_tmp="$(mktemp -d)" + mkdir -p "$resolver_tmp/prefix/bin" \ + "$resolver_tmp/prefix/share/friidump/redump_dat" \ + "$resolver_tmp/env-dat" \ + "$resolver_tmp/explicit-dat" + printf 'installed\n' > "$resolver_tmp/prefix/share/friidump/redump_dat/Test.dat" + printf 'environment\n' > "$resolver_tmp/env-dat/Test.dat" + printf 'explicit\n' > "$resolver_tmp/explicit-dat/Test.dat" + + "$build_root/src/friidump-redump-dat-resolver-test" \ + - \ + "$resolver_tmp/prefix/bin" \ + Test.dat \ + "$resolver_tmp/prefix/bin/../share/friidump/redump_dat/Test.dat" \ + 1 + + FRIIDUMP_REDUMP_DAT_DIR="$resolver_tmp/env-dat" \ + "$build_root/src/friidump-redump-dat-resolver-test" \ + - \ + "$resolver_tmp/prefix/bin" \ + Test.dat \ + "$resolver_tmp/env-dat/Test.dat" \ + 1 + + FRIIDUMP_REDUMP_DAT_DIR="$resolver_tmp/env-dat" \ + "$build_root/src/friidump-redump-dat-resolver-test" \ + "$resolver_tmp/explicit-dat" \ + "$resolver_tmp/prefix/bin" \ + Test.dat \ + "$resolver_tmp/explicit-dat/Test.dat" \ + 1 + + + rm -rf -- "$resolver_tmp" + + set +e + help_output="$("$build_root/src/friidump" --help 2>&1)" + help_status=$? + set -e + + if test "$help_status" -ne 1; then + echo "Unexpected FriiDump --help exit status: $help_status (expected 1)." >&2 + exit 1 + fi + + grep -F "FriiDump $expected_version" <<<"$help_output" + grep -F "FRIIDUMP_REDUMP_DAT_DIR" <<<"$help_output" + grep -F "Linux raw-I/O requirement" <<<"$help_output" + + echo + echo "WARNING: this Linux build does not yet carry CAP_SYS_RAWIO." + echo "Vendor-command hardware operations will be refused until the exact" + echo "validated executable receives cap_sys_rawio=ep. Do not run FriiDump" + echo "itself as root. Rebuilding or replacing the executable clears the capability." + echo "Install and verify with:" + echo " bash $source_root/validation/friidump-v0.5.3.16-pf1-candidate21-linux-live-validation.sh capability" + echo + echo "FRIIDUMP CANDIDATE21 LINUX BUILD: PASS" +} + +capability_phase() { + require_command getcap libcap2-bin + require_command setcap libcap2-bin + require_command sudo sudo + ensure_build + + bash "$source_root/validation/friidump-linux-rawio-capability.sh" install "$build_root/src/friidump" + + bash "$source_root/validation/friidump-linux-rawio-capability.sh" status "$build_root/src/friidump" + + echo "FRIIDUMP CANDIDATE21 LINUX CAPABILITY: PASS" +} + +ensure_build() { + if ! test -x "$build_root/src/friidump"; then + build_phase + fi +} + +device_model() { + local block_name model_path + + block_name="$(basename -- "$device")" + model_path="/sys/class/block/$block_name/device/model" + + if test -r "$model_path"; then + tr -d '\000' < "$model_path" | xargs + else + printf '%s\n' "" + fi +} + +no_media_phase() { + local output_root report_path output_path exit_code + + require_build_dependencies + ensure_build + + output_root="$source_root/_native-report-linux-validation/$(date -u +%Y%m%dT%H%M%SZ)-no-media" + mkdir -p "$output_root" + + echo "Remove media from $device, close the tray, wait for the drive to settle, then press Enter." + read -r + + output_path="$output_root/no-media-placeholder.iso" + report_path="$output_root/no-media-placeholder.iso.friidump.json" + + set +e + ( + cd "$output_root" + "$build_root/src/friidump" \ + -d "$device" \ + -i "no-media-placeholder.iso" + ) + exit_code=$? + set -e + + test -f "$report_path" || { + echo "FAIL: expected native report was not created: $report_path" >&2 + exit 1 + } + + validate_report_directory "$output_root" + + python3 - "$report_path" "$device" "$expected_version" "$expected_commit" <<'PYREPORT' +import json +import sys +from pathlib import Path + +report_path = Path(sys.argv[1]) +expected_device = sys.argv[2] +expected_version = sys.argv[3] +expected_commit = sys.argv[4] + +report = json.loads(report_path.read_text(encoding="utf-8")) + +checks = { + "schema": report.get("schema") == "friidump-test-result.v1", + "version": report.get("generator", {}).get("version") == expected_version, + "commit": report.get("generator", {}).get("build_commit") == expected_commit, + "device": report.get("drive", {}).get("device_path") == expected_device, + "stock_firmware": report.get("drive", {}).get("firmware_modified") is False, + "no_modification_note": report.get("drive", {}).get("modification_note") is None, + "test_type": report.get("run", {}).get("test_type") == "diagnostic_no_media", + "run_result": report.get("run", {}).get("result") == "not_applicable", + "unknown_platform": report.get("media", {}).get("platform") == "unknown", + "seed_not_attempted": report.get("seed", {}).get("attempted") is False, + "dump_not_attempted": report.get("dump", {}).get("attempted") is False, + "no_output_path": report.get("dump", {}).get("output_path") is None, + "no_hashes": all(value is None for value in report.get("hashes", {}).values()), + "no_artifacts": report.get("artifacts") == [], +} + +failed = [name for name, ok in checks.items() if not ok] +for name, ok in checks.items(): + print(f"{'PASS' if ok else 'FAIL'}: {name}") + +if failed: + raise SystemExit("No-media report contract failed: " + ", ".join(failed)) +PYREPORT + + test ! -e "$output_path" || { + echo "FAIL: no-media validation unexpectedly created an image: $output_path" >&2 + exit 1 + } + + echo "FriiDump exit code: $exit_code (nonzero is expected for no media)" + echo "Report: $report_path" + echo "FRIIDUMP CANDIDATE21 LINUX NO-MEDIA: PASS" +} + +xbox_cancel_phase() { + local model output_root + + require_build_dependencies + ensure_build + + model="$(device_model)" + if [[ "$model" != *GDR8050L* && "$model" != *GDR-8050L* ]]; then + echo "Refusing Xbox cancellation validation on this device." >&2 + echo "Expected a GDR-8050L inquiry identity; found: ${model:-unknown}" >&2 + exit 1 + fi + + if [[ -z "$firmware_note" ]]; then + echo "Xbox cancellation validation requires an explicit modified-firmware note." >&2 + echo "Pass it as argument 3 or set FRIIDUMP_FIRMWARE_MODIFIED_NOTE." >&2 + exit 1 + fi + + output_root="$source_root/_native-report-linux-validation/$(date -u +%Y%m%dT%H%M%SZ)-xbox-cancel" + mkdir -p "$output_root" + + echo "Insert the Xbox disc in $device. Press Ctrl+C once after XISO progress begins." + + ( + cd "$output_root" + "$build_root/src/friidump" \ + -d "$device" \ + -T 4 \ + -X "Red Faction II [TQ00501A].xiso" \ + --firmware-modified "$firmware_note" + ) || true + + echo "Inspect the generated partial XISO, .friidump.json, and log under: $output_root" +} + +identify_phase() { + local block_name + + block_name="$(basename -- "$device")" + + echo "Device: $device" + for field in vendor model rev; do + printf '%-8s ' "$field:" + cat "/sys/class/block/$block_name/device/$field" 2>/dev/null || echo "unavailable" + done + + if command -v sg_inq >/dev/null 2>&1; then + echo + sg_inq "$device" + else + echo + echo "Optional detailed inquiry: sudo apt-get install -y sg3-utils" + fi + + echo + stat -c 'Access: %A Owner: %U Group: %G' "$device" + id + + if test -x "$build_root/src/friidump"; then + echo + bash "$source_root/validation/friidump-linux-rawio-capability.sh" status "$build_root/src/friidump" || true + fi +} + +case "$phase" in + build) build_phase ;; + capability) capability_phase ;; + identify) identify_phase ;; + no-media) no_media_phase ;; + xbox-cancel) xbox_cancel_phase ;; + *) + echo "Usage: $0 {build|capability|identify|no-media|xbox-cancel} [/dev/sr0] [firmware-note]" >&2 + exit 2 + ;; +esac diff --git a/validation/friidump-v0.5.3.16-pf1-candidate21-windows-live-validation.ps1 b/validation/friidump-v0.5.3.16-pf1-candidate21-windows-live-validation.ps1 new file mode 100644 index 0000000..8c537b1 --- /dev/null +++ b/validation/friidump-v0.5.3.16-pf1-candidate21-windows-live-validation.ps1 @@ -0,0 +1,887 @@ +[CmdletBinding()] +param( + [ValidateSet( + 'Build', + 'NoMedia', + 'GameCube', + 'XboxIso', + 'XboxXiso', + 'XboxCancel', + 'PrepareCancellation', + 'ValidateReports', + 'Collect' + )] + [string]$Phase = 'Build', + + [string]$SourceRoot = (Get-Location).Path, + + [string]$Drive, + + [string]$ResultsRoot, + + [string]$OutputName, + + [string]$FirmwareModifiedNote, + + [bool]$UseMethod8 = $true, + + [string[]]$ExtraFriiDumpArgs = @(), + + [string]$PhpValidator +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$ExpectedVersion = '0.5.3.16-pf1-candidate21' +$ExpectedCommit = $null +$ExpectedSchemaSha256 = 'd1e97cb3e88e0f5a94ccf9b9edf921554fc15278662e69420b8c1e564c600411' + +$SourceRoot = (Resolve-Path -LiteralPath $SourceRoot).Path +$CandidateMetadataPath = Join-Path $SourceRoot 'CANDIDATE.json' +if (-not (Test-Path -LiteralPath $CandidateMetadataPath -PathType Leaf)) { + throw "Candidate metadata is missing: $CandidateMetadataPath" +} +$CandidateMetadata = Get-Content -Raw -LiteralPath $CandidateMetadataPath | ConvertFrom-Json +$ExpectedCommit = [string]$CandidateMetadata.candidate_implementation.commit +if ($ExpectedCommit -notmatch '^[0-9a-fA-F]{40}$') { + throw "Candidate implementation commit is invalid: $ExpectedCommit" +} +$ExpectedCommit = $ExpectedCommit.ToLowerInvariant() + +if ([string]::IsNullOrWhiteSpace($ResultsRoot)) { + $ResultsRoot = Join-Path $SourceRoot '_native-report-live-validation' +} +$ResultsRoot = [System.IO.Path]::GetFullPath($ResultsRoot) + +$Exe = Join-Path $SourceRoot 'friidump.exe' +$BuildScript = Join-Path $SourceRoot 'build_msvc32.cmd' +$FixtureBuildScript = Join-Path $SourceRoot 'build_msvc32_native_report_tests.cmd' +$Validator = Join-Path $SourceRoot 'tests\validate_native_reports.py' +$Schema = Join-Path $SourceRoot 'tests\contracts\friidump-test-result.v1.schema.json' +$SourceManifest = Join-Path $SourceRoot 'SOURCE_SHA256SUMS.txt' +$MainResponseFile = Join-Path $SourceRoot 'msvc32_friidump.rsp' +$FixtureResponseFile = Join-Path $SourceRoot 'msvc32_native_report_tests.rsp' + +function Assert-True { + param( + [Parameter(Mandatory)] + [bool]$Condition, + [Parameter(Mandatory)] + [string]$Message + ) + + if (-not $Condition) { + throw $Message + } +} + +function Assert-LastExitCode { + param( + [Parameter(Mandatory)] + [string]$Operation + ) + + if ($LASTEXITCODE -ne 0) { + throw "$Operation failed with exit code $LASTEXITCODE." + } +} + +function Get-Sha256 { + param([Parameter(Mandatory)][string]$Path) + + return (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant() +} + +function Test-SourceManifest { + if (-not (Test-Path -LiteralPath $SourceManifest -PathType Leaf)) { + Write-Host '[INFO] SOURCE_SHA256SUMS.txt is not present; package-manifest validation skipped.' + return + } + + $checked = 0 + foreach ($line in Get-Content -LiteralPath $SourceManifest) { + if ([string]::IsNullOrWhiteSpace($line)) { + continue + } + if ($line -notmatch '^([0-9a-fA-F]{64}) (.+)$') { + throw "Malformed source-manifest line: $line" + } + + $expected = $Matches[1].ToLowerInvariant() + $relative = $Matches[2] -replace '/', '\' + $path = Join-Path $SourceRoot $relative + Assert-True (Test-Path -LiteralPath $path -PathType Leaf) "Manifest file is missing: $relative" + $actual = Get-Sha256 $path + Assert-True ($actual -eq $expected) "Manifest SHA-256 mismatch: $relative" + $checked++ + } + + Write-Host "Source manifest: PASS ($checked files)" +} + +function Test-MsvcSplitLinkContract { + param( + [Parameter(Mandatory)][string]$ResponsePath, + [Parameter(Mandatory)][string]$BuildScriptPath, + [Parameter(Mandatory)][string]$ExpectedInvocation, + [Parameter(Mandatory)][string]$Label + ) + + Assert-True (Test-Path -LiteralPath $ResponsePath -PathType Leaf) "$Label response file is missing: $ResponsePath" + Assert-True (Test-Path -LiteralPath $BuildScriptPath -PathType Leaf) "$Label build script is missing: $BuildScriptPath" + + $lines = @( + Get-Content -LiteralPath $ResponsePath | + ForEach-Object { $_.Trim() } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + + Assert-True (-not ($lines -icontains '/link')) "$Label response file must not contain /link. The linker boundary must be on the cl.exe command line." + Assert-True (-not ($lines -icontains 'ole32.lib')) "$Label response file must not contain ole32.lib. The library must follow /link on the cl.exe command line." + + $buildText = Get-Content -Raw -LiteralPath $BuildScriptPath + Assert-True ($buildText.Contains($ExpectedInvocation)) "$Label build script is missing the required split compile/link invocation: $ExpectedInvocation" + + Write-Host "$Label MSVC command-line linker boundary: PASS" +} + +function Test-PythonValidatorAvailable { + & py -3 -c 'import jsonschema' 2>$null + if ($LASTEXITCODE -ne 0) { + throw 'Python 3 with jsonschema is required. Install it with: py -3 -m pip install jsonschema' + } +} + +function Invoke-ReportValidation { + param([Parameter(Mandatory)][string]$ReportRoot) + + Assert-True (Test-Path -LiteralPath $Validator -PathType Leaf) "Validator is missing: $Validator" + Assert-True (Test-Path -LiteralPath $Schema -PathType Leaf) "Schema is missing: $Schema" + Assert-True ((Get-Sha256 $Schema) -eq $ExpectedSchemaSha256) 'Bundled schema SHA-256 mismatch.' + + Test-PythonValidatorAvailable + + $arguments = @( + '-3', + $Validator, + '--fixtures', $ReportRoot, + '--schema', $Schema, + '--allow-arbitrary' + ) + + if (-not [string]::IsNullOrWhiteSpace($PhpValidator)) { + Assert-True (Test-Path -LiteralPath $PhpValidator -PathType Leaf) "PHP validator is missing: $PhpValidator" + $arguments += @('--php-validator', $PhpValidator) + } + + & py @arguments + Assert-LastExitCode 'Strict native-report validation' +} + +function Get-NewReport { + param( + [Parameter(Mandatory)][string]$ReportDirectory, + [Parameter(Mandatory)][datetime]$Started + ) + + $reports = @( + Get-ChildItem -LiteralPath $ReportDirectory -Recurse -Filter '*.friidump.json' -File | + Where-Object { $_.LastWriteTimeUtc -ge $Started.ToUniversalTime().AddSeconds(-2) } | + Sort-Object LastWriteTimeUtc + ) + + Assert-True ($reports.Count -eq 1) "Expected exactly one new report; found $($reports.Count)." + return $reports[0] +} + +function Read-Report { + param([Parameter(Mandatory)][string]$Path) + + return Get-Content -Raw -LiteralPath $Path | ConvertFrom-Json +} + +function Test-CommonReportIdentity { + param( + [Parameter(Mandatory)]$Report, + [Parameter(Mandatory)][string]$ReportPath + ) + + Assert-True ($Report.schema -eq 'friidump-test-result.v1') "${ReportPath}: schema mismatch." + Assert-True ($Report.generator.name -eq 'FriiDump') "${ReportPath}: generator name mismatch." + Assert-True ($Report.generator.version -eq $ExpectedVersion) "${ReportPath}: generator version mismatch." + Assert-True ($Report.generator.build_commit -eq $ExpectedCommit) "${ReportPath}: build commit mismatch." + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.run.run_id)) "${ReportPath}: run UUID missing." + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.drive.model)) "${ReportPath}: drive model missing." + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.drive.firmware_revision)) "${ReportPath}: firmware revision missing." + + if ([string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) { + Assert-True ($Report.drive.firmware_modified -eq $false) "${ReportPath}: omitted --firmware-modified must assume stock firmware." + Assert-True ($null -eq $Report.drive.modification_note) "${ReportPath}: stock firmware must not have a modification note." + } + else { + Assert-True ($Report.drive.firmware_modified -eq $true) "${ReportPath}: modified-firmware flag was not recorded." + Assert-True ([string]$Report.drive.modification_note -eq $FirmwareModifiedNote) "${ReportPath}: modified-firmware explanation mismatch." + } +} + +function Test-DumpOutputIdentity { + param( + [Parameter(Mandatory)]$Report, + [Parameter(Mandatory)][string]$ExpectedOutput + ) + + $resolvedOutput = [System.IO.Path]::GetFullPath($ExpectedOutput) + Assert-True (Test-Path -LiteralPath $resolvedOutput -PathType Leaf) "Dump output is missing: $resolvedOutput" + + $actualBytes = (Get-Item -LiteralPath $resolvedOutput).Length + Assert-True ([int64]$Report.dump.byte_count -eq $actualBytes) 'Report byte count does not match the output file.' + Assert-True ([string]$Report.dump.output_path -eq $resolvedOutput) 'Report output path does not match the requested path.' + + $artifacts = @($Report.artifacts | Where-Object { $_.type -eq 'dump_output' }) + Assert-True ($artifacts.Count -eq 1) 'Exactly one dump_output artifact is required.' + Assert-True ([string]$artifacts[0].path -eq $resolvedOutput) 'dump_output artifact path mismatch.' + Assert-True ([int64]$artifacts[0].bytes -eq $actualBytes) 'dump_output artifact byte count mismatch.' + + if ($null -ne $Report.hashes.sha256) { + $actualSha = Get-Sha256 $resolvedOutput + Assert-True ([string]$Report.hashes.sha256 -eq $actualSha) 'Report SHA-256 does not match the output file.' + Assert-True ([string]$artifacts[0].sha256 -eq $actualSha) 'Artifact SHA-256 does not match the output file.' + } +} + +function New-RunDirectory { + param([Parameter(Mandatory)][string]$Name) + + $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ') + $path = Join-Path $ResultsRoot "$stamp-$Name" + New-Item -ItemType Directory -Force -Path $path | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $path 'reports') | Out-Null + return $path +} + +function Invoke-FriiDumpRun { + param( + [Parameter(Mandatory)][string[]]$Arguments, + [Parameter(Mandatory)][string]$RunDirectory, + [Parameter(Mandatory)][bool]$ExpectSuccess + ) + + $started = (Get-Date).ToUniversalTime() + + Write-Host "Working directory: $RunDirectory" + Write-Host "Command: $Exe $($Arguments -join ' ')" + Write-Host 'Console mode: direct live FriiDump output; FriiDump native .log is the evidence log.' + + # Do not route an interactive FriiDump run through a PowerShell + # pipeline. Tee-Object buffers carriage-return progress updates, turns + # native stderr into PowerShell error records, and also contaminates a + # function's return stream with every line emitted by the child. + # + # Start FriiDump with inherited console handles instead. This preserves + # real-time progress output and leaves this function with exactly one + # return object. + function ConvertTo-WindowsCommandLineArgument { + param([AllowEmptyString()][string]$Value) + + if ($Value.Length -gt 0 -and $Value -notmatch '[\s"]') { + return $Value + } + + $builder = New-Object System.Text.StringBuilder + [void]$builder.Append('"') + $backslashes = 0 + + foreach ($character in $Value.ToCharArray()) { + if ($character -eq '\') { + $backslashes++ + continue + } + + if ($character -eq '"') { + [void]$builder.Append(('\' * (($backslashes * 2) + 1))) + [void]$builder.Append('"') + $backslashes = 0 + continue + } + + if ($backslashes -gt 0) { + [void]$builder.Append(('\' * $backslashes)) + $backslashes = 0 + } + + [void]$builder.Append($character) + } + + if ($backslashes -gt 0) { + [void]$builder.Append(('\' * ($backslashes * 2))) + } + + [void]$builder.Append('"') + return $builder.ToString() + } + + $argumentLine = ( + $Arguments | + ForEach-Object { + ConvertTo-WindowsCommandLineArgument ([string]$_) + } + ) -join ' ' + + $startInfo = New-Object System.Diagnostics.ProcessStartInfo + $startInfo.FileName = $Exe + $startInfo.Arguments = $argumentLine + $startInfo.WorkingDirectory = $RunDirectory + $startInfo.UseShellExecute = $false + $startInfo.CreateNoWindow = $false + $startInfo.RedirectStandardOutput = $false + $startInfo.RedirectStandardError = $false + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $startInfo + + try { + [void]$process.Start() + $process.WaitForExit() + $exitCode = $process.ExitCode + } + finally { + $process.Dispose() + } + + Set-Content -LiteralPath (Join-Path $RunDirectory 'exit-code.txt') -Value $exitCode -Encoding ASCII + + if ($ExpectSuccess) { + Assert-True ($exitCode -eq 0) "FriiDump was expected to succeed but exited with $exitCode." + } + + return [pscustomobject]@{ + Started = $started + ExitCode = [int]$exitCode + Log = $null + } +} + +function Add-ModifiedFirmwareArgument { + param([string[]]$Arguments) + + if (-not [string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) { + return $Arguments + @('--firmware-modified', $FirmwareModifiedNote) + } + return $Arguments +} + +New-Item -ItemType Directory -Force -Path $ResultsRoot | Out-Null + +switch ($Phase) { + 'Build' { + Assert-True (Test-Path -LiteralPath $BuildScript -PathType Leaf) "Build script is missing: $BuildScript" + Assert-True (Test-Path -LiteralPath $FixtureBuildScript -PathType Leaf) "Fixture build script is missing: $FixtureBuildScript" + Test-SourceManifest + + $XboxRegionHeader = Join-Path $SourceRoot 'libfriidump\xbox_region.h' + $XboxLegacyUtilsHeader = Join-Path $SourceRoot 'libfriidump\xbox_ref\utils.h' + $XboxRegionHeaderText = Get-Content -Raw -LiteralPath $XboxRegionHeader + $XboxLegacyUtilsText = Get-Content -Raw -LiteralPath $XboxLegacyUtilsHeader + + Assert-True (-not $XboxRegionHeaderText.Contains('')) ` + 'xbox_region.h must not introduce stdbool.h into the copied Xbox header graph.' + Assert-True ($XboxRegionHeaderText.Contains('int xbox_region_format(')) ` + 'xbox_region.h must expose the MSVC-safe int return type.' + Assert-True ($XboxLegacyUtilsText.Contains('typedef int bool;')) ` + 'The copied Xbox legacy bool declaration changed unexpectedly.' + Write-Host 'Xbox region/MSVC bool isolation: PASS' + + $XboxCancelContract = Join-Path $SourceRoot 'tests\test_xbox_cancel_contract.py' + Assert-True (Test-Path -LiteralPath $XboxCancelContract -PathType Leaf) ` + 'Xbox copied-dumper cancellation contract test is missing.' + & py -3 $XboxCancelContract --source-root $SourceRoot + Assert-LastExitCode 'Xbox copied-dumper cancellation contract' + + $SeedDiagnosticContract = Join-Path $SourceRoot 'tests\test_seed_diagnostics_contract.py' + Assert-True (Test-Path -LiteralPath $SeedDiagnosticContract -PathType Leaf) ` + 'Seed diagnostics contract test is missing.' + & py -3 $SeedDiagnosticContract + Assert-LastExitCode 'Seed diagnostics contract' + + $LinuxRawIoContract = Join-Path $SourceRoot 'tests\test_linux_rawio_contract.py' + Assert-True (Test-Path -LiteralPath $LinuxRawIoContract -PathType Leaf) ` + 'Linux raw-I/O source/documentation contract test is missing.' + & py -3 $LinuxRawIoContract + Assert-LastExitCode 'Linux raw-I/O source/documentation contract' + + $XboxDvdStructureContract = Join-Path $SourceRoot 'tests\test_xbox_dvd_structure_contract.py' + Assert-True (Test-Path -LiteralPath $XboxDvdStructureContract -PathType Leaf) ` + 'Xbox READ DVD STRUCTURE CDB contract test is missing.' + & py -3 $XboxDvdStructureContract --source-root $SourceRoot + Assert-LastExitCode 'Xbox READ DVD STRUCTURE CDB contract' + + Test-MsvcSplitLinkContract ` + -ResponsePath $MainResponseFile ` + -BuildScriptPath $BuildScript ` + -ExpectedInvocation 'cl @"%EFFECTIVE_RSP%" /link ole32.lib' ` + -Label 'FriiDump executable' + + Test-MsvcSplitLinkContract ` + -ResponsePath $FixtureResponseFile ` + -BuildScriptPath $FixtureBuildScript ` + -ExpectedInvocation 'cl @"%~dp0msvc32_native_report_tests.rsp" /link ole32.lib' ` + -Label 'Native-report fixtures' + + $env:FRIIDUMP_BUILD_COMMIT = $ExpectedCommit + + & $BuildScript clean + Assert-LastExitCode 'MSVC clean' + + & $BuildScript + Assert-LastExitCode 'MSVC candidate build' + + & $FixtureBuildScript + Assert-LastExitCode 'MSVC native-report fixture validation' + + Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'friidump.exe was not produced.' + + # Windows PowerShell 5.1 converts native stderr records into + # NativeCommandError objects when $ErrorActionPreference is Stop. + # FriiDump intentionally writes part of its startup/help text to + # stderr, so capture both streams through ProcessStartInfo instead + # of invoking the executable directly through the PowerShell + # pipeline. + $helpStartInfo = New-Object System.Diagnostics.ProcessStartInfo + $helpStartInfo.FileName = $Exe + $helpStartInfo.Arguments = '--help' + $helpStartInfo.UseShellExecute = $false + $helpStartInfo.RedirectStandardOutput = $true + $helpStartInfo.RedirectStandardError = $true + $helpStartInfo.CreateNoWindow = $true + + $helpProcess = New-Object System.Diagnostics.Process + $helpProcess.StartInfo = $helpStartInfo + + try { + [void]$helpProcess.Start() + + # Drain stdout and stderr concurrently. Reading either redirected + # stream synchronously before the other can deadlock when the + # second pipe fills while the process is still running. + $helpStdoutTask = $helpProcess.StandardOutput.ReadToEndAsync() + $helpStderrTask = $helpProcess.StandardError.ReadToEndAsync() + + $helpProcess.WaitForExit() + + $helpStdout = $helpStdoutTask.GetAwaiter().GetResult() + $helpStderr = $helpStderrTask.GetAwaiter().GetResult() + $helpExitCode = $helpProcess.ExitCode + } + finally { + $helpProcess.Dispose() + } + + $help = @( + $helpStdout, + $helpStderr + ) -join [Environment]::NewLine + + Assert-True ($helpExitCode -eq 1) "Candidate help command returned unexpected exit code $helpExitCode; FriiDump historically returns 1 for --help." + Assert-True ($help.Contains("FriiDump $ExpectedVersion")) 'Candidate version is missing from help output.' + foreach ($option in @('--report-json', '--report-dir', '--firmware-modified')) { + Assert-True ($help.Contains($option)) "Candidate help is missing $option." + } + Assert-True ($help.Contains('Native .friidump.json reports are created by default')) 'Default native-report behavior is missing from help output.' + Assert-True ($help.Contains('beside the final log')) 'Default beside-log report placement is missing from help output.' + Assert-True ($help.Contains('(may be combined with --report-json)')) 'Combined --report-json/--report-dir behavior is missing from help output.' + Assert-True ($help.Contains('omission assumes stock')) 'Stock-firmware default is missing from help output.' + + $buildRecord = @( + "Generated UTC: $((Get-Date).ToUniversalTime().ToString('o'))", + "Version: $ExpectedVersion", + "Commit: $ExpectedCommit", + "Executable: $Exe", + "Executable SHA-256: $(Get-Sha256 $Exe)", + 'MSVC build: PASS', + 'MSVC native-report fixtures: PASS', + 'Schema and semantic validation: PASS (13 reports)', + 'Help/version/options/default-location smoke: PASS', + 'Xbox copied-dumper cancellation, hashing, and summary contract: PASS', + 'Seed diagnostics, transport evidence, and seed-failure STOP UNIT contract: PASS', + 'Linux CAP_SYS_RAWIO preflight, documentation, helper, and build-warning contract: PASS' + ) + $recordPath = Join-Path $ResultsRoot 'build-validation.txt' + Set-Content -LiteralPath $recordPath -Value $buildRecord -Encoding UTF8 + + Write-Host '' + Write-Host 'FRIIDUMP CANDIDATE21 WINDOWS BUILD: PASS' + Write-Host "Build record: $recordPath" + break + } + + 'NoMedia' { + Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' + Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' + + Read-Host "Remove all media from $Drive, wait for the drive to settle, then press Enter" + $runDir = New-RunDirectory 'no-media-default-report' + $output = Join-Path $runDir 'no-media-placeholder.iso' + $args = @('-d', $Drive, '-i', $output) + $args = Add-ModifiedFirmwareArgument $args + $args += $ExtraFriiDumpArgs + + $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $false + $reportFile = Get-NewReport -ReportDirectory $runDir -Started $run.Started + Assert-True ($reportFile.Name -eq 'no-media-placeholder.iso.friidump.json') "Default report filename mismatch: $($reportFile.Name)" + Assert-True ($reportFile.DirectoryName -eq $runDir) "Default no-media report was not placed beside the final log/output path." + Invoke-ReportValidation $runDir + $report = Read-Report $reportFile.FullName + Test-CommonReportIdentity $report $reportFile.FullName + + Assert-True ($report.run.test_type -eq 'diagnostic_no_media') 'No-media test_type mismatch.' + Assert-True ($report.run.result -eq 'not_applicable') 'No-media run result mismatch.' + Assert-True ($report.dump.attempted -eq $false) 'No-media report must not claim a dump.' + Assert-True (-not (Test-Path -LiteralPath $output)) 'No-media test unexpectedly created an output image.' + + Write-Host '' + Write-Host 'NO-MEDIA DEFAULT NATIVE REPORT: PASS' + Write-Host "Report: $($reportFile.FullName)" + break + } + + 'GameCube' { + Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' + Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' + + $runDir = New-RunDirectory 'gamecube-full-dump' + if ([string]::IsNullOrWhiteSpace($OutputName)) { + $OutputName = 'Sonic Mega Collection.iso' + } + $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) + $args = @('-d', $Drive) + if ($UseMethod8) { + $args += '-8' + } + $args += @('-i', $output, '--report-dir', $reportDir) + $args = Add-ModifiedFirmwareArgument $args + $args += $ExtraFriiDumpArgs + + $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true + $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started + Invoke-ReportValidation $reportDir + $report = Read-Report $reportFile.FullName + Test-CommonReportIdentity $report $reportFile.FullName + Test-DumpOutputIdentity $report $output + + Assert-True ($report.media.platform -eq 'gamecube') 'Expected a GameCube report.' + Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'GameCube run outcome mismatch.' + Assert-True ($report.dump.result -eq 'pass') 'GameCube dump result mismatch.' + Assert-True (@($report.notes) -contains 'Measurement scope: full_optical_payload.') 'GameCube measurement scope mismatch.' + + Write-Host '' + Write-Host 'GAMECUBE FULL-DUMP NATIVE REPORT: PASS' + Write-Host "Report: $($reportFile.FullName)" + break + } + + 'XboxIso' { + Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' + Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' + + $runDir = New-RunDirectory 'xbox-redump-iso' + $reportDir = Join-Path $runDir 'reports' + if ([string]::IsNullOrWhiteSpace($OutputName)) { + $OutputName = 'Red Faction II [TQ00501A].iso' + } + $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) + $args = @('-d', $Drive, '-T', '4', '-i', $output, '--report-dir', $reportDir) + $args = Add-ModifiedFirmwareArgument $args + $args += $ExtraFriiDumpArgs + + $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true + $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started + Invoke-ReportValidation $reportDir + $report = Read-Report $reportFile.FullName + Test-CommonReportIdentity $report $reportFile.FullName + Test-DumpOutputIdentity $report $output + + Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.' + Assert-True ($report.media.region -eq 'North America') 'Expected the XBE GameRegion to report North America.' + Assert-True (@($report.notes) -contains 'Xbox XBE GameRegion mask: 0x00000001.') 'Expected the raw XBE GameRegion mask note.' + Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'Xbox ISO run outcome mismatch.' + Assert-True (@($report.notes) -contains 'Measurement scope: assembled_output.') 'Xbox ISO measurement scope mismatch.' + + Write-Host '' + Write-Host 'XBOX REDUMP-STYLE ISO NATIVE REPORT: PASS' + Write-Host "Report: $($reportFile.FullName)" + break + } + + 'XboxXiso' { + Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' + Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' + + $runDir = New-RunDirectory 'xbox-xiso' + $reportDir = Join-Path $runDir 'reports' + if ([string]::IsNullOrWhiteSpace($OutputName)) { + $OutputName = 'Red Faction II [TQ00501A].xiso' + } + $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) + $args = @('-d', $Drive, '-T', '4', '-X', $output, '--report-dir', $reportDir) + $args = Add-ModifiedFirmwareArgument $args + $args += $ExtraFriiDumpArgs + + $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true + $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started + Invoke-ReportValidation $reportDir + $report = Read-Report $reportFile.FullName + Test-CommonReportIdentity $report $reportFile.FullName + Test-DumpOutputIdentity $report $output + + Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.' + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.media.region)) 'Expected an XBE-derived Xbox region.' + Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'Xbox XISO run outcome mismatch.' + Assert-True (@($report.notes) -contains 'Measurement scope: assembled_output.') 'Xbox XISO measurement scope mismatch.' + + Write-Host '' + Write-Host 'XBOX XISO NATIVE REPORT: PASS' + Write-Host "Report: $($reportFile.FullName)" + break + } + + 'XboxCancel' { + Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' + Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' + + $runDir = New-RunDirectory 'xbox-controlled-cancellation' + $reportDir = Join-Path $runDir 'reports' + if ([string]::IsNullOrWhiteSpace($OutputName)) { + $OutputName = 'Red Faction II [TQ00501A].xiso' + } + $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) + $args = @('-d', $Drive, '-T', '4', '-X', $output, '--report-dir', $reportDir) + $args = Add-ModifiedFirmwareArgument $args + $args += $ExtraFriiDumpArgs + + Write-Host '' + Write-Host 'After Regions: North America appears and XISO progress begins, press Ctrl+C once.' + Write-Host '' + + $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $false + $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started + Invoke-ReportValidation $reportDir + $report = Read-Report $reportFile.FullName + Test-CommonReportIdentity $report $reportFile.FullName + Test-DumpOutputIdentity $report $output + + Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.' + Assert-True ($report.media.region -eq 'North America') 'Expected the XBE GameRegion to report North America.' + Assert-True (@($report.notes) -contains 'Xbox XBE GameRegion mask: 0x00000001.') 'Expected the raw XBE GameRegion mask note.' + Assert-True ($report.run.test_type -eq 'partial_dump' -and $report.run.result -eq 'partial') 'Xbox cancellation run outcome mismatch.' + Assert-True ($report.dump.result -eq 'partial') 'Xbox cancellation dump result mismatch.' + Assert-True ($report.dump.failure_stage -eq 'user_cancelled') 'Xbox cancellation failure stage mismatch.' + Assert-True ([int64]$report.dump.sector_count -gt 32) 'Xbox cancellation did not record meaningful partial progress.' + Assert-True ([int64]$report.dump.byte_count -eq ([int64]$report.dump.sector_count * 2048)) 'Xbox cancellation byte/sector accounting mismatch.' + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.crc32)) 'Partial Xbox CRC32 is missing.' + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.md5)) 'Partial Xbox MD5 is missing.' + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.sha1)) 'Partial Xbox SHA-1 is missing.' + Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.sha256)) 'Partial Xbox SHA-256 is missing.' + + $nativeLog = "$output.log" + Assert-True (Test-Path -LiteralPath $nativeLog -PathType Leaf) "Xbox native log is missing: $nativeLog" + $logText = Get-Content -Raw -LiteralPath $nativeLog + foreach ($expectedText in @( + 'Game/Media ID.....: TQ00501A', + 'Title.............: Red Faction', + 'Region............: North America', + 'Seed read.........: N/A (Xbox reference auth path)', + 'Source sectors....: 3431264', + 'Expected output...: 1913920 sectors', + '[OK] Controlled partial Xbox output hashes captured.' + )) { + Assert-True ($logText.Contains($expectedText)) "Xbox summary/log is missing: $expectedText" + } + + Write-Host '' + Write-Host 'XBOX REGION, SUMMARY, GEOMETRY, CANCELLATION, AND HASHING: PASS' + Write-Host "Report: $($reportFile.FullName)" + break + } + + 'PrepareCancellation' { + Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' + Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' + + $runDir = New-RunDirectory 'controlled-cancellation' + if ([string]::IsNullOrWhiteSpace($OutputName)) { + $OutputName = 'controlled-cancellation.partial.iso' + } + $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) + $wrapper = Join-Path $runDir 'run-controlled-cancellation.ps1' + + $argLines = @("'-d'", "'$($Drive.Replace("'", "''"))'") + if ($UseMethod8) { + $argLines += "'-8'" + } + $argLines += @( + "'-i'", "'$($output.Replace("'", "''"))'" + ) + if (-not [string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) { + $argLines += @("'--firmware-modified'", "'$($FirmwareModifiedNote.Replace("'", "''"))'") + } + foreach ($arg in $ExtraFriiDumpArgs) { + $argLines += "'$($arg.Replace("'", "''"))'" + } + + $wrapperText = @" +`$ErrorActionPreference = 'Stop' +`$exe = '$($Exe.Replace("'", "''"))' +`$arguments = @( + $($argLines -join ",`r`n ") +) +Write-Host 'Allow data to be written, then press Ctrl+C once.' +& `$exe @arguments +exit `$LASTEXITCODE +"@ + Set-Content -LiteralPath $wrapper -Value $wrapperText -Encoding UTF8 + + Write-Host '' + Write-Host 'CONTROLLED CANCELLATION TEST PREPARED' + Write-Host 'Run this wrapper in a separate PowerShell window:' + Write-Host "powershell -NoProfile -ExecutionPolicy Bypass -File `"$wrapper`"" + Write-Host '' + Write-Host 'After the dump has written data, press Ctrl+C once. Then validate all generated reports with:' + Write-Host "powershell -NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" -Phase ValidateReports -SourceRoot `"$SourceRoot`" -ResultsRoot `"$ResultsRoot`"" + Write-Host '' + Write-Host 'Expected cancellation report: beside the final .log, partial_dump / partial, failure_stage=user_cancelled, scope=partial_progress.' + Write-Host 'The copied GDR-8050L Windows reference-dumper path is not yet an approved cancellation target.' + break + } + + 'ValidateReports' { + $reports = @(Get-ChildItem -LiteralPath $ResultsRoot -Recurse -Filter '*.friidump.json' -File) + Assert-True ($reports.Count -gt 0) "No native reports were found under $ResultsRoot." + Invoke-ReportValidation $ResultsRoot + + $runIds = @{} + foreach ($file in $reports) { + $report = Read-Report $file.FullName + Test-CommonReportIdentity $report $file.FullName + $runId = [string]$report.run.run_id + Assert-True (-not $runIds.ContainsKey($runId)) "Duplicate run UUID: $runId" + $runIds[$runId] = $true + + if ($report.dump.attempted -and $null -ne $report.dump.output_path) { + Test-DumpOutputIdentity $report ([string]$report.dump.output_path) + } + } + + $cancelReports = @( + $reports | Where-Object { + $document = Read-Report $_.FullName + $document.dump.failure_stage -eq 'user_cancelled' + } + ) + foreach ($file in $cancelReports) { + $report = Read-Report $file.FullName + Assert-True ($report.run.test_type -eq 'partial_dump') "$($file.FullName): cancellation test_type mismatch." + Assert-True ($report.run.result -eq 'partial') "$($file.FullName): cancellation run result mismatch." + Assert-True ($report.dump.result -eq 'partial') "$($file.FullName): cancellation dump result mismatch." + Assert-True (@($report.notes) -contains 'Measurement scope: partial_progress.') "$($file.FullName): cancellation scope mismatch." + + $outputPath = [System.IO.Path]::GetFullPath([string]$report.dump.output_path) + $finalLogPath = "$outputPath.log" + $reportParent = [System.IO.Path]::GetFullPath($file.DirectoryName).TrimEnd([char[]]"\/") + $logParent = [System.IO.Path]::GetFullPath((Split-Path -Parent $finalLogPath)).TrimEnd([char[]]"\/") + + Assert-True ($reportParent -eq $logParent) "$($file.FullName): default report is not beside the final FriiDump log." + Assert-True (Test-Path -LiteralPath $finalLogPath -PathType Leaf) "$($file.FullName): final FriiDump log is missing: $finalLogPath" + + $logText = Get-Content -Raw -LiteralPath $finalLogPath + Assert-True ($logText.Contains('Dump status.......: CANCELLED (PARTIAL)')) "${finalLogPath}: corrected cancellation status is missing." + Assert-True ($logText.Contains("Cancelled at......: sector $($report.dump.failure_sector)")) "${finalLogPath}: corrected cancellation-sector line is missing." + Assert-True ($logText.Contains("Completed sectors.: $($report.dump.sector_count)")) "${finalLogPath}: completed-sector line is missing or inconsistent." + Assert-True ($logText.Contains("Completed bytes...: $($report.dump.byte_count)")) "${finalLogPath}: completed-byte line is missing or inconsistent." + Assert-True (-not $logText.Contains('MiB ISO payload')) "${finalLogPath}: obsolete full-payload throughput wording remains." + + $averageMatch = [regex]::Match( + $logText, + 'Observed average\.\.: ([0-9]+(?:\.[0-9]+)?) MiB/h over ([0-9]+(?:\.[0-9]+)?) MiB completed output' + ) + Assert-True ($averageMatch.Success) "${finalLogPath}: corrected completed-output throughput line is missing." + + $culture = [System.Globalization.CultureInfo]::InvariantCulture + $observedRate = [double]::Parse($averageMatch.Groups[1].Value, $culture) + $observedMiB = [double]::Parse($averageMatch.Groups[2].Value, $culture) + $expectedMiB = [double]$report.dump.byte_count / 1MB + $expectedRate = $expectedMiB / [double]$report.dump.duration_seconds * 3600.0 + + Assert-True ([Math]::Abs($observedMiB - $expectedMiB) -lt 0.01) "${finalLogPath}: completed MiB does not match the report byte count." + Assert-True ([Math]::Abs($observedRate - $expectedRate) -lt 0.05) "${finalLogPath}: observed average does not use completed bytes and report duration." + + # Verify that the native report and human-readable summary agree + # with the authoritative Nintendo platform magic in sector 0. + # This catches HLDS drives that accept out-of-range dummy reads + # and therefore defeat the legacy capacity-only classifier. + $header = New-Object byte[] 32 + $stream = [System.IO.File]::OpenRead($outputPath) + + try { + $headerBytes = $stream.Read($header, 0, $header.Length) + } + finally { + $stream.Dispose() + } + + if ($headerBytes -ge 32) { + $wiiMagic = ( + ([uint32]$header[0x18] -shl 24) -bor + ([uint32]$header[0x19] -shl 16) -bor + ([uint32]$header[0x1a] -shl 8) -bor + [uint32]$header[0x1b] + ) + + $gameCubeMagic = ( + ([uint32]$header[0x1c] -shl 24) -bor + ([uint32]$header[0x1d] -shl 16) -bor + ([uint32]$header[0x1e] -shl 8) -bor + [uint32]$header[0x1f] + ) + + if ($wiiMagic -eq [uint32]0x5d1c9ea3) { + Assert-True ( + $report.media.platform -eq 'wii' + ) "${finalLogPath}: Wii header magic does not match the native-report platform." + } + elseif ($gameCubeMagic -eq [uint32]0xc2339f3d) { + Assert-True ( + $report.media.platform -eq 'gamecube' + ) "${finalLogPath}: GameCube header magic does not match the native-report platform." + + Assert-True ( + $logText.Contains('Disc type..........: GameCube') + ) "${finalLogPath}: GameCube header magic does not match the human-readable disc type." + + Assert-True ( + $logText.Contains('Expected sectors..: 712880') + ) "${finalLogPath}: GameCube header magic does not match the expected-sector geometry." + } + } + } + + Write-Host '' + Write-Host "LIVE NATIVE REPORT VALIDATION: PASS ($($reports.Count) reports)" + Write-Host "Controlled cancellation reports: $($cancelReports.Count)" + break + } + + 'Collect' { + Assert-True (Test-Path -LiteralPath $ResultsRoot -PathType Container) "Results directory is missing: $ResultsRoot" + $destination = "$ResultsRoot-results.zip" + if (Test-Path -LiteralPath $destination) { + Remove-Item -LiteralPath $destination -Force + } + Compress-Archive -Path (Join-Path $ResultsRoot '*') -DestinationPath $destination -CompressionLevel Optimal + Write-Host 'VALIDATION RESULT COLLECTION: PASS' + Write-Host "Archive: $destination" + Write-Host "Bytes: $((Get-Item -LiteralPath $destination).Length)" + Write-Host "SHA-256: $(Get-Sha256 $destination)" + break + } +}