From: jtryba Date: Sat, 18 Jul 2026 17:56:40 +0000 (-0500) Subject: FriiDump 0.5.3.15: close XGD1 geometry and lead-in capture X-Git-Tag: v0.5.3.15-pf1^0 X-Git-Url: https://git.jtryba.com/browse/friidump.git/commitdiff_plain/refs/heads/test/xgd1-security-cache-probe-v1?ds=inline;hp=6e43cd3fd4f7324a177331a01d653b4c19c4bca7 FriiDump 0.5.3.15: close XGD1 geometry and lead-in capture Promote the validated drive-captured 32-sector game lead-in, retain compact XGD1 closure evidence, correct executable-relative Redump DAT discovery, and remove transient probe artifacts. --- diff --git a/.gitignore b/.gitignore index 568cc49..419b1f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,36 @@ -# CMake and generic build output -/CMakeCache.txt -/CMakeFiles/ -/Makefile -/cmake_install.cmake -/install_manifest.txt -/BUILD/ +# CMake and generated build trees +CMakeCache.txt +CMakeFiles +Makefile +cmake_install.cmake +install_manifest.txt /build/ /build-*/ -/out/ - -# Windows/MSVC output -/.vs/ /build-msvc32/ -/friidump.exe -/friidump.pdb -/friidump.ilk -/friidump.exp -/friidump.lib -/friidump_msvc32_build_results.zip + +# Compiler and executable outputs *.obj +*.o +*.a +*.so +*.dll +*.exe +*.pdb +*.ilk -# Runtime logs and generated media artifacts +# Runtime logs and generated disc outputs at repository root /friidump.log -*.log -*.iso -*.xiso -*.raw -*.pfi.bin -*.dmi.bin -*.redump.json -*.media.json -*.xdvd.json +/*.iso +/*.xiso +/*.raw +/*.iso.log +/*.media.json +/*.xdvd.json +/*.pfi.bin +/*.dmi.bin +/*.ad_c0.bin +/*_build_results.zip -.DS_Store -Thumbs.db +# Local test/evidence bundles +/*-live-test-*.zip +/*-results.zip diff --git a/README b/README index 00bbfb8..a481527 100644 --- a/README +++ b/README @@ -201,6 +201,22 @@ checked against DiscImageCreator's Original Xbox/XGD1 model: total size 3,820,880 sectors, layer break LBA 1,913,776, DVD start PSN 0x30000, and Xbox/game start PSN 0x60600. +FriiDump 0.5.3.11 adds a read-only evidence mode for the unresolved synthetic +XGD1 ranges: + +```powershell +friidump -d --xgd1-layout-probe xgd1-layout-probe.json +``` + +This mode creates no ISO. On the GDR-8050L challenge-handshake profile it +establishes locked/video and unlocked/game states, reads selected boundary LBAs +with READ(10) and READ(12), verifies the exact expected capacities, and writes +an atomic JSON report containing SCSI status/sense evidence, SHA-1, nullable +failed-read classifications, explicit command comparability, and the complete +sector data for successful reads. The current pregame padding, postgame padding, and 32-sector +game lead-in remain unresolved; probe success must not be represented as an +exact Redump image. + On a GDR-8050L, FriiDump follows the original dumper's state order and timing: primary handshake, media-cycle, re-handshake, RefreshVolume plus full settle delays, metadata/XBE probe, media-cycle back to the visible DVD-video view, @@ -476,9 +492,9 @@ does the final handshake for game/XDVDFS data. FriiDump 0.5.3.9 also closes the Xbox live-summary boundary: the copied GDR-8050L path now returns title, media ID, finalized output sectors, and elapsed evidence to the final validation summary, and final STOP UNIT ownership is singular. -FriiDump 0.5.3.10 checks each successfully completed ISO against the canonical -Redump DAT selected for the detected or forced disc type. Verification begins -only after CRC32, MD5, SHA-1, and SHA-256 have been finalized. +FriiDump checks each successfully completed ISO against the canonical Redump +DAT selected for the detected or forced disc type. Verification begins only +after CRC32, MD5, SHA-1, and SHA-256 have been finalized. Stable DAT paths: @@ -542,18 +558,22 @@ an incomplete object with missing string values. For the GDR-8050L/cross-flashed GDR-8163B full-ISO path, the report identifies `GDR-8050L reference finalized full-file hashes` as the hash source. FriiDump also records that XGD1 acquisition success and exact Redump hash identity are -separate claims. The current reconstruction contains documented synthetic -ranges, so a clean physical read can legitimately produce `NO EXACT MATCH`. -That result must not be promoted to archive identity. +separate claims. The current reconstruction contains unresolved zero-filled pregame and +postgame spans, so a clean physical read can legitimately produce +`NO EXACT MATCH`. That result must not be promoted to archive identity. Options: -- `--redump-dat-dir ` selects another directory containing the three stable - DAT filenames above. +- `--redump-dat-dir ` explicitly selects a directory containing the + three stable DAT filenames above. Without it, FriiDump checks `redump_dat` + beside the executable and then under the current working directory. - `--redump-report ` writes the atomic machine-readable evidence report. - `--no-redump-verify` disables DAT lookup for a run. - `--nohash` also prevents DAT verification because the required hashes are not calculated. +- `--xgd1-layout-probe ` is a separate read-only Windows diagnostic for + the GDR-8050L challenge-handshake profile. It cannot be combined with image + output, conversion, all-methods, or HLDS 0xE7 probe options. Example: @@ -568,5 +588,68 @@ Example: ``` The DAT files are data inputs, not compiled into the executable. Replace the -three files in `redump_dat` with newer Redump exports while keeping the stable -filenames, or point `--redump-dat-dir` at an alternate set. +three files in the executable-relative `redump_dat` directory with newer Redump +exports while keeping the stable filenames, or point `--redump-dat-dir` at an +alternate set. The current-working-directory lookup is retained as a fallback +for source-tree and legacy workflows. + +FriiDump 0.5.3.12 adds a second read-only XGD1 evidence mode for the user's +modified GDR-8050L firmware: + + --xgd1-raw-id-probe + +The probe establishes the locked/video and unlocked/game views, performs +controlled 16-sector READ(12) cache fills, then uses the accepted HIT 0xE7 +memdump command at cache base 0x80000000 to capture the selected 2064-byte raw +sector. The report records the raw ID field and decodes its 24-bit physical +sector number. This is intended to test logical-to-physical geometry; it does +not read inaccessible filler sectors, does not modify the current XGD1 image +layout, and requires the modified 0xE7 firmware profile. + +The 0.5.3.11 live logical probe found 17 successful READ(10)/READ(12) pairs, +all byte-identical. Both active capacities rejected the sampled out-of-range +LBAs with 05/21/00. Therefore ordinary logical reads do not supply the current +synthetic pregame/postgame filler bytes. + +FriiDump 0.5.3.13 corrects the raw-ID probe after the first live v68 run +showed that arbitrary request LBAs could leave cache base 0x80000000 pointing +at an earlier or block-start window. The corrected probe aligns every request +to a 16-sector block, issues the proven zero-transfer READ(12) cache flush, +and dumps only the target slot's 12-byte header and four-byte EDC. It validates +layer bits and raw/normalized PSNs against the current full-output geometry. +Raw-cache user-data equality is no longer treated as evidence because Method 8 +reconstructs that field from READ(12) data before unscrambling. Geometry may be +resolved by a complete match; inaccessible filler bytes remain unresolved. + +FriiDump 0.5.3.14 drive-captured XGD1 game lead-in +---------------------------------------------------- + +The 0.5.3.13 live cache-aligned raw-ID run matched all 13 selected locked/video +and unlocked/game samples. It proved that unlocked source LBA 0..31 occupies +full-output LBA 198144..198175 and immediately precedes the XDVDFS header at +unlocked source LBA 32. FriiDump therefore no longer synthesizes the 32-sector +game lead-in for the GDR-8050L redump-style path. + +The reference Xbox path now reads unlocked source LBA 0..31 strictly before +copying source LBA 32..3431263. The native libfriidump path uses the same source +mapping and retains per-sector zero-fill only as an explicitly counted fallback +for a genuinely unreadable lead-in sector. Pregame and postgame physical +locations are resolved, but their inaccessible content remains zero-filled and +must not be described as exact without independent evidence. + + +FriiDump 0.5.3.15 release cleanup +----------------------------------- + +The 0.5.3.15 release candidate consolidates the XGD1 investigation into a +repository-ready tree. Local build logs, runner transcripts, package rehearsals, +duplicate stdout/stderr captures, and full logical-sector byte dumps are not +retained as source authority. Compact evidence is stored under +`docs/evidence/xgd1`. + +Automatic DAT discovery is now executable-relative first, with the historical +current-working-directory lookup retained as a fallback. An explicit +`--redump-dat-dir` remains authoritative. + +The reference and native full-output mappings remain those validated in +0.5.3.14. No XGD1 output bytes changed in this cleanup. diff --git a/docs/ChangeLog b/docs/ChangeLog index d865b6b..f893c78 100644 --- a/docs/ChangeLog +++ b/docs/ChangeLog @@ -1,3 +1,101 @@ +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 + 0012. The 3,820,880-sector output completed successfully and the + drive-captured 32-sector game lead-in matched the independent 0.5.3.13 + geometry evidence at both boundaries. + * Recorded final output hashes: CRC32 cc5f0e71, MD5 + 1207b2090701ba1f221d486c83c863e7, SHA-1 + 5ea9b2262ba951afd9a5d2d44cd2b62a4d9decc7, and SHA-256 + 5995f823c6d67b9e3e3b3e1c2b54e1e036793188bec6b5be4220f559305f53b9. + * Confirmed NO EXACT MATCH against 2,689 Microsoft Xbox DAT entries. This + does not invalidate acquisition: the inaccessible pregame and postgame + contents remain zero-filled unresolved placeholders. + * Made automatic DAT discovery executable-relative first, with the historical + current-working-directory lookup retained as a fallback. Explicit + --redump-dat-dir remains authoritative. + * Corrected sidecar wording: hardware geometry validation is complete, while + filler content remains unresolved. + * Replaced 71 local build/probe/debug artifacts with a compact evidence + directory containing the final geometry samples, machine-readable closure + record, media-layout evidence, and completed live summary. + * Added a C resolver test and expanded .gitignore coverage for build outputs, + runtime logs, and generated disc images. + * No XGD1 output-layout bytes changed after the accepted 0.5.3.14 live dump. + +2026-07-18 - FriiDump 0.5.3.14 drive-captured XGD1 game lead-in + + * Promoted the successful 0.5.3.13 live geometry result: all 13 selected + locked/video and unlocked/game raw-sector IDs matched the expected + full-output mapping. + * Replaced the GDR-8050L full-output synthetic game lead-in with drive reads + from unlocked source LBA 0..31 at output LBA 198144..198175. + * Kept XDVDFS/game data at unlocked source LBA 32..3431263 and output LBA + 198176..3629407. + * Updated .redump.json, .media.json, and .xdvd.json layout evidence to record + the drive-captured lead-in source and the 0.5.3.13 physical-geometry proof. + * Pregame and postgame physical locations are now documented as geometry + verified, while their inaccessible content remains zero-filled and + unresolved. + * No claim of Redump identity is made until a new 0.5.3.14 full-disc dump is + hashed and compared with the canonical DAT. + +2026-07-18 - FriiDump 0.5.3.13 XGD1 cache-aligned raw-ID geometry correction + + * Corrected the v68 raw-ID probe to mirror the proven modified-GDR-8050L + Method 8 cache contract: zero-transfer READ(12) cache flush, 16-sector + block-aligned streaming fill, and 2064-byte slot indexing. + * Removed raw-cache user-data equality as a geometry criterion. Method 8 + reconstructs sectors from cache header/EDC plus READ(12) data before + unscrambling, so equality is not expected. + * Added explicit full-output LBA mapping, layer-bit decoding, 24-bit layer-1 + PSN normalization, expected raw/normalized PSNs, and per-sample geometry + verdicts. + * Schema 2 reports now set physical_geometry_resolved only when all 13 + selected locked/video and unlocked/game samples match exactly. + * Retained the v68 live run as negative/diagnostic evidence; it is not a + geometry promotion. No reconstruction bytes or filler policy changed. + +2026-07-18 - FriiDump 0.5.3.12 XGD1 raw-sector ID geometry probe + + * Live validation of 0.5.3.11 confirmed that sampled READ(10) and READ(12) + sectors are identical in both locked/video and unlocked/game states. + * The locked view ends at LBA 6991 and the unlocked view ends at LBA + 3431263; ordinary logical reads expose no alternate source for the + documented synthetic pregame or postgame ranges. + * Added --xgd1-raw-id-probe for modified GDR-8050L firmware with HIT 0xE7 + memdump support. It performs controlled READ(12) cache fills, captures the + corresponding 2064-byte raw cache sectors at 0x80000000, and records the + decoded DVD physical-sector number from each raw ID field. + * XGD1 probe modes now measure real elapsed time and suppress the unrelated + HLDS GC/Wii validation summary that previously reported misleading + seed/dump/STOP fields for a successful diagnostic run. + * No reconstruction mapping changed. Raw IDs are geometry evidence only; + inaccessible filler bytes remain unresolved. + +2026-07-18 - FriiDump 0.5.3.11 read-only XGD1 logical-boundary probe + +- Adds `--xgd1-layout-probe ` for the GDR-8050L challenge-handshake + profile on Windows. +- Establishes and verifies the locked/video and unlocked/game views using + READ CAPACITY(10), then probes selected XGD1 boundary LBAs with one-sector + READ(10) and READ(12) commands. +- Records per-command success, Win32 error, SCSI status, sense key/ASC/ASCQ, + nullable nonzero-byte/all-zero fields for failed reads, SHA-1, complete + 2048-byte sector data for successful reads, and explicit READ(10)/READ(12) + comparability in an atomic JSON report. +- Includes the current output-layout constants and probes the visible video + boundaries, synthetic pregame/postgame boundaries, game lead-in/XDVDFS + boundary, output layer break mapping, and each reported-capacity edge. +- Restores the entry locked/video state when the probe began locked; an + already-unlocked entry remains in the game view. +- Keeps the existing 0.5.3.10 ISO/XISO output unchanged. The probe explicitly + leaves the pregame padding, postgame padding, and game lead-in unresolved + until live evidence supports a source mapping. +- Uses only logical read commands, the established authentication handshake, + tray transitions, volume guard, and STOP UNIT cleanup. No firmware write, + flash, erase, 3B06, 3B07, or FLASHUP path was added or changed. + 2026-07-18 - FriiDump 0.5.3.10 state-aware GDR-8050L handshake cleanup - Replaces the copied reference bridge's unconditional unlock -> tray cycle -> diff --git a/docs/README.technical b/docs/README.technical index 058de16..c820e86 100644 --- a/docs/README.technical +++ b/docs/README.technical @@ -315,9 +315,9 @@ media FriiDump writes this layout: - output LBA 0..6831: visible DVD-video L0, read from source LBA 0..6831; - output LBA 6832..198143: pregame padding, currently zero-filled; -- output LBA 198144..198175: 32-sector game lead-in; FriiDump attempts to read - these sectors from the active unlocked/game view and zero-fills only unreadable - sectors; +- output LBA 198144..198175: 32-sector game lead-in, read from unlocked source + LBA 0..31; the native libfriidump path zero-fills only a genuinely unreadable + sector and records the fallback count; - output LBA 198176..3629407: unlocked game/XDVDFS data, normally read from source LBA 32 on GDR-8050L, or from the detected XDVDFS source LBA on other supported profiles; @@ -342,9 +342,10 @@ detected XDVDFS start LBA through LBA 1913920 and prepends a 32-sector game lead-in. For single-layer/homebrew-style media it treats the XDVDFS VolumeSize field as a byte count and converts it to 2048-byte sectors. -Both Xbox output styles attempt to read their 32-sector game lead-in from the -active drive view and zero-fill only sectors that are not readable through the -selected drive/profile. +Both Xbox output styles preserve the 32-sector game lead-in from unlocked +source LBA 0..31. The GDR-8050L reference path fails the dump if this proven +range cannot be captured; the native libfriidump path retains a counted +per-sector zero-fill fallback for other profiles or transient read failures. Supported-drive notes diff --git a/docs/XBOX.md b/docs/XBOX.md index 29c1f6e..40b17fb 100644 --- a/docs/XBOX.md +++ b/docs/XBOX.md @@ -70,11 +70,12 @@ When available, FriiDump also writes these files next to the ISO: - `.redump.json` - JSON metadata describing the layout, hashes, drive profile, PFI/DMI filenames, and lead-in capture status. -For the GDR-8050L redump path, the 32-sector game lead-in follows the original -dumper's option-1 behavior and is written as zero-fill before copying unlocked -XDVDFS data from source LBA `32`. For non-8050L Xbox strategies, FriiDump still -attempts to read lead-in sectors when the active drive/profile can expose them, -and zero-fills only unreadable lead-in sectors. +For the GDR-8050L redump path, the 32-sector game lead-in is captured from +unlocked source LBA `0..31` before copying XDVDFS data from source LBA `32`. +FriiDump 0.5.3.13 proved this placement with cache-aligned raw-sector IDs. The +native libfriidump path retains per-sector zero-fill only for a genuinely +unreadable lead-in sector and records the read/zero-fill counts; the reference +Xbox path treats failure to capture the proven lead-in as a dump failure. FriiDump also records a `discimagecreator_reference` section in the JSON. The redump-style constants are checked against DiscImageCreator's Original @@ -109,6 +110,36 @@ to per-sector zero-fill only where reads fail. `--xiso` is a separate output mode and cannot be combined with `-i`, `-r`, or `-a` in one run. +## Read-only XGD1 logical-boundary probe + +FriiDump 0.5.3.11 adds a Windows-only diagnostic that does not create or alter +an ISO: + +```powershell +friidump -d f: --xgd1-layout-probe "docs\debug\xgd1-layout-probe.json" +``` + +The mode is currently restricted to the GDR-8050L challenge-handshake profile. +It performs the following evidence sequence: + +1. Record the entry READ CAPACITY(10) state. +2. Cycle the tray and verify the exact 6,992-sector locked/video capacity. +3. Probe selected boundary LBAs with one-sector READ(10) and READ(12). +4. Apply the established full Xbox handshake and verify the unlocked/game + capacity. +5. Probe the corresponding unlocked-source and capacity-edge LBAs. +6. Restore the locked/video entry state when the run began locked. +7. Atomically publish the JSON report and issue STOP UNIT. + +Each command record includes the Win32 result, SCSI status, sense +key/ASC/ASCQ, all-zero/nonzero classification, SHA-1, and full 2048-byte sector +data when the read succeeds. Expected out-of-range failures are evidence and +do not by themselves invalidate the report. + +This is a logical-read probe, not a physical-sector dumper. It does not assert +that inaccessible filler is zero, does not replace the current synthetic +ranges, and does not promote the reconstructed XGD1 image to Redump identity. + ## GDR-8050L handshake sequence The GDR-8050L path performs this sequence when switching from visible DVD-video view @@ -185,7 +216,7 @@ bare `-i` with XBE/DMI-derived names without hashing the 7.29 GiB file twice. Use `--redump-report ` to publish the same atomic schema-2 evidence record used by GameCube/Wii paths. -The current XGD1 image map contains documented synthetic zero-filled ranges. +The current XGD1 image map contains unresolved zero-filled pregame and postgame ranges. Accordingly, `NO EXACT MATCH` is a valid evidence result even when all readable sectors were acquired successfully. The verifier reports acquisition evidence and archive identity as separate claims. `-X` remains outside DAT verification @@ -194,6 +225,39 @@ because XISO is not a full Redump disc image. GDR-3120L remains on the separate `FF 08 01` vendor lock/unlock path. Forced Xbox mode on non-supported Xbox unlock profiles is rejected rather than routed through FriiDump GC/Wii fallback methods. + +## XGD1 cache-aligned raw-ID geometry correction (0.5.3.13) + +The first 0.5.3.12 hardware report contained a mixture of valid physical IDs +and stale/block-start cache entries. The corrected probe now performs the same +zero-transfer READ(12) flush and 16-sector block alignment used by the proven +modified-GDR-8050L Method 8 path. It dumps only the raw 12-byte header and +four-byte EDC for the selected slot, decodes the layer bit, normalizes layer-1 +PSNs by 24-bit complement, and compares each sample with its mapped full-output +LBA. + +The live 0.5.3.13 run matched all 13 selected samples and set +`physical_geometry_resolved` true. The result proves the physical placement of +the visible video ranges, the full unlocked game range, and the omitted +pregame/postgame spans. It does not establish the byte content of the omitted +filler spans. + +## Drive-captured XGD1 game lead-in (0.5.3.14) + +The successful 0.5.3.13 geometry report established these adjacent mappings: + +- unlocked source LBA `0..31` -> output LBA `198144..198175`, raw PSN + `0x060600..0x06061F`; +- unlocked source LBA `32` -> output LBA `198176`, raw PSN `0x060620`, where + the `MICROSOFT*XBOX*MEDIA` XDVDFS descriptor begins. + +FriiDump 0.5.3.14 therefore replaces the historical synthetic 32-sector +game-lead-in write with drive capture from source LBA `0..31`. This changes the +full XGD1 output bytes and requires a new live full-disc dump plus Redump DAT +comparison. The 191,312-sector pregame and postgame spans retain zero-filled +placeholder content: their physical geometry is proven, but their inaccessible +byte content is not. + ## Logging The original Xbox reference logger is now the shared FriiDump run logger. Every @@ -273,3 +337,48 @@ XDVD sidecar authentication evidence now distinguishes two separate facts: The compatibility field `requires_media_transition` is also `false`; it refers to authentication/unlock acceptance, not to restoring the locked video view. + +## XGD1 raw-sector ID geometry probe (0.5.3.12) + +Live 0.5.3.11 evidence closed the ordinary logical-read branch: all 17 +successful READ(10)/READ(12) pairs were identical, the locked view stopped at +6991, and the unlocked view stopped at 3431263. Output-space LBAs outside +those capacities returned ILLEGAL REQUEST / LBA OUT OF RANGE (05/21/00). +This means neither READ opcode exposes a hidden logical source for the +synthetic pregame or postgame ranges. + +`--xgd1-raw-id-probe ` is the next read-only geometry probe. On the +modified GDR-8050L profile it fills the drive cache with a controlled 16-sector +READ(12), dumps the selected 2064-byte raw sector through `E7 48 49 54 01`, and +decodes bytes 1..3 of the raw DVD ID as the physical-sector number. Adjacent +locked and unlocked boundary samples can establish whether the active logical +views map linearly or jump across omitted physical regions. + +Raw physical IDs do not reveal the contents of inaccessible filler sectors. +No zero-filled range may be promoted to exact merely because its geometry is +confirmed. + + +## 0.5.3.15 live closure + +The required 0.5.3.14 hardware run completed on the modified GDR-8050L +revision `0012`. + +The drive-captured source LBA `0..31` lead-in was independently verified at +output LBA `198144..198175`, and source LBA `32` remained the XDVDFS boundary at +output LBA `198176`. + +Final output: + +- sectors: `3,820,880`; +- bytes: `7,825,162,240`; +- SHA-256: + `5995f823c6d67b9e3e3b3e1c2b54e1e036793188bec6b5be4220f559305f53b9`. + +The Microsoft Xbox DAT scan returned `NO EXACT MATCH`. Pregame and postgame +geometry is proven, but their inaccessible content remains zero-filled and +unresolved. + +Without `--redump-dat-dir`, DAT discovery checks the executable-relative +`redump_dat` directory before the current working directory. This prevents +runner working directories from hiding the packaged DAT set. diff --git a/docs/evidence/xgd1/FRIIDUMP_XGD1_CLOSURE_EVIDENCE.json b/docs/evidence/xgd1/FRIIDUMP_XGD1_CLOSURE_EVIDENCE.json new file mode 100644 index 0000000..df5c415 --- /dev/null +++ b/docs/evidence/xgd1/FRIIDUMP_XGD1_CLOSURE_EVIDENCE.json @@ -0,0 +1,347 @@ +{ + "schema": "project-frankenstein-friidump-xgd1-closure.v1", + "generated_utc": "2026-07-18T17:00:00Z", + "software": { + "friidump_version_under_test": "0.5.3.14", + "cleanup_release_candidate": "0.5.3.15", + "base_commit_before_cleanup": "6e43cd3fd4f7324a177331a01d653b4c19c4bca7" + }, + "hardware": { + "drive_model": "HL-DT-ST DVD-ROM GDR8050L", + "firmware_revision": "0012", + "modified_firmware_e7_memdump": true + }, + "logical_boundary_probe": { + "locked_visible_sectors": 6992, + "unlocked_visible_sectors": 3431264, + "read10_read12_successful_pairs": 17, + "read10_read12_all_equal": true, + "ordinary_reads_expose_pregame_or_postgame_filler": false + }, + "physical_geometry_probe": { + "schema_version": 2, + "required_states_verified": true, + "geometry_validation_failures": 0, + "physical_geometry_resolved": true, + "geometry_model": { + "dvd_start_psn": 196608, + "game_output_start_lba": 198144, + "layer_break_lba": 1913776, + "layer_zero_last_normalized_psn": 2110383, + "layer_one_raw_psn_is_24bit_complement": true + }, + "sample_count": 13, + "samples": [ + { + "state": "locked_video", + "lba": 0, + "label": "locked_logical_start", + "cache_fill_lba": 0, + "cache_index": 0, + "layer_number": 0, + "physical_sector_number": 196608, + "normalized_physical_sector_number": 196608, + "output_lba": 0, + "expected_layer_number": 0, + "expected_physical_sector_number": 196608, + "expected_normalized_physical_sector_number": 196608, + "geometry_matches_expected": true, + "logical_sha1": "605db3fdbaff4ba13729371ad0c4fbab3889378e", + "raw_header_hex": "20030000d6f5000000000000", + "raw_edc_hex": "0560250d" + }, + { + "state": "locked_video", + "lba": 31, + "label": "locked_early_lba_31", + "cache_fill_lba": 16, + "cache_index": 15, + "layer_number": 0, + "physical_sector_number": 196639, + "normalized_physical_sector_number": 196639, + "output_lba": 31, + "expected_layer_number": 0, + "expected_physical_sector_number": 196639, + "expected_normalized_physical_sector_number": 196639, + "geometry_matches_expected": true, + "logical_sha1": "605db3fdbaff4ba13729371ad0c4fbab3889378e", + "raw_header_hex": "2003001ff7cb000000000000", + "raw_edc_hex": "83903453" + }, + { + "state": "locked_video", + "lba": 32, + "label": "locked_early_lba_32", + "cache_fill_lba": 32, + "cache_index": 0, + "layer_number": 0, + "physical_sector_number": 196640, + "normalized_physical_sector_number": 196640, + "output_lba": 32, + "expected_layer_number": 0, + "expected_physical_sector_number": 196640, + "expected_normalized_physical_sector_number": 196640, + "geometry_matches_expected": true, + "logical_sha1": "3e4eb33f37c052df89b0ccc4e239828c3b9556aa", + "raw_header_hex": "20030020b6b5000000000000", + "raw_edc_hex": "0b23613f" + }, + { + "state": "locked_video", + "lba": 6831, + "label": "video_l0_last", + "cache_fill_lba": 6816, + "cache_index": 15, + "layer_number": 0, + "physical_sector_number": 203439, + "normalized_physical_sector_number": 203439, + "output_lba": 6831, + "expected_layer_number": 0, + "expected_physical_sector_number": 203439, + "expected_normalized_physical_sector_number": 203439, + "geometry_matches_expected": true, + "logical_sha1": "36661a56261adccc33185e383fb022c0e36da109", + "raw_header_hex": "20031aaf7cea000000000000", + "raw_edc_hex": "cf771b52" + }, + { + "state": "locked_video", + "lba": 6832, + "label": "video_l1_first", + "cache_fill_lba": 6832, + "cache_index": 0, + "layer_number": 1, + "physical_sector_number": 16573776, + "normalized_physical_sector_number": 203439, + "output_lba": 3820720, + "expected_layer_number": 1, + "expected_physical_sector_number": 16573776, + "expected_normalized_physical_sector_number": 203439, + "geometry_matches_expected": true, + "logical_sha1": "605db3fdbaff4ba13729371ad0c4fbab3889378e", + "raw_header_hex": "01fce550135b000000000000", + "raw_edc_hex": "d8734d6e" + }, + { + "state": "locked_video", + "lba": 6991, + "label": "locked_video_last", + "cache_fill_lba": 6976, + "cache_index": 15, + "layer_number": 1, + "physical_sector_number": 16573935, + "normalized_physical_sector_number": 203280, + "output_lba": 3820879, + "expected_layer_number": 1, + "expected_physical_sector_number": 16573935, + "expected_normalized_physical_sector_number": 203280, + "geometry_matches_expected": true, + "logical_sha1": "775869f6db440ccfd5f1cc826a725c745ffda9d4", + "raw_header_hex": "01fce5efcf38000000000000", + "raw_edc_hex": "84c7dc79" + }, + { + "state": "unlocked_game", + "lba": 0, + "label": "unlocked_source_start", + "cache_fill_lba": 0, + "cache_index": 0, + "layer_number": 0, + "physical_sector_number": 394752, + "normalized_physical_sector_number": 394752, + "output_lba": 198144, + "expected_layer_number": 0, + "expected_physical_sector_number": 394752, + "expected_normalized_physical_sector_number": 394752, + "geometry_matches_expected": true, + "logical_sha1": "22815399cde9e12d539d64cec38f332830994614", + "raw_header_hex": "6e060600deb0000000000000", + "raw_edc_hex": "60ae4fb1" + }, + { + "state": "unlocked_game", + "lba": 31, + "label": "unlocked_leadin_last", + "cache_fill_lba": 16, + "cache_index": 15, + "layer_number": 0, + "physical_sector_number": 394783, + "normalized_physical_sector_number": 394783, + "output_lba": 198175, + "expected_layer_number": 0, + "expected_physical_sector_number": 394783, + "expected_normalized_physical_sector_number": 394783, + "geometry_matches_expected": true, + "logical_sha1": "b9aee89323d1fa0071c13d736b3536251a4773d2", + "raw_header_hex": "6e06061fff8e000000000000", + "raw_edc_hex": "e873673b" + }, + { + "state": "unlocked_game", + "lba": 32, + "label": "xdfs_source_start", + "cache_fill_lba": 32, + "cache_index": 0, + "layer_number": 0, + "physical_sector_number": 394784, + "normalized_physical_sector_number": 394784, + "output_lba": 198176, + "expected_layer_number": 0, + "expected_physical_sector_number": 394784, + "expected_normalized_physical_sector_number": 394784, + "geometry_matches_expected": true, + "logical_sha1": "827a963f0b7db9749bf127525c45f84cf18ef666", + "raw_header_hex": "6e060620bef0000000000000", + "raw_edc_hex": "5f747f7e" + }, + { + "state": "unlocked_game", + "lba": 33, + "label": "xdfs_source_next", + "cache_fill_lba": 32, + "cache_index": 1, + "layer_number": 0, + "physical_sector_number": 394785, + "normalized_physical_sector_number": 394785, + "output_lba": 198177, + "expected_layer_number": 0, + "expected_physical_sector_number": 394785, + "expected_normalized_physical_sector_number": 394785, + "geometry_matches_expected": true, + "logical_sha1": "cdda4c9956920f00e23cea7a48a4e7a74244b565", + "raw_header_hex": "6e060621bdf2000000000000", + "raw_edc_hex": "fe1f7ff9" + }, + { + "state": "unlocked_game", + "lba": 1715631, + "label": "mapped_layer_break_source_minus_one", + "cache_fill_lba": 1715616, + "cache_index": 15, + "layer_number": 0, + "physical_sector_number": 2110383, + "normalized_physical_sector_number": 2110383, + "output_lba": 1913775, + "expected_layer_number": 0, + "expected_physical_sector_number": 2110383, + "expected_normalized_physical_sector_number": 2110383, + "geometry_matches_expected": true, + "logical_sha1": "ad29c120fb69e05dceec32ef76674d8d4c38c739", + "raw_header_hex": "6e2033af66b4000000000000", + "raw_edc_hex": "8fc60c80" + }, + { + "state": "unlocked_game", + "lba": 1715632, + "label": "mapped_layer_break_source", + "cache_fill_lba": 1715632, + "cache_index": 0, + "layer_number": 1, + "physical_sector_number": 14666832, + "normalized_physical_sector_number": 2110383, + "output_lba": 1913776, + "expected_layer_number": 1, + "expected_physical_sector_number": 14666832, + "expected_normalized_physical_sector_number": 2110383, + "geometry_matches_expected": true, + "logical_sha1": "99b55f2cc7da36729f1597a5f6b28131e8be6417", + "raw_header_hex": "4fdfcc500905000000000000", + "raw_edc_hex": "f420ba4b" + }, + { + "state": "unlocked_game", + "lba": 3431263, + "label": "unlocked_source_last", + "cache_fill_lba": 3431248, + "cache_index": 15, + "layer_number": 1, + "physical_sector_number": 16382463, + "normalized_physical_sector_number": 394752, + "output_lba": 3629407, + "expected_layer_number": 1, + "expected_physical_sector_number": 16382463, + "expected_normalized_physical_sector_number": 394752, + "geometry_matches_expected": true, + "logical_sha1": "de52cf1b22c636d9f360e4faeaad55271dff459d", + "raw_header_hex": "4ff9f9ffb101000000000000", + "raw_edc_hex": "79dfff25" + } + ] + }, + "drive_captured_game_leadin": { + "unlocked_source_start_lba": 0, + "sector_count": 32, + "output_start_lba": 198144, + "output_end_lba": 198175, + "nonzero_bytes": 65266, + "sha1": "e5cd93fe0f7c11c93878d2cb9d909de1c333978c", + "sha256": "0ada0cffb6fe9ff506955cb2a16a7f33a4ff9ae8360e9c352a4fcc843abf5af2", + "first_sector_sha1": "22815399cde9e12d539d64cec38f332830994614", + "last_sector_sha1": "b9aee89323d1fa0071c13d736b3536251a4773d2", + "xdfs_output_lba": 198176, + "xdfs_first_sector_sha1": "827a963f0b7db9749bf127525c45f84cf18ef666", + "xdfs_magic": "MICROSOFT*XBOX*MEDIA", + "source": "drive_read10", + "verified": true + }, + "full_output": { + "title": "Red Faction II", + "media_id": "TQ00501A", + "sector_size": 2048, + "sector_count": 3820880, + "byte_count": 7825162240, + "hashes": { + "crc32": "cc5f0e71", + "md5": "1207b2090701ba1f221d486c83c863e7", + "sha1": "5ea9b2262ba951afd9a5d2d44cd2b62a4d9decc7", + "sha256": "5995f823c6d67b9e3e3b3e1c2b54e1e036793188bec6b5be4220f559305f53b9" + }, + "layout": { + "layout_type": "original_xbox_xgd1_redump_style_2048", + "layers": 2, + "layer_break_lba": 1913776, + "video_l0_start_lba": 0, + "video_l0_sector_count": 6832, + "pregame_padding_start_lba": 6832, + "pregame_padding_sector_count": 191312, + "game_output_start_lba": 198144, + "game_leadin_unlocked_source_start_lba": 0, + "game_leadin_source_sector_count": 32, + "game_leadin_source": "drive_read10", + "game_unlocked_source_start_lba": 32, + "game_unlocked_source_sector_count": 3431232, + "game_xiso_leadin_sector_count": 32, + "xdfs_volume_lba_within_game_region": 32, + "game_sector_count": 3431264, + "postgame_padding_start_lba": 3629408, + "postgame_padding_sector_count": 191312, + "video_l1_start_lba": 3820720, + "video_l1_sector_count": 160, + "legacy_contiguous_visible_sector_count": 198144, + "drive_locked_visible_sector_count": 6992, + "drive_reported_unlocked_sector_count": 3431264, + "reconstructed_output_sector_count": 3820880 + } + }, + "redump_verification": { + "dat_name": "Microsoft - Xbox.dat", + "dat_sha256": "e7a76ba732b7b978148e93ecd92befd3b59ef8e850eca10241390d405f7f4116", + "entries_scanned": 2689, + "status": "NO MATCH", + "overall": "NO EXACT MATCH", + "exact_matches": 0, + "size_matches": 2626, + "crc32_matches": 0, + "md5_matches": 0, + "sha1_matches": 0, + "detail": "No entry matched all required fields and no hash-correlated candidate was found." + }, + "evidence_boundary": { + "pregame_physical_geometry_resolved": true, + "postgame_physical_geometry_resolved": true, + "pregame_content_resolved": false, + "postgame_content_resolved": false, + "current_filler_policy": "zero-filled unresolved placeholder", + "exact_redump_identity_established": false + } +} diff --git a/docs/evidence/xgd1/FRIIDUMP_XGD1_GEOMETRY_SAMPLES.csv b/docs/evidence/xgd1/FRIIDUMP_XGD1_GEOMETRY_SAMPLES.csv new file mode 100644 index 0000000..3b4c19f --- /dev/null +++ b/docs/evidence/xgd1/FRIIDUMP_XGD1_GEOMETRY_SAMPLES.csv @@ -0,0 +1,14 @@ +state,lba,label,cache_fill_lba,cache_index,layer_number,physical_sector_number,normalized_physical_sector_number,output_lba,expected_layer_number,expected_physical_sector_number,expected_normalized_physical_sector_number,geometry_matches_expected,logical_sha1,raw_header_hex,raw_edc_hex +locked_video,0,locked_logical_start,0,0,0,196608,196608,0,0,196608,196608,True,605db3fdbaff4ba13729371ad0c4fbab3889378e,20030000d6f5000000000000,0560250d +locked_video,31,locked_early_lba_31,16,15,0,196639,196639,31,0,196639,196639,True,605db3fdbaff4ba13729371ad0c4fbab3889378e,2003001ff7cb000000000000,83903453 +locked_video,32,locked_early_lba_32,32,0,0,196640,196640,32,0,196640,196640,True,3e4eb33f37c052df89b0ccc4e239828c3b9556aa,20030020b6b5000000000000,0b23613f +locked_video,6831,video_l0_last,6816,15,0,203439,203439,6831,0,203439,203439,True,36661a56261adccc33185e383fb022c0e36da109,20031aaf7cea000000000000,cf771b52 +locked_video,6832,video_l1_first,6832,0,1,16573776,203439,3820720,1,16573776,203439,True,605db3fdbaff4ba13729371ad0c4fbab3889378e,01fce550135b000000000000,d8734d6e +locked_video,6991,locked_video_last,6976,15,1,16573935,203280,3820879,1,16573935,203280,True,775869f6db440ccfd5f1cc826a725c745ffda9d4,01fce5efcf38000000000000,84c7dc79 +unlocked_game,0,unlocked_source_start,0,0,0,394752,394752,198144,0,394752,394752,True,22815399cde9e12d539d64cec38f332830994614,6e060600deb0000000000000,60ae4fb1 +unlocked_game,31,unlocked_leadin_last,16,15,0,394783,394783,198175,0,394783,394783,True,b9aee89323d1fa0071c13d736b3536251a4773d2,6e06061fff8e000000000000,e873673b +unlocked_game,32,xdfs_source_start,32,0,0,394784,394784,198176,0,394784,394784,True,827a963f0b7db9749bf127525c45f84cf18ef666,6e060620bef0000000000000,5f747f7e +unlocked_game,33,xdfs_source_next,32,1,0,394785,394785,198177,0,394785,394785,True,cdda4c9956920f00e23cea7a48a4e7a74244b565,6e060621bdf2000000000000,fe1f7ff9 +unlocked_game,1715631,mapped_layer_break_source_minus_one,1715616,15,0,2110383,2110383,1913775,0,2110383,2110383,True,ad29c120fb69e05dceec32ef76674d8d4c38c739,6e2033af66b4000000000000,8fc60c80 +unlocked_game,1715632,mapped_layer_break_source,1715632,0,1,14666832,2110383,1913776,1,14666832,2110383,True,99b55f2cc7da36729f1597a5f6b28131e8be6417,4fdfcc500905000000000000,f420ba4b +unlocked_game,3431263,unlocked_source_last,3431248,15,1,16382463,394752,3629407,1,16382463,394752,True,de52cf1b22c636d9f360e4faeaad55271dff459d,4ff9f9ffb101000000000000,79dfff25 diff --git a/docs/evidence/xgd1/FRIIDUMP_XGD1_V70_LIVE_SUMMARY.txt b/docs/evidence/xgd1/FRIIDUMP_XGD1_V70_LIVE_SUMMARY.txt new file mode 100644 index 0000000..d23ac3a --- /dev/null +++ b/docs/evidence/xgd1/FRIIDUMP_XGD1_V70_LIVE_SUMMARY.txt @@ -0,0 +1,27 @@ +FriiDump XGD1 drive-captured lead-in live closure +Result: PASS +Validated FriiDump version: 0.5.3.14 +Drive: HL-DT-ST DVD-ROM GDR8050L revision 0012 +Output sectors: 3820880 +Output bytes: 7825162240 +CRC32: cc5f0e71 +MD5: 1207b2090701ba1f221d486c83c863e7 +SHA-1: 5ea9b2262ba951afd9a5d2d44cd2b62a4d9decc7 +SHA-256: 5995f823c6d67b9e3e3b3e1c2b54e1e036793188bec6b5be4220f559305f53b9 +Lead-in unlocked source LBA: 0..31 +Lead-in output LBA: 198144..198175 +Lead-in nonzero bytes: 65266 +Lead-in SHA-1: e5cd93fe0f7c11c93878d2cb9d909de1c333978c +Lead-in SHA-256: 0ada0cffb6fe9ff506955cb2a16a7f33a4ff9ae8360e9c352a4fcc843abf5af2 +Lead-in first sector SHA-1: 22815399cde9e12d539d64cec38f332830994614 +Lead-in last sector SHA-1: b9aee89323d1fa0071c13d736b3536251a4773d2 +XDVDFS output LBA: 198176 +XDVDFS first sector SHA-1: 827a963f0b7db9749bf127525c45f84cf18ef666 +XDVDFS magic: MICROSOFT*XBOX*MEDIA +DAT: Microsoft - Xbox.dat +DAT SHA-256: e7a76ba732b7b978148e93ecd92befd3b59ef8e850eca10241390d405f7f4116 +DAT entries scanned: 2689 +DAT status: NO EXACT MATCH +Exact matches: 0 +Pregame content: unresolved zero-filled placeholder +Postgame content: unresolved zero-filled placeholder diff --git a/docs/evidence/xgd1/FRIIDUMP_XGD1_V70_MEDIA_EVIDENCE.json b/docs/evidence/xgd1/FRIIDUMP_XGD1_V70_MEDIA_EVIDENCE.json new file mode 100644 index 0000000..ec395c7 --- /dev/null +++ b/docs/evidence/xgd1/FRIIDUMP_XGD1_V70_MEDIA_EVIDENCE.json @@ -0,0 +1,57 @@ +{ + "format": "xdvd-media-profile", + "version": 1, + "media_id": "TQ00501A", + "title_hint": "Red_Faction_II", + "image": { + "sector_size": 2048, + "sector_count": 3820880, + "byte_count": 7825162240, + "hashes": { + "crc32": "cc5f0e71", + "md5": "1207b2090701ba1f221d486c83c863e7", + "sha1": "5ea9b2262ba951afd9a5d2d44cd2b62a4d9decc7", + "sha256": "5995f823c6d67b9e3e3b3e1c2b54e1e036793188bec6b5be4220f559305f53b9" + } + }, + "layout": { + "layout_type": "original_xbox_xgd1_redump_style_2048", + "layers": 2, + "layer_break_lba": 1913776, + "video_l0_start_lba": 0, + "video_l0_sector_count": 6832, + "pregame_padding_start_lba": 6832, + "pregame_padding_sector_count": 191312, + "game_output_start_lba": 198144, + "game_leadin_unlocked_source_start_lba": 0, + "game_leadin_source_sector_count": 32, + "game_leadin_source": "drive_read10", + "game_unlocked_source_start_lba": 32, + "game_unlocked_source_sector_count": 3431232, + "game_xiso_leadin_sector_count": 32, + "xdfs_volume_lba_within_game_region": 32, + "game_sector_count": 3431264, + "postgame_padding_start_lba": 3629408, + "postgame_padding_sector_count": 191312, + "video_l1_start_lba": 3820720, + "video_l1_sector_count": 160, + "legacy_contiguous_visible_sector_count": 198144, + "drive_locked_visible_sector_count": 6992, + "drive_reported_unlocked_sector_count": 3431264, + "reconstructed_output_sector_count": 3820880 + }, + "reconstruction": { + "is_reconstructed_layout": true, + "filler_policy": "pregame_and_postgame_zero_fill_content_placeholder", + "filler_geometry_verified": true, + "filler_verified_from_disc": false, + "pending_hardware_capture": false, + "unresolved_filler_content": true, + "note": "Pregame and postgame physical locations are verified; inaccessible contents remain unresolved. The 32-sector game lead-in is drive-captured." + }, + "validation": { + "byte_count_matches_sector_count": true, + "lead_in_mapping_verified": true, + "redump_exact_match": false + } +} diff --git a/docs/evidence/xgd1/README.md b/docs/evidence/xgd1/README.md new file mode 100644 index 0000000..d4f7f08 --- /dev/null +++ b/docs/evidence/xgd1/README.md @@ -0,0 +1,31 @@ +# XGD1 geometry and drive-captured lead-in evidence + +This directory contains the compact, repository-appropriate evidence retained +from the FriiDump 0.5.3.11 through 0.5.3.14 investigation. + +Retained: + +- the 13 cache-aligned physical-sector geometry samples; +- the consolidated machine-readable closure record; +- the completed 0.5.3.14 full-output and Redump comparison summary; +- a path-sanitized media/layout evidence record. + +Not retained: + +- local build logs; +- runner transcripts; +- package checksum rehearsals; +- duplicate stdout/stderr captures; +- full logical-sector byte dumps; +- the 7.29 GiB ISO; +- PFI, DMI, AD C0, or other disc-specific binary payloads. + +Evidence conclusion: + +- unlocked source LBA `0..31` is real drive-readable content and maps to + full-output LBA `198144..198175`; +- unlocked source LBA `32` begins XDVDFS at output LBA `198176`; +- pregame and postgame physical geometry is resolved; +- inaccessible pregame and postgame contents remain unresolved zero-filled + placeholders; +- no exact Microsoft Xbox DAT identity was established. diff --git a/docs/options b/docs/options index b39d268..b106e85 100644 --- a/docs/options +++ b/docs/options @@ -1,4 +1,4 @@ -FriiDump 0.5.3.2 - Copyright (C) 2007 Arep +FriiDump 0.5.3.15 - 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. @@ -36,21 +36,24 @@ Available command line options: 3 - DVD 4 - Xbox/XGD 2048-byte-sector mode Native profiles: GDR-8050L and GDR-3120L. - Forced Xbox mode requires a supported Xbox - unlock profile; no GC/Wii fallback is used. + Other drives keep normal FriiDump behavior + unless Xbox mode is explicitly forced. -S, --size Force disc size -r, --raw Output to file in raw format (2064-byte sectors) - -i, --iso Output to file in ISO format (2048-byte - sectors). For Xbox/XGD this reconstructs the - redump-style XGD1 layout and writes .pfi.bin, - .dmi.bin, and .redump.json metadata when possible - -X, --xiso Output Xbox/XGD game partition as XISO (.xiso). - Attempts to read the 32-sector game lead-in - from drive-readable sectors and zero-fills only - unreadable lead-in sectors. Use -i for - redump-style ISO + PFI/DMI/JSON; use -X only - for game-partition XISO. + -i, --iso[=] Output to file in ISO format (2048-byte + sectors). For Xbox/GDR-8050L, omitting + derives Title[MediaID].iso from the XBE/DMI; + providing is an explicit override. For + Xbox/XGD this reconstructs the redump-style + XGD1 layout and writes .pfi.bin, .dmi.bin, and + .redump.json metadata when possible + -X, --xiso[=] Output Xbox/XGD game partition as XISO (.xiso). + For Xbox/GDR-8050L, omitting derives + Title[MediaID].xiso from the XBE/DMI; providing + is an explicit override. Attempts to read + the 32-sector game lead-in from drive-readable + sectors and zero-fills only unreadable sectors. -u, --unscramble Convert (unscramble) raw image contained in to ISO format -H, --nohash Do not compute CRC32/MD5/SHA-1/SHA-256 hashes @@ -77,14 +80,23 @@ Available command line options: -9, --method9 Use dumping method 9 (Read and dump 5 blocks at a time, using streaming read, using DMA and some speed tricks) + --hlds-e7-scan Probe HLDS HIT 0xE7 cache/memdump bases only; + writes JSON and does not crack seeds or dump data + --hlds-e7-subcmd-sweep Probe HIT 0xE7 subcommands/address candidates only; + writes JSON and does not crack seeds or dump data + --hlds-e7-memrange-sweep Sweep wider HIT 0xE7 subcmd 0x01 address ranges; + writes JSON and does not crack seeds or dump data + --scan-log JSON output path for --hlds-e7-scan + --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) + --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 -A, --allmethods Try all known command/method combinations until one works. Reopens the drive for each command so command-specific vendor handlers are rebound. - -Xbox dump notes: - - GDR-8050L redump-style -i follows the original dumper order: - primary handshake, media-cycle, re-handshake, metadata/auth probe, - media-cycle back to visible DVD-video view, capture video L0/L1, final - handshake, zero-fill the 32-sector game lead-in, read game data, then write - .pfi.bin, .dmi.bin, and .redump.json. - - FriiDump sends STOP UNIT / spin-down after dump attempts, success or failure. diff --git a/docs/reports/FRIIDUMP_XGD1_CACHE_ALIGNED_GEOMETRY_PROBE_0.5.3.13.md b/docs/reports/FRIIDUMP_XGD1_CACHE_ALIGNED_GEOMETRY_PROBE_0.5.3.13.md new file mode 100644 index 0000000..78a1a92 --- /dev/null +++ b/docs/reports/FRIIDUMP_XGD1_CACHE_ALIGNED_GEOMETRY_PROBE_0.5.3.13.md @@ -0,0 +1,60 @@ +# FriiDump 0.5.3.13 XGD1 cache-aligned raw-ID geometry probe + +## Objective + +Correct the v68 raw-ID acquisition contract and test the current XGD1 +logical-to-physical output mapping with the same cache discipline used by the +proven modified-GDR-8050L Method 8 path. + +## Why v68 was not promotable + +The v68 hardware run succeeded at issuing commands, but its raw-ID table mixed +valid physical IDs with stale or block-start cache entries. It issued streaming +READ(12) at arbitrary LBAs without the zero-transfer cache flush and without +aligning the request to the 16-sector block represented at cache base +`0x80000000`. It also treated raw-cache user-data equality as meaningful even +though Method 8 reconstructs the raw sector from cache header/EDC plus READ(12) +user data before unscrambling. + +## Corrected capture sequence + +For every selected logical LBA: + +1. Align the cache-fill LBA downward to a 16-sector boundary. +2. Issue READ(12) with byte 1 set to `0x08` and zero transfer length to reset the + cache mapping. +3. Issue a 16-sector streaming READ(12) at the aligned block start. +4. Retain the requested logical sector from the returned 2048-byte data. +5. Dump only the selected cache slot's 12-byte ID/IED/CPR_MAI header and + four-byte EDC using `E7 48 49 54 01`. +6. Decode layer number from sector-information bit 0 and decode the 24-bit raw + physical-sector number. +7. Normalize layer-1 PSNs by 24-bit complement and compare each sample to the + full reconstructed-output geometry. + +## Geometry model under test + +- DVD start PSN: `0x030000`. +- Full output layer break: LBA `1,913,776`. +- Layer-0 last normalized PSN: `0x2033AF`. +- Unlocked/game view maps to full output beginning at LBA `198,144`. +- Locked LBAs `0..6831` map to output video L0. +- Locked LBAs `6832..6991` map to output LBAs `3,820,720..3,820,879`. +- Layer-1 raw PSNs are compared in their 24-bit complemented representation. + +The probe reports `physical_geometry_resolved=true` only when both states are +verified and all 13 selected samples match the expected layer and PSN exactly. +A mismatch is preserved in the atomic JSON report and causes a nonzero exit. + +## Interpretation boundary + +A full geometry match validates placement of readable video and game sectors in +the current reconstructed output. It does not reveal inaccessible pregame or +postgame filler sectors and does not establish their byte values. Filler-content +claims remain false. + +## Safety + +The probe uses readiness, tray, authentication, READ CAPACITY, cache-flush +READ(12), streaming READ(12), and HIT 0xE7 data-in reads. It issues no firmware +write, flash, erase, 3B06, 3B07, or FLASHUP command and creates no ISO. diff --git a/docs/reports/FRIIDUMP_XGD1_DRIVE_CAPTURED_GAME_LEADIN_0.5.3.14.md b/docs/reports/FRIIDUMP_XGD1_DRIVE_CAPTURED_GAME_LEADIN_0.5.3.14.md new file mode 100644 index 0000000..3658701 --- /dev/null +++ b/docs/reports/FRIIDUMP_XGD1_DRIVE_CAPTURED_GAME_LEADIN_0.5.3.14.md @@ -0,0 +1,73 @@ +# FriiDump 0.5.3.14 drive-captured XGD1 game lead-in + +## Evidence basis + +The 0.5.3.13 live cache-aligned raw-ID probe completed successfully on the +modified `HL-DT-ST DVD-ROM GDR8050L` revision `0012`. Both required drive states +were established, all 13 selected samples matched the expected layer and raw +physical-sector number, independent validation reported zero failures, and the +report set `physical_geometry_resolved` to true. + +The decisive adjacent samples were: + +| Unlocked source LBA | Output LBA | Raw PSN | Meaning | +|---:|---:|---:|---| +| 0 | 198144 | `0x060600` | first game-region lead-in sector | +| 31 | 198175 | `0x06061F` | last game-region lead-in sector | +| 32 | 198176 | `0x060620` | XDVDFS descriptor sector | +| 33 | 198177 | `0x060621` | next XDVDFS sector | + +This closes the earlier ambiguity around the 32-sector region. It is not +synthetic padding: it is a readable physical range exposed by the unlocked +GDR-8050L view. + +## 0.5.3.14 behavior + +The redump-style full-output path now writes: + +- unlocked source LBA `0..31` to output LBA `198144..198175`; +- unlocked source LBA `32..3431263` to output LBA `198176..3629407`. + +The Windows reference path treats failure to capture LBA `0..31` as a dump +failure. The native libfriidump path uses the existing per-sector read/fallback +helper and records `readable_sectors` and `zero_filled_sectors` in +`.redump.json`. + +The metadata sidecars now distinguish: + +- drive-captured game lead-in; +- geometry-verified pregame/postgame spans; +- unresolved pregame/postgame content that remains zero-filled. + +## Evidence boundary + +This update does not claim the pregame or postgame zero-filled bytes are exact. +The 0.5.3.13 proof establishes where those omitted physical spans lie, not their +inaccessible contents. + +## Live closure + +The required full-disc 0.5.3.14 run completed on the modified GDR-8050L +revision `0012`. + +Observed output: + +- sectors: `3,820,880`; +- bytes: `7,825,162,240`; +- CRC32: `cc5f0e71`; +- MD5: `1207b2090701ba1f221d486c83c863e7`; +- SHA-1: `5ea9b2262ba951afd9a5d2d44cd2b62a4d9decc7`; +- SHA-256: + `5995f823c6d67b9e3e3b3e1c2b54e1e036793188bec6b5be4220f559305f53b9`. + +The lead-in at output LBA `198144..198175` contained `65,266` nonzero bytes. +Its first and last sector SHA-1 values matched the independent 0.5.3.13 source +LBA `0` and source LBA `31` evidence. The XDVDFS descriptor at output LBA +`198176` also matched source LBA `32` and contained +`MICROSOFT*XBOX*MEDIA`. + +The Microsoft Xbox DAT comparison scanned `2,689` entries and returned +`NO EXACT MATCH`: `2,626` size matches and zero CRC32, MD5, or SHA-1 matches. +No archive identity is established. The unresolved pregame and postgame +placeholder contents remain the documented reason that a clean acquisition can +differ from a canonical full-disc image. diff --git a/docs/reports/FRIIDUMP_XGD1_LIVE_CLOSURE_AND_RELEASE_CLEANUP_0.5.3.15.md b/docs/reports/FRIIDUMP_XGD1_LIVE_CLOSURE_AND_RELEASE_CLEANUP_0.5.3.15.md new file mode 100644 index 0000000..9bb1191 --- /dev/null +++ b/docs/reports/FRIIDUMP_XGD1_LIVE_CLOSURE_AND_RELEASE_CLEANUP_0.5.3.15.md @@ -0,0 +1,59 @@ +# FriiDump 0.5.3.15 XGD1 live closure and release cleanup + +## Scope + +This release candidate consolidates the accepted 0.5.3.11 through 0.5.3.14 +XGD1 investigation into a repository-ready state. It does not change the +0.5.3.14 full-output mapping. + +## Proven behavior + +The modified GDR-8050L revision `0012` established: + +- locked/video visible capacity: `6,992` sectors; +- unlocked/game visible capacity: `3,431,264` sectors; +- all 17 successful paired READ(10)/READ(12) samples were byte-identical; +- all 13 cache-aligned raw-sector-ID samples matched the expected physical + geometry; +- unlocked source LBA `0..31` maps to output LBA `198144..198175`; +- unlocked source LBA `32` begins XDVDFS at output LBA `198176`. + +The completed 0.5.3.14 full-output run independently verified the first and last +lead-in sectors, the XDVDFS boundary, all sidecar mappings, and finalized output +hashes. + +## Evidence boundary + +The pregame and postgame physical locations are resolved. Their inaccessible +contents are not. FriiDump continues to write zero-filled placeholders for +those two spans and records that limitation in sidecars and Redump evidence. + +The Microsoft Xbox DAT comparison returned `NO EXACT MATCH`. Acquisition +success and archive identity remain separate claims. + +## Repository cleanup + +The development packages contained local CMake logs, package rehearsals, +stdout/stderr captures, runner transcripts, and duplicate raw probe reports. +Those files are not source authority. + +The cleaned tree retains: + +- the implementation and diagnostic probe commands; +- four engineering reports for versions 0.5.3.11 through 0.5.3.14; +- a compact machine-readable XGD1 closure record; +- the 13 geometry samples; +- path-sanitized 0.5.3.14 media-layout evidence; +- the completed live summary; +- a C test for DAT path resolution. + +## DAT discovery correction + +Without `--redump-dat-dir`, FriiDump now checks: + +1. `redump_dat` beside the running executable; +2. `redump_dat` under the current working directory. + +An explicit `--redump-dat-dir` remains the first and authoritative choice. +This prevents a valid packaged DAT set from being missed merely because a test +runner launches FriiDump from another directory. diff --git a/docs/reports/FRIIDUMP_XGD1_LOGICAL_BOUNDARY_PROBE_0.5.3.11.md b/docs/reports/FRIIDUMP_XGD1_LOGICAL_BOUNDARY_PROBE_0.5.3.11.md new file mode 100644 index 0000000..0ccca91 --- /dev/null +++ b/docs/reports/FRIIDUMP_XGD1_LOGICAL_BOUNDARY_PROBE_0.5.3.11.md @@ -0,0 +1,94 @@ +# FriiDump 0.5.3.11 XGD1 logical-boundary probe + +## Objective + +Collect direct, read-only evidence about the XGD1 ranges that FriiDump +0.5.3.10 still represents with documented synthetic zero-fill. This update +does not change the reconstructed ISO layout. + +## Command + +```powershell +friidump -d f: --xgd1-layout-probe "docs\debug\xgd1-layout-probe.json" +``` + +The option is exclusive with image output, conversion, `--allmethods`, and the +HLDS 0xE7 probe modes. + +## State sequence + +1. Record entry READ CAPACITY(10). +2. Cycle the tray to establish the locked/video view. +3. Probe locked/video boundary LBAs using one-sector READ(10) and READ(12). +4. Apply the existing GDR-8050L full authentication handshake. +5. Verify the unlocked/game READ CAPACITY(10) state. +6. Probe unlocked-source and capacity-edge LBAs using READ(10) and READ(12). +7. Restore the entry locked/video state when required. +8. Atomically publish the JSON report. +9. Return to FriiDump for one STOP UNIT cleanup. + +## Evidence captured per command + +- command success; +- Win32 error; +- SCSI status; +- sense key, ASC, and ASCQ; +- count of nonzero bytes for a successful read, otherwise `null`; +- all-zero classification for a successful read, otherwise `null`; +- SHA-1 of a successful 2048-byte sector; +- complete successful sector bytes as hexadecimal; +- explicit READ(10)/READ(12) comparability and equality for the same LBA; +- exact 6,992-sector locked/video and 3,431,264-sector unlocked/game state + verification. + +## Probe points + +The locked/video state includes the logical start, LBA 31/32, VIDEO-L0 end, +locked VIDEO-L1 start/end, locked-capacity edge, synthetic pregame boundary, +game-output start, output layer break, synthetic postgame boundary, VIDEO-L1 +output start, and full-output end. + +The unlocked/game state includes source LBA 0/31/32/33, the synthetic-gap +length, output-boundary values interpreted as unlocked source LBAs, the mapped +output-layer-break source, the final reported game-view sector, and +out-of-range capacity/output candidates. + +## Interpretation boundary + +A successful logical read proves only that the active firmware state exposes +that LBA through ordinary READ(10) or READ(12). A failed read is also useful +sense evidence. Neither result alone proves the physical mastering contents of +an inaccessible XGD1 filler range. + +The report therefore fixes these fields to false until later analysis: + +- `pregame_padding_resolved` +- `postgame_padding_resolved` +- `game_leadin_resolved` + +## Safety + +No firmware update, write, flash, erase, vendor-memory write, 3B06, 3B07, or +FLASHUP command is used. The probe uses readiness checks, tray transitions, +the already-established authentication handshake, READ CAPACITY, logical +READ(10)/READ(12), a Windows volume guard, atomic report publication, and STOP +UNIT cleanup. + +## Live validation result (2026-07-18) + +The cross-flashed GDR-8163B running modified GDR-8050L 0012 firmware passed +the probe. Entry state was the 3,431,264-sector game view. A tray cycle +established the exact 6,992-sector locked/video view, the full challenge +handshake restored the exact game view, and the run ended in its original +game-view state. + +Seventeen sampled LBAs were readable in-range. READ(10) and READ(12) returned +identical 2048-byte sectors for every successful pair. Eleven sampled +out-of-range LBAs failed under both commands with sense 05/21/00. Locked LBA +6992 was rejected, and unlocked LBA 3431264 was rejected. + +This closes the ordinary logical-read branch: there is no second READ(12) +window and no numeric output-LBA path to the synthetic pregame or postgame +regions. Unlocked LBA 0..31 is directly readable and nonzero, while LBA 32 +contains `MICROSOFT*XBOX*MEDIA`; exact physical placement still requires raw +sector-ID geometry evidence. diff --git a/docs/reports/FRIIDUMP_XGD1_RAW_ID_GEOMETRY_PROBE_0.5.3.12.md b/docs/reports/FRIIDUMP_XGD1_RAW_ID_GEOMETRY_PROBE_0.5.3.12.md new file mode 100644 index 0000000..20019b6 --- /dev/null +++ b/docs/reports/FRIIDUMP_XGD1_RAW_ID_GEOMETRY_PROBE_0.5.3.12.md @@ -0,0 +1,58 @@ +# FriiDump 0.5.3.12 XGD1 raw-sector ID geometry probe + +## Objective + +Use the modified GDR-8050L HIT 0xE7 memory-dump surface to recover the raw DVD +ID fields associated with selected logical LBAs. The 24-bit physical-sector +number in each raw ID can test the logical-to-physical geometry that ordinary +READ(10)/READ(12) could not resolve. + +## Command + +```powershell +friidump -d f: --xgd1-raw-id-probe "docs\debug\xgd1-raw-id-probe.json" +``` + +This mode is Windows-only, exclusive with all image-output and other probe +modes, limited to the GDR-8050L challenge-handshake path, and additionally +requires the modified HIT 0xE7 profile. + +## Capture sequence + +1. Record entry capacity. +2. Tray-cycle to the 6,992-sector locked/video state. +3. For selected locked LBAs, issue a 16-sector streaming READ(12), retain the + target 2048-byte logical sector, and dump the corresponding 2064-byte raw + cache slot from base 0x80000000. +4. Apply the existing challenge handshake and verify the 3,431,264-sector game + state. +5. Repeat at selected unlocked LBAs, including source start, XDFS start, mapped + layer-break neighbors, and source end. +6. Restore the entry state when needed, atomically publish JSON, and issue one + STOP UNIT. + +## Evidence per sample + +- logical LBA, cache-fill LBA, cache slot, and absolute memory address; +- READ(12) and HIT 0xE7 command status plus sense; +- full logical 2048-byte sector and full raw 2064-byte cache sector; +- SHA-1 for logical, raw, and raw-user-data regions; +- raw-user/logical equality; +- raw 12-byte ID/IED/CPR_MAI header and four-byte EDC; +- sector-information byte; +- decoded 24-bit physical-sector number. + +## Interpretation boundary + +A valid raw ID can prove geometry or a discontinuity between logical views. It +does not make omitted filler sectors readable and does not establish their +bytes. `physical_geometry_resolved`, `pregame_filler_content_resolved`, and +`postgame_filler_content_resolved` remain false in the generated report until +the captured values are analyzed. + +## Safety + +The probe issues readiness, tray, authentication, READ CAPACITY, READ(12), and +HIT 0xE7 data-in memory reads only. It does not issue firmware updates, flash, +erase, vendor-memory writes, 3B06, 3B07, or FLASHUP commands and does not create +an ISO. diff --git a/libfriidump/CMakeLists.txt b/libfriidump/CMakeLists.txt index 1356bab..0670c87 100644 --- a/libfriidump/CMakeLists.txt +++ b/libfriidump/CMakeLists.txt @@ -33,6 +33,7 @@ add_library ( win32compat.c xbox_ref_bridge.h xbox_ref_bridge.c + xbox_xgd1_raw_id_probe.c xbox_ref/xbox_ref_log.c ) diff --git a/libfriidump/dumper.c b/libfriidump/dumper.c index 36ac682..5279535 100644 --- a/libfriidump/dumper.c +++ b/libfriidump/dumper.c @@ -321,6 +321,10 @@ static void xbox_write_redump_metadata (dumper *dmp, xbox_redump_metadata *meta) fprintf (json, " \"pregame_padding_sector_count\": %u,\n", XBOX_XGD1_GAME_OUTPUT_START_LBA - XBOX_XGD1_VIDEO_L0_SECTORS); fprintf (json, " \"game_output_start_lba\": %u,\n", XBOX_XGD1_GAME_OUTPUT_START_LBA); fprintf (json, " \"game_leadin_sector_count\": %u,\n", XBOX_XISO_LEADIN_SECTORS); + fprintf (json, " \"game_leadin_unlocked_source_start_lba\": %u,\n", + (meta -> game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ? + meta -> game_source_lba - XBOX_XISO_LEADIN_SECTORS : 0); + fprintf (json, " \"game_leadin_source_policy\": \"drive_read10_with_per_sector_zero_fallback\",\n"); fprintf (json, " \"game_unlocked_source_start_lba\": %u,\n", meta -> game_source_lba); fprintf (json, " \"game_unlocked_source_sector_count\": %u,\n", meta -> game_source_sectors); fprintf (json, " \"postgame_padding_start_lba\": %u,\n", XBOX_XGD1_GAME_OUTPUT_START_LBA + XBOX_XGD1_UNLOCKED_GAME_VIEW_SECTORS); @@ -339,8 +343,13 @@ static void xbox_write_redump_metadata (dumper *dmp, xbox_redump_metadata *meta) fprintf (json, " },\n"); fprintf (json, " \"lead_in_capture\": {\n"); fprintf (json, " \"game_leadin_attempted\": true,\n"); + fprintf (json, " \"unlocked_source_start_lba\": %u,\n", + (meta -> game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ? + meta -> game_source_lba - XBOX_XISO_LEADIN_SECTORS : 0); + fprintf (json, " \"sector_count\": %u,\n", XBOX_XISO_LEADIN_SECTORS); fprintf (json, " \"readable_sectors\": %u,\n", meta -> game_leadin_read_sectors); - fprintf (json, " \"zero_filled_sectors\": %u\n", meta -> game_leadin_zero_sectors); + fprintf (json, " \"zero_filled_sectors\": %u,\n", meta -> game_leadin_zero_sectors); + fprintf (json, " \"physical_geometry_evidence\": \"friidump_0.5.3.13_cache_aligned_raw_id_probe\"\n"); fprintf (json, " },\n"); fprintf (json, " \"dvd_structures\": {\n"); fprintf (json, " \"pfi\": "); if (pfi_leaf) xbox_json_string (json, pfi_leaf); else fprintf (json, "null"); fprintf (json, ",\n"); @@ -348,7 +357,8 @@ static void xbox_write_redump_metadata (dumper *dmp, xbox_redump_metadata *meta) fprintf (json, " },\n"); fprintf (json, " \"notes\": [\n"); fprintf (json, " \"PFI and DMI are stored as separate binary READ DVD STRUCTURE captures because they are not READ(10) user-data sectors.\",\n"); - fprintf (json, " \"Pregame and postgame padding are zero-filled unless a future workflow proves readable source sectors for those ranges.\"\n"); + fprintf (json, " \"The 32-sector game lead-in is captured from unlocked source LBA 0..31; only unreadable lead-in sectors use zero-fill fallback.\",\n"); + fprintf (json, " \"Pregame and postgame padding retain zero-filled placeholder content; their physical locations are resolved but their inaccessible bytes are not.\"\n"); fprintf (json, " ]\n"); fprintf (json, "}\n"); fclose (json); @@ -1064,21 +1074,19 @@ static int dumper_dump_xbox_redump_iso (dumper *dmp, u_int32_t *current_sector) 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); - if (disc_is_xbox_challenge_drive (dmp -> dsk)) { - /* Match the original GDR-8050L dumper's option-1 redump path: the - * 32-sector XISO/game lead-in area is synthetic zero-fill, and the real - * XDVDFS data begins at unlocked source LBA 32. */ - xbox_ref_log_fprintf (stderr, "[XBOX] Writing 32-sector game lead-in as zero-fill for GDR-8050L redump flow.\n"); - meta.game_leadin_zero_sectors += XBOX_XISO_LEADIN_SECTORS; - if (current_sector) *current_sector = XBOX_XGD1_GAME_OUTPUT_START_LBA; - if (!xbox_write_zero_iso_range (dmp, XBOX_XISO_LEADIN_SECTORS, XBOX_XGD1_GAME_OUTPUT_START_LBA, meta.output_sectors, "GAME-XISO-LEADIN")) - goto cleanup; - } else { - 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)) - goto cleanup; - } + /* FriiDump 0.5.3.13 cache-aligned raw-ID validation proved that the + * unlocked GDR-8050L view exposes source LBA 0..31 as the physical + * game-region lead-in immediately preceding the XDVDFS header at LBA 32. + * Read those sectors from the drive; retain the existing per-sector + * zero-fill fallback only for genuinely unreadable sectors. */ + xbox_ref_log_fprintf (stderr, + "[XBOX] Capturing 32-sector game lead-in from unlocked source LBA %u..%u.\n", + (game_source_lba >= XBOX_XISO_LEADIN_SECTORS) ? game_source_lba - XBOX_XISO_LEADIN_SECTORS : 0, + (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)) + 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)) diff --git a/libfriidump/xbox_ref/utils.c b/libfriidump/xbox_ref/utils.c index 3cf2811..c63b03b 100644 --- a/libfriidump/xbox_ref/utils.c +++ b/libfriidump/xbox_ref/utils.c @@ -1981,7 +1981,7 @@ static void JsonWriteValidationWarnings(FILE *f, const XboxDvdSidecarCapture *ca if (!payloadFilesPresent) WRITE_WARNING("payload_files_not_fully_present"); if (redumpStyleZeroFilledPadding) - WRITE_WARNING("redump_style_padding_zero_filled_pending_hardware_capture"); + WRITE_WARNING("redump_style_padding_zero_filled_unresolved_content"); #undef WRITE_WARNING @@ -2476,6 +2476,9 @@ static BOOL WriteXboxDvdMediaProfileFile(const char *isoFilename, fprintf(json, " \"pregame_padding_start_lba\": %u,\n", redumpStyle ? XGD1_VIDEO_L0_SECTORS : 0U); fprintf(json, " \"pregame_padding_sector_count\": %u,\n", redumpStyle ? (XGD1_GAME_OUTPUT_START_LBA - XGD1_VIDEO_L0_SECTORS) : 0U); fprintf(json, " \"game_output_start_lba\": %u,\n", gameOutputLba); + fprintf(json, " \"game_leadin_unlocked_source_start_lba\": %u,\n", redumpStyle ? 0U : gameSourceLba); + fprintf(json, " \"game_leadin_source_sector_count\": %u,\n", redumpStyle ? XGD1_XISO_LEADIN_SECTORS : 0U); + fprintf(json, " \"game_leadin_source\": "); JsonWriteEscapedString(json, redumpStyle ? "drive_read10" : "not_applicable"); fprintf(json, ",\n"); fprintf(json, " \"game_unlocked_source_start_lba\": %u,\n", gameSourceLba); fprintf(json, " \"game_unlocked_source_sector_count\": %u,\n", redumpStyle ? XGD1_GAME_SOURCE_SECTORS : gameSectors); fprintf(json, " \"game_xiso_leadin_sector_count\": %u,\n", redumpStyle ? XGD1_XISO_LEADIN_SECTORS : 0U); @@ -2494,10 +2497,12 @@ static BOOL WriteXboxDvdMediaProfileFile(const char *isoFilename, fprintf(json, " \"reconstruction\": {\n"); fprintf(json, " \"is_reconstructed_layout\": %s,\n", redumpStyle ? "true" : "false"); - fprintf(json, " \"filler_policy\": "); JsonWriteEscapedString(json, redumpStyle ? "zero_fill_until_drive_can_read_filler" : "not_applicable"); fprintf(json, ",\n"); + fprintf(json, " \"filler_policy\": "); JsonWriteEscapedString(json, redumpStyle ? "pregame_and_postgame_zero_fill_content_placeholder" : "not_applicable"); fprintf(json, ",\n"); + fprintf(json, " \"filler_geometry_verified\": %s,\n", redumpStyle ? "true" : "false"); fprintf(json, " \"filler_verified_from_disc\": false,\n"); fprintf(json, " \"filler_byte_value\": %s,\n", redumpStyle ? "0" : "null"); - fprintf(json, " \"pending_hardware_capture\": %s,\n", redumpStyle ? "true" : "false"); + fprintf(json, " \"pending_hardware_capture\": false,\n"); + fprintf(json, " \"unresolved_filler_content\": %s,\n", redumpStyle ? "true" : "false"); fprintf(json, " \"filler_ranges\": [\n"); if (redumpStyle) { @@ -2515,7 +2520,7 @@ static BOOL WriteXboxDvdMediaProfileFile(const char *isoFilename, fprintf(json, " }\n"); } fprintf(json, " ],\n"); - fprintf(json, " \"note\": \"Padding/filler ranges are intentionally zero-filled placeholders until a drive/workflow capable of reading those regions is available.\"\n"); + fprintf(json, " \"note\": \"Pregame and postgame physical locations are verified by cache-aligned raw sector IDs, but their inaccessible contents remain zero-filled placeholders. The 32-sector game lead-in is drive-captured from unlocked LBA 0..31.\"\n"); fprintf(json, " },\n"); fprintf(json, " \"dvd_structures\": {\n"); @@ -2628,6 +2633,9 @@ static BOOL WriteXboxDvdSidecarFiles(const char *isoFilename, fprintf(json, " \"pregame_padding_start_lba\": %u,\n", redumpStyle ? XGD1_VIDEO_L0_SECTORS : 0U); fprintf(json, " \"pregame_padding_sector_count\": %u,\n", redumpStyle ? (XGD1_GAME_OUTPUT_START_LBA - XGD1_VIDEO_L0_SECTORS) : 0U); fprintf(json, " \"game_output_start_lba\": %u,\n", gameOutputLba); + fprintf(json, " \"game_leadin_unlocked_source_start_lba\": %u,\n", redumpStyle ? 0U : gameSourceLba); + fprintf(json, " \"game_leadin_source_sector_count\": %u,\n", redumpStyle ? XGD1_XISO_LEADIN_SECTORS : 0U); + fprintf(json, " \"game_leadin_source\": "); JsonWriteEscapedString(json, redumpStyle ? "drive_read10" : "not_applicable"); fprintf(json, ",\n"); fprintf(json, " \"game_unlocked_source_start_lba\": %u,\n", gameSourceLba); fprintf(json, " \"game_unlocked_source_sector_count\": %u,\n", redumpStyle ? XGD1_GAME_SOURCE_SECTORS : gameSectors); fprintf(json, " \"game_xiso_leadin_sector_count\": %u,\n", redumpStyle ? XGD1_XISO_LEADIN_SECTORS : 0U); @@ -2941,20 +2949,28 @@ BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat, uin printf("[WARN] XDFS was detected at unlocked source LBA %u, not the expected XISO header LBA 32.\n", detectedXdfsLba); } - // Build the Redump-style XISO/game region using the same proven - // convention as option 2: the first 32 sectors are synthetic XISO - // lead-in/padding, and the real XDFS volume begins at source LBA 32. - // Do not read unlocked source LBA 0..31 here; on the 8050L path those - // LBAs are not the XDFS header sectors we want in the rebuilt image. + // FriiDump 0.5.3.13 cache-aligned raw-ID validation proved that + // unlocked source LBA 0..31 is the physical 32-sector game-region + // lead-in and that XDVDFS begins at source LBA 32. Preserve the + // drive-captured lead-in instead of synthesizing zero sectors. gameSourceLba = XGD1_GAME_SOURCE_START_LBA; gameSectors = REDUMP_SECTORS; rawVideoSectors = XGD1_GAME_OUTPUT_START_LBA; rawGameSourceLba = gameSourceLba; rawGameSectors = gameSectors; - printf("[RAW] Writing %u-sector XISO lead-in/padding at output LBA %u.\n", - XGD1_XISO_LEADIN_SECTORS, XGD1_GAME_OUTPUT_START_LBA); - if (!WriteZeroSectorsOutput(outFile, hHash, XGD1_XISO_LEADIN_SECTORS, XGD1_GAME_OUTPUT_START_LBA, "GAME-XISO-LEADIN")) + printf("[RAW] Capturing %u-sector game lead-in from unlocked source LBA 0..%u at output LBA %u.\n", + XGD1_XISO_LEADIN_SECTORS, + XGD1_XISO_LEADIN_SECTORS - 1, + XGD1_GAME_OUTPUT_START_LBA); + if (!DumpSectorRangeWithRetry(hDevice, + outFile, + hHash, + 0, + XGD1_XISO_LEADIN_SECTORS, + XGD1_GAME_OUTPUT_START_LBA, + "GAME-XISO-LEADIN", + TRUE)) { VirtualFree(videoL1Buffer, 0, MEM_RELEASE); goto cleanup; diff --git a/libfriidump/xbox_ref_bridge.c b/libfriidump/xbox_ref_bridge.c index 663805d..33e987c 100644 --- a/libfriidump/xbox_ref_bridge.c +++ b/libfriidump/xbox_ref_bridge.c @@ -13,8 +13,12 @@ void xbox_ref_dump_result_init(xbox_ref_dump_result *result) #define _WIN32_WINNT 0x0500 #include #include +#include #include +#include +#include #include "xbox_ref/utils.h" +#include "xbox_ref/sha1.h" #include "xbox_ref/unlock.h" #include "xbox_ref/xbox_ref_log.h" @@ -27,6 +31,16 @@ static BOOL xbox_ref_capacity_is_game_view(uint32_t sectors) return sectors > XBOX_REF_LOCKED_VIDEO_VIEW_MAX_SECTORS; } +static BOOL xbox_ref_capacity_is_xgd1_locked_view(uint32_t sectors) +{ + return sectors == XGD1_VIDEO_TOTAL_SECTORS; +} + +static BOOL xbox_ref_capacity_is_xgd1_game_view(uint32_t sectors) +{ + return sectors == REDUMP_SECTORS; +} + static uint32_t xbox_ref_refresh_ready_capacity(HANDLE hDevice) { RefreshVolume(hDevice); @@ -43,6 +57,746 @@ static char xbox_ref_drive_letter_from_device(const char *device) return device[0]; } +#define XGD1_LAYOUT_PROBE_SCHEMA_VERSION 1 +#define XGD1_LAYOUT_PROBE_SECTOR_BYTES 2048U +#define XGD1_LAYOUT_PROBE_MAX_SAMPLES 20U + +typedef struct xbox_ref_probe_read_s +{ + int read_ok; + DWORD win32_error; + UCHAR scsi_status; + UCHAR sense_key; + UCHAR asc; + UCHAR ascq; + uint32_t nonzero_bytes; + int all_zero; + char sha1[41]; + unsigned char data[XGD1_LAYOUT_PROBE_SECTOR_BYTES]; +} xbox_ref_probe_read; + +typedef struct xbox_ref_probe_sample_s +{ + uint32_t lba; + const char *label; + xbox_ref_probe_read read10; + xbox_ref_probe_read read12; +} xbox_ref_probe_sample; + +typedef struct xbox_ref_probe_state_s +{ + const char *name; + uint32_t capacity; + int capacity_valid; + int state_verified; + uint32_t sample_count; + xbox_ref_probe_sample samples[XGD1_LAYOUT_PROBE_MAX_SAMPLES]; +} xbox_ref_probe_state; + +typedef struct xbox_ref_probe_point_s +{ + uint32_t lba; + const char *label; +} xbox_ref_probe_point; + +typedef struct xbox_ref_sptd_with_sense_s +{ + SCSI_PASS_THROUGH_DIRECT sptd; + ULONG filler; + UCHAR sense[32]; +} xbox_ref_sptd_with_sense; + +static const xbox_ref_probe_point xbox_ref_locked_probe_points[] = { + {0U, "locked_logical_start"}, + {31U, "locked_early_lba_31"}, + {32U, "locked_early_lba_32"}, + {XGD1_VIDEO_L0_SECTORS - 1U, "video_l0_last"}, + {XGD1_VIDEO_L0_SECTORS, "locked_video_l1_first"}, + {XGD1_VIDEO_TOTAL_SECTORS - 1U, "locked_video_last"}, + {XGD1_VIDEO_TOTAL_SECTORS, "locked_capacity_plus_one"}, + {XGD1_GAME_OUTPUT_START_LBA - 1U, "pregame_output_last"}, + {XGD1_GAME_OUTPUT_START_LBA, "game_output_start"}, + {XGD1_REDUMP_LAYER_BREAK_LBA - 1U, "output_layer_break_minus_one"}, + {XGD1_REDUMP_LAYER_BREAK_LBA, "output_layer_break"}, + {XGD1_VIDEO_L1_OUTPUT_START_LBA - 1U, "postgame_output_last"}, + {XGD1_VIDEO_L1_OUTPUT_START_LBA, "video_l1_output_start"}, + {XGD1_FULL_REDUMP_SECTORS - 1U, "full_output_last"} +}; + +static const xbox_ref_probe_point xbox_ref_unlocked_probe_points[] = { + {0U, "unlocked_source_start"}, + {XGD1_XISO_LEADIN_SECTORS - 1U, "unlocked_source_leadin_last"}, + {XGD1_GAME_SOURCE_START_LBA, "xdfs_source_start"}, + {XGD1_GAME_SOURCE_START_LBA + 1U, "xdfs_source_next"}, + {(XGD1_GAME_OUTPUT_START_LBA - XGD1_VIDEO_L0_SECTORS) - 1U, "synthetic_gap_length_minus_one"}, + {XGD1_GAME_OUTPUT_START_LBA - XGD1_VIDEO_L0_SECTORS, "synthetic_gap_length"}, + {XGD1_GAME_OUTPUT_START_LBA - 1U, "pregame_output_last_as_unlocked_source"}, + {XGD1_GAME_OUTPUT_START_LBA, "game_output_start_as_unlocked_source"}, + {(XGD1_REDUMP_LAYER_BREAK_LBA - XGD1_GAME_OUTPUT_START_LBA) - 1U, "mapped_layer_break_source_minus_one"}, + {XGD1_REDUMP_LAYER_BREAK_LBA - XGD1_GAME_OUTPUT_START_LBA, "mapped_layer_break_source"}, + {XGD1_GAME_SOURCE_START_LBA + XGD1_GAME_SOURCE_SECTORS - 1U, "unlocked_source_last"}, + {REDUMP_SECTORS, "unlocked_capacity_plus_one"}, + {XGD1_GAME_OUTPUT_START_LBA + REDUMP_SECTORS, "postgame_output_start_as_unlocked_source"}, + {XGD1_VIDEO_L1_OUTPUT_START_LBA, "video_l1_output_start_as_unlocked_source"} +}; + +static void xbox_ref_probe_parse_sense(const UCHAR *sense, + UCHAR *sense_key, + UCHAR *asc, + UCHAR *ascq) +{ + UCHAR response; + + if (sense_key) *sense_key = 0; + if (asc) *asc = 0; + if (ascq) *ascq = 0; + if (!sense) + return; + + response = (UCHAR)(sense[0] & 0x7f); + if (response == 0x70 || response == 0x71) + { + if (sense_key) *sense_key = (UCHAR)(sense[2] & 0x0f); + if (asc) *asc = sense[12]; + if (ascq) *ascq = sense[13]; + } + else if (response == 0x72 || response == 0x73) + { + if (sense_key) *sense_key = (UCHAR)(sense[1] & 0x0f); + if (asc) *asc = sense[2]; + if (ascq) *ascq = sense[3]; + } +} + +static void xbox_ref_probe_sha1(const unsigned char *data, + uint32_t data_size, + char out_sha1[41]) +{ + SHA1_CTX ctx; + unsigned char digest[20]; + uint32_t i; + + if (!out_sha1) + return; + out_sha1[0] = '\0'; + if (!data || data_size == 0) + return; + + SHA1_Init(&ctx); + SHA1_Update(&ctx, data, data_size); + SHA1_Final(digest, &ctx); + + for (i = 0; i < 20U; ++i) + sprintf(out_sha1 + (i * 2U), "%02x", digest[i]); + out_sha1[40] = '\0'; +} + +static void xbox_ref_probe_read_one(HANDLE hDevice, + UCHAR opcode, + uint32_t lba, + xbox_ref_probe_read *result) +{ + xbox_ref_sptd_with_sense packet; + unsigned char *buffer; + DWORD bytes_returned; + BOOL ioctl_ok; + uint32_t i; + + if (!result) + return; + memset(result, 0, sizeof(*result)); + + buffer = (unsigned char *)VirtualAlloc(NULL, + XGD1_LAYOUT_PROBE_SECTOR_BYTES, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE); + if (!buffer) + { + result->win32_error = ERROR_NOT_ENOUGH_MEMORY; + return; + } + + memset(buffer, 0, XGD1_LAYOUT_PROBE_SECTOR_BYTES); + memset(&packet, 0, sizeof(packet)); + + packet.sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT); + packet.sptd.CdbLength = (opcode == 0xA8) ? 12 : 10; + packet.sptd.SenseInfoLength = sizeof(packet.sense); + packet.sptd.DataIn = SCSI_IOCTL_DATA_IN; + packet.sptd.DataTransferLength = XGD1_LAYOUT_PROBE_SECTOR_BYTES; + packet.sptd.TimeOutValue = 10; + packet.sptd.DataBuffer = buffer; + packet.sptd.SenseInfoOffset = offsetof(xbox_ref_sptd_with_sense, sense); + packet.sptd.Cdb[0] = opcode; + packet.sptd.Cdb[2] = (UCHAR)((lba >> 24) & 0xff); + packet.sptd.Cdb[3] = (UCHAR)((lba >> 16) & 0xff); + packet.sptd.Cdb[4] = (UCHAR)((lba >> 8) & 0xff); + packet.sptd.Cdb[5] = (UCHAR)(lba & 0xff); + + if (opcode == 0xA8) + { + packet.sptd.Cdb[9] = 1; + } + else + { + packet.sptd.Cdb[8] = 1; + } + + bytes_returned = 0; + ioctl_ok = DeviceIoControl(hDevice, + IOCTL_SCSI_PASS_THROUGH_DIRECT, + &packet, + sizeof(packet), + &packet, + sizeof(packet), + &bytes_returned, + NULL); + + result->win32_error = ioctl_ok ? ERROR_SUCCESS : GetLastError(); + result->scsi_status = packet.sptd.ScsiStatus; + xbox_ref_probe_parse_sense(packet.sense, + &result->sense_key, + &result->asc, + &result->ascq); + result->read_ok = (ioctl_ok && packet.sptd.ScsiStatus == 0) ? 1 : 0; + + if (result->read_ok) + { + memcpy(result->data, buffer, XGD1_LAYOUT_PROBE_SECTOR_BYTES); + result->nonzero_bytes = 0; + for (i = 0; i < XGD1_LAYOUT_PROBE_SECTOR_BYTES; ++i) + { + if (buffer[i] != 0) + result->nonzero_bytes++; + } + result->all_zero = (result->nonzero_bytes == 0U) ? 1 : 0; + xbox_ref_probe_sha1(buffer, + XGD1_LAYOUT_PROBE_SECTOR_BYTES, + result->sha1); + } + + VirtualFree(buffer, 0, MEM_RELEASE); +} + +static void xbox_ref_probe_capture_state(HANDLE hDevice, + xbox_ref_probe_state *state, + const xbox_ref_probe_point *points, + uint32_t point_count) +{ + uint32_t i; + + if (!state || !points) + return; + + if (point_count > XGD1_LAYOUT_PROBE_MAX_SAMPLES) + point_count = XGD1_LAYOUT_PROBE_MAX_SAMPLES; + + state->sample_count = point_count; + for (i = 0; i < point_count; ++i) + { + state->samples[i].lba = points[i].lba; + state->samples[i].label = points[i].label; + + printf("[XGD1-PROBE] %s: probing LBA %u (%s) with READ(10) and READ(12).\n", + state->name ? state->name : "state", + points[i].lba, + points[i].label ? points[i].label : "unlabeled"); + + xbox_ref_probe_read_one(hDevice, + 0x28, + points[i].lba, + &state->samples[i].read10); + xbox_ref_probe_read_one(hDevice, + 0xA8, + points[i].lba, + &state->samples[i].read12); + } +} + +static void xbox_ref_probe_json_string(FILE *json, const char *value) +{ + const unsigned char *p; + + if (!json) + return; + if (!value) + value = ""; + + fputc('"', json); + p = (const unsigned char *)value; + while (*p) + { + switch (*p) + { + case '\\': fputs("\\\\", json); break; + case '"': fputs("\\\"", json); break; + case '\b': fputs("\\b", json); break; + case '\f': fputs("\\f", json); break; + case '\n': fputs("\\n", json); break; + case '\r': fputs("\\r", json); break; + case '\t': fputs("\\t", json); break; + default: + if (*p < 0x20) + fprintf(json, "\\u%04x", (unsigned int)*p); + else + fputc(*p, json); + break; + } + ++p; + } + fputc('"', json); +} + +static void xbox_ref_probe_json_hex(FILE *json, + const unsigned char *data, + uint32_t data_size) +{ + uint32_t i; + + if (!json) + return; + fputc('"', json); + if (data) + { + for (i = 0; i < data_size; ++i) + fprintf(json, "%02x", data[i]); + } + fputc('"', json); +} + +static void xbox_ref_probe_write_cdb(FILE *json, + UCHAR opcode, + uint32_t lba) +{ + if (!json) + return; + + if (opcode == 0xA8) + { + fprintf(json, + "\"a800%08x000000010000\"", + lba); + } + else + { + fprintf(json, + "\"2800%08x00000100\"", + lba); + } +} + +static void xbox_ref_probe_write_read(FILE *json, + const char *indent, + const char *name, + const xbox_ref_probe_read *read) +{ + if (!json || !read) + return; + + fprintf(json, "%s", indent); + xbox_ref_probe_json_string(json, name); + fprintf(json, ": {\n"); + fprintf(json, "%s \"read_ok\": %s,\n", indent, read->read_ok ? "true" : "false"); + fprintf(json, "%s \"win32_error\": %lu,\n", indent, (unsigned long)read->win32_error); + fprintf(json, "%s \"scsi_status\": %u,\n", indent, (unsigned int)read->scsi_status); + fprintf(json, "%s \"sense_key\": %u,\n", indent, (unsigned int)read->sense_key); + fprintf(json, "%s \"asc\": %u,\n", indent, (unsigned int)read->asc); + fprintf(json, "%s \"ascq\": %u,\n", indent, (unsigned int)read->ascq); + if (read->read_ok) + { + fprintf(json, "%s \"nonzero_bytes\": %u,\n", indent, read->nonzero_bytes); + fprintf(json, "%s \"all_zero\": %s,\n", indent, read->all_zero ? "true" : "false"); + } + else + { + fprintf(json, "%s \"nonzero_bytes\": null,\n", indent); + fprintf(json, "%s \"all_zero\": null,\n", indent); + } + fprintf(json, "%s \"sha1\": ", indent); + if (read->read_ok) + xbox_ref_probe_json_string(json, read->sha1); + else + fprintf(json, "null"); + fprintf(json, ",\n"); + fprintf(json, "%s \"data_hex\": ", indent); + if (read->read_ok) + xbox_ref_probe_json_hex(json, read->data, XGD1_LAYOUT_PROBE_SECTOR_BYTES); + else + xbox_ref_probe_json_string(json, ""); + fprintf(json, "\n%s}", indent); +} + +static void xbox_ref_probe_write_state(FILE *json, + const xbox_ref_probe_state *state, + int trailing_comma) +{ + uint32_t i; + + fprintf(json, " {\n"); + fprintf(json, " \"name\": "); + xbox_ref_probe_json_string(json, state->name); + fprintf(json, ",\n"); + fprintf(json, " \"capacity_valid\": %s,\n", state->capacity_valid ? "true" : "false"); + fprintf(json, " \"capacity_sectors\": %u,\n", state->capacity); + fprintf(json, " \"state_verified\": %s,\n", state->state_verified ? "true" : "false"); + fprintf(json, " \"samples\": [\n"); + + for (i = 0; i < state->sample_count; ++i) + { + const xbox_ref_probe_sample *sample = &state->samples[i]; + int identical = 0; + + if (sample->read10.read_ok && sample->read12.read_ok) + { + identical = (memcmp(sample->read10.data, + sample->read12.data, + XGD1_LAYOUT_PROBE_SECTOR_BYTES) == 0) ? 1 : 0; + } + + fprintf(json, " {\n"); + fprintf(json, " \"lba\": %u,\n", sample->lba); + fprintf(json, " \"label\": "); + xbox_ref_probe_json_string(json, sample->label); + fprintf(json, ",\n"); + fprintf(json, " \"read10_cdb_hex\": "); + xbox_ref_probe_write_cdb(json, 0x28, sample->lba); + fprintf(json, ",\n"); + fprintf(json, " \"read12_cdb_hex\": "); + xbox_ref_probe_write_cdb(json, 0xA8, sample->lba); + fprintf(json, ",\n"); + xbox_ref_probe_write_read(json, " ", "read10", &sample->read10); + fprintf(json, ",\n"); + xbox_ref_probe_write_read(json, " ", "read12", &sample->read12); + fprintf(json, ",\n"); + fprintf(json, + " \"read10_read12_comparable\": %s,\n", + (sample->read10.read_ok && sample->read12.read_ok) ? "true" : "false"); + fprintf(json, " \"read10_read12_identical\": "); + if (sample->read10.read_ok && sample->read12.read_ok) + fprintf(json, "%s\n", identical ? "true" : "false"); + else + fprintf(json, "null\n"); + fprintf(json, " }%s\n", (i + 1U < state->sample_count) ? "," : ""); + } + + fprintf(json, " ]\n"); + fprintf(json, " }%s\n", trailing_comma ? "," : ""); +} + +static BOOL xbox_ref_probe_write_report(const char *report_path, + const char *device, + uint32_t entry_capacity, + int entry_game_view, + int volume_lock_acquired, + const xbox_ref_probe_state *locked_state, + const xbox_ref_probe_state *unlocked_state, + int restore_attempted, + uint32_t restored_capacity, + int restore_verified) +{ + char tmp_path[XBOX_REF_RESULT_PATH_MAX]; + const char *final_path; + FILE *json; + int fd; + BOOL moved; + + final_path = (report_path && report_path[0]) ? report_path : "xgd1_layout_probe.json"; + { + int path_length = snprintf(tmp_path, + sizeof(tmp_path), + "%s.tmp.%lu", + final_path, + (unsigned long)GetCurrentProcessId()); + if (path_length < 0 || (size_t)path_length >= sizeof(tmp_path)) + { + printf("[XGD1-PROBE] Fatal: report path is too long.\n"); + return FALSE; + } + } + + json = fopen(tmp_path, "wb"); + if (!json) + { + printf("[XGD1-PROBE] Fatal: could not create temporary report %s.\n", tmp_path); + return FALSE; + } + + fprintf(json, "{\n"); + fprintf(json, " \"schema_version\": %d,\n", XGD1_LAYOUT_PROBE_SCHEMA_VERSION); + fprintf(json, " \"producer\": \"friidump-0.5.3.11\",\n"); + fprintf(json, " \"probe\": \"original_xbox_xgd1_logical_layout_boundary\",\n"); + fprintf(json, " \"device\": "); + xbox_ref_probe_json_string(json, device ? device : ""); + fprintf(json, ",\n"); + fprintf(json, " \"safety\": {\n"); + fprintf(json, " \"read_only_sector_commands\": true,\n"); + fprintf(json, " \"authentication_handshake_used\": true,\n"); + fprintf(json, " \"tray_cycle_used\": true,\n"); + fprintf(json, " \"firmware_write_used\": false,\n"); + fprintf(json, " \"flash_command_used\": false\n"); + fprintf(json, " },\n"); + fprintf(json, " \"constants\": {\n"); + fprintf(json, " \"full_output_sectors\": %u,\n", XGD1_FULL_REDUMP_SECTORS); + fprintf(json, " \"game_output_start_lba\": %u,\n", XGD1_GAME_OUTPUT_START_LBA); + fprintf(json, " \"game_view_sectors\": %u,\n", REDUMP_SECTORS); + fprintf(json, " \"video_total_sectors\": %u,\n", XGD1_VIDEO_TOTAL_SECTORS); + fprintf(json, " \"video_l0_sectors\": %u,\n", XGD1_VIDEO_L0_SECTORS); + fprintf(json, " \"video_l1_sectors\": %u,\n", XGD1_VIDEO_L1_SECTORS); + fprintf(json, " \"video_l1_output_start_lba\": %u,\n", XGD1_VIDEO_L1_OUTPUT_START_LBA); + fprintf(json, " \"output_layer_break_lba\": %u\n", XGD1_REDUMP_LAYER_BREAK_LBA); + fprintf(json, " },\n"); + fprintf(json, " \"entry\": {\n"); + fprintf(json, " \"capacity_sectors\": %u,\n", entry_capacity); + fprintf(json, " \"game_view\": %s,\n", entry_game_view ? "true" : "false"); + fprintf(json, " \"classification\": "); + if (xbox_ref_capacity_is_xgd1_game_view(entry_capacity)) + xbox_ref_probe_json_string(json, "unlocked_game"); + else if (xbox_ref_capacity_is_xgd1_locked_view(entry_capacity)) + xbox_ref_probe_json_string(json, "locked_video"); + else + xbox_ref_probe_json_string(json, "other_or_unknown"); + fprintf(json, "\n"); + fprintf(json, " },\n"); + fprintf(json, " \"volume_lock_acquired_at_least_once\": %s,\n", volume_lock_acquired ? "true" : "false"); + fprintf(json, + " \"required_states_verified\": %s,\n", + (locked_state->state_verified && unlocked_state->state_verified) ? "true" : "false"); + fprintf(json, " \"states\": [\n"); + xbox_ref_probe_write_state(json, locked_state, 1); + xbox_ref_probe_write_state(json, unlocked_state, 0); + fprintf(json, " ],\n"); + fprintf(json, " \"restoration\": {\n"); + fprintf(json, " \"attempted\": %s,\n", restore_attempted ? "true" : "false"); + fprintf(json, " \"capacity_sectors\": %u,\n", restored_capacity); + fprintf(json, " \"verified\": %s\n", restore_verified ? "true" : "false"); + fprintf(json, " },\n"); + fprintf(json, " \"interpretation_boundary\": {\n"); + fprintf(json, " \"pregame_padding_resolved\": false,\n"); + fprintf(json, " \"postgame_padding_resolved\": false,\n"); + fprintf(json, " \"game_leadin_resolved\": false,\n"); + fprintf(json, " \"note\": \"This report records ordinary logical READ(10)/READ(12) behavior in locked/video and unlocked/game states. It does not claim that inaccessible physical filler sectors are zero or that the current reconstructed image is Redump-exact.\"\n"); + fprintf(json, " }\n"); + fprintf(json, "}\n"); + + if (fflush(json) != 0) + { + fclose(json); + DeleteFileA(tmp_path); + return FALSE; + } + + fd = _fileno(json); + if (fd < 0 || _commit(fd) != 0) + { + fclose(json); + DeleteFileA(tmp_path); + return FALSE; + } + + if (fclose(json) != 0) + { + DeleteFileA(tmp_path); + return FALSE; + } + + moved = MoveFileExA(tmp_path, + final_path, + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH); + if (!moved) + { + printf("[XGD1-PROBE] Fatal: could not atomically publish report %s (error %lu).\n", + final_path, + (unsigned long)GetLastError()); + DeleteFileA(tmp_path); + return FALSE; + } + + printf("[XGD1-PROBE] Atomic report written: %s\n", final_path); + return TRUE; +} + +static int xbox_ref_xgd1_layout_probe_core(HANDLE hDevice, + const char *device, + const char *report_path, + BOOL owns_handle) +{ + xbox_ref_probe_state locked_state; + xbox_ref_probe_state unlocked_state; + uint32_t entry_capacity; + uint32_t restored_capacity; + int entry_game_view; + int volume_lock_acquired; + int volume_lock_ever; + int restore_attempted; + int restore_verified; + int states_verified; + DWORD bytes_returned; + BOOL report_ok; + + memset(&locked_state, 0, sizeof(locked_state)); + memset(&unlocked_state, 0, sizeof(unlocked_state)); + locked_state.name = "locked_video"; + unlocked_state.name = "unlocked_game"; + entry_capacity = 0; + restored_capacity = 0; + entry_game_view = 0; + volume_lock_acquired = 0; + volume_lock_ever = 0; + restore_attempted = 0; + restore_verified = 0; + states_verified = 0; + bytes_returned = 0; + + xbox_ref_log_open_for_target(report_path, xbox_ref_drive_letter_from_device(device)); + printf("[XGD1-PROBE] Starting read-only logical layout boundary probe.\n"); + printf("[XGD1-PROBE] The existing XGD1 output layout will not be modified.\n"); + + if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) + { + printf("[XGD1-PROBE] Fatal: invalid drive handle.\n"); + if (owns_handle) + xbox_ref_log_close(); + return 1; + } + + if (!EnsureDriveReady(hDevice, 30000)) + { + printf("[XGD1-PROBE] Fatal: media did not become ready.\n"); + if (owns_handle) + xbox_ref_log_close(); + return 1; + } + + entry_capacity = GetTotalSectors(hDevice); + entry_game_view = xbox_ref_capacity_is_xgd1_game_view(entry_capacity) ? 1 : 0; + printf("[XGD1-PROBE] Entry READ CAPACITY: %u sectors (%s).\n", + entry_capacity, + entry_game_view ? "game view" : "locked/video or unknown view"); + + DeviceIoControl(hDevice, + FSCTL_UNLOCK_VOLUME, + NULL, + 0, + NULL, + 0, + &bytes_returned, + NULL); + + printf("[XGD1-PROBE] Cycling tray to establish locked/video view.\n"); + AutomateTrayCycle(hDevice); + locked_state.capacity = xbox_ref_refresh_ready_capacity(hDevice); + locked_state.capacity_valid = locked_state.capacity != 0U; + locked_state.state_verified = (locked_state.capacity_valid && + xbox_ref_capacity_is_xgd1_locked_view(locked_state.capacity)) ? 1 : 0; + printf("[XGD1-PROBE] Locked/video READ CAPACITY: %u sectors; verified=%s.\n", + locked_state.capacity, + locked_state.state_verified ? "yes" : "no"); + + if (DeviceIoControl(hDevice, + FSCTL_LOCK_VOLUME, + NULL, + 0, + NULL, + 0, + &bytes_returned, + NULL)) + { + volume_lock_acquired = 1; + volume_lock_ever = 1; + printf("[XGD1-PROBE] Windows volume lock acquired.\n"); + } + else + { + printf("[XGD1-PROBE][WARN] Windows volume lock was not acquired; close AutoPlay and Explorer dialogs if results are unstable.\n"); + } + + SetDriveSpeedMax(hDevice); + xbox_ref_probe_capture_state(hDevice, + &locked_state, + xbox_ref_locked_probe_points, + (uint32_t)(sizeof(xbox_ref_locked_probe_points) / + sizeof(xbox_ref_locked_probe_points[0]))); + + printf("[XGD1-PROBE] Applying the full Xbox handshake to establish game view.\n"); + UnlockDrive(hDevice); + unlocked_state.capacity = xbox_ref_refresh_ready_capacity(hDevice); + unlocked_state.capacity_valid = unlocked_state.capacity != 0U; + unlocked_state.state_verified = (unlocked_state.capacity_valid && + xbox_ref_capacity_is_xgd1_game_view(unlocked_state.capacity)) ? 1 : 0; + printf("[XGD1-PROBE] Unlocked/game READ CAPACITY: %u sectors; verified=%s.\n", + unlocked_state.capacity, + unlocked_state.state_verified ? "yes" : "no"); + + xbox_ref_probe_capture_state(hDevice, + &unlocked_state, + xbox_ref_unlocked_probe_points, + (uint32_t)(sizeof(xbox_ref_unlocked_probe_points) / + sizeof(xbox_ref_unlocked_probe_points[0]))); + + if (volume_lock_acquired) + { + DeviceIoControl(hDevice, + FSCTL_UNLOCK_VOLUME, + NULL, + 0, + NULL, + 0, + &bytes_returned, + NULL); + volume_lock_acquired = 0; + } + + if (!entry_game_view && entry_capacity != 0U) + { + restore_attempted = 1; + printf("[XGD1-PROBE] Restoring the entry locked/video state with a final tray cycle.\n"); + AutomateTrayCycle(hDevice); + restored_capacity = xbox_ref_refresh_ready_capacity(hDevice); + restore_verified = (restored_capacity != 0U && + xbox_ref_capacity_is_xgd1_locked_view(restored_capacity)) ? 1 : 0; + } + else + { + restored_capacity = GetTotalSectors(hDevice); + restore_verified = (entry_game_view && + xbox_ref_capacity_is_xgd1_game_view(restored_capacity)) ? 1 : 0; + } + + states_verified = (locked_state.state_verified && + unlocked_state.state_verified) ? 1 : 0; + + report_ok = xbox_ref_probe_write_report(report_path, + device, + entry_capacity, + entry_game_view, + volume_lock_ever, + &locked_state, + &unlocked_state, + restore_attempted, + restored_capacity, + restore_verified); + + if (owns_handle) + { + printf("[XGD1-PROBE] Issuing STOP UNIT / spin-down after probe... "); + printf("%s\n", StopDriveUnit(hDevice) ? "OK" : "FAILED"); + CloseDrive(hDevice); + xbox_ref_log_close(); + } + + if (!report_ok) + return 1; + if (!states_verified) + { + printf("[XGD1-PROBE] Probe report is partial because one or more drive states were not verified.\n"); + return 1; + } + + printf("[XGD1-PROBE] Probe complete. Synthetic ranges remain unresolved until the captured evidence supports a source mapping.\n"); + return 0; +} + + static int xbox_ref_gdr8050l_dump_core(HANDLE hDevice, const char *device, const char *filename, char mode, BOOL ownsHandle, xbox_ref_dump_result *result) { char driveLetter; @@ -241,6 +995,48 @@ cleanup: return ok ? 0 : 1; } +int xbox_ref_xgd1_layout_probe_with_handle(void *native_handle, + const char *device, + const char *report_path) +{ + return xbox_ref_xgd1_layout_probe_core((HANDLE)native_handle, + device, + report_path, + FALSE); +} + +int xbox_ref_xgd1_layout_probe(const char *device, + const char *report_path) +{ + char drive_letter; + HANDLE hDevice; + + drive_letter = xbox_ref_drive_letter_from_device(device); + if (!drive_letter) + { + xbox_ref_log_open_for_target(report_path, 0); + printf("[XGD1-PROBE] Fatal: could not parse drive letter from device '%s'.\n", + device ? device : "(null)"); + xbox_ref_log_close(); + return 1; + } + + hDevice = OpenDrive(drive_letter); + if (hDevice == INVALID_HANDLE_VALUE) + { + xbox_ref_log_open_for_target(report_path, drive_letter); + printf("[XGD1-PROBE] Fatal: cannot open drive %c:. Run as Administrator.\n", + drive_letter); + xbox_ref_log_close(); + return 1; + } + + return xbox_ref_xgd1_layout_probe_core(hDevice, + device, + report_path, + TRUE); +} + int xbox_ref_gdr8050l_dump_with_handle(void *native_handle, const char *device, const char *filename, char mode, xbox_ref_dump_result *result) { return xbox_ref_gdr8050l_dump_core((HANDLE)native_handle, device, filename, mode, FALSE, result); @@ -270,6 +1066,24 @@ int xbox_ref_gdr8050l_dump(const char *device, const char *filename, char mode, return xbox_ref_gdr8050l_dump_core(hDevice, device, filename, mode, TRUE, result); } #else +int xbox_ref_xgd1_layout_probe_with_handle(void *native_handle, + const char *device, + const char *report_path) +{ + (void)native_handle; + (void)device; + (void)report_path; + return 1; +} + +int xbox_ref_xgd1_layout_probe(const char *device, + const char *report_path) +{ + (void)device; + (void)report_path; + 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) { (void)native_handle; (void)device; (void)filename; (void)mode; xbox_ref_dump_result_init(result); return 1; diff --git a/libfriidump/xbox_ref_bridge.h b/libfriidump/xbox_ref_bridge.h index 8d0c278..6593856 100644 --- a/libfriidump/xbox_ref_bridge.h +++ b/libfriidump/xbox_ref_bridge.h @@ -38,6 +38,16 @@ int xbox_ref_gdr8050l_dump(const char *device, const char *filename, char mode, xbox_ref_dump_result *result); +int xbox_ref_xgd1_layout_probe_with_handle(void *native_handle, + const char *device, + const char *report_path); +int xbox_ref_xgd1_layout_probe(const char *device, + const char *report_path); +int xbox_ref_xgd1_raw_id_probe_with_handle(void *native_handle, + const char *device, + const char *report_path); +int xbox_ref_xgd1_raw_id_probe(const char *device, + const char *report_path); #ifdef __cplusplus } diff --git a/libfriidump/xbox_xgd1_raw_id_probe.c b/libfriidump/xbox_xgd1_raw_id_probe.c new file mode 100644 index 0000000..df21a56 --- /dev/null +++ b/libfriidump/xbox_xgd1_raw_id_probe.c @@ -0,0 +1,1112 @@ +#include "xbox_ref_bridge.h" + +#include + +#ifdef WIN32 +#define WIN32_LEAN_AND_MEAN +#define _WIN32_WINNT 0x0500 +#include +#include +#include +#include +#include +#include + +#ifndef SCSI_IOCTL_DATA_UNSPECIFIED +#define SCSI_IOCTL_DATA_UNSPECIFIED 2 +#endif + +#include "xbox_ref/utils.h" +#include "xbox_ref/sha1.h" +#include "xbox_ref/unlock.h" +#include "xbox_ref/xbox_ref_log.h" + +#define printf xbox_ref_printf + +#define XGD1_RAW_ID_PROBE_SCHEMA_VERSION 2 +#define XGD1_RAW_ID_CACHE_BASE 0x80000000U +#define XGD1_RAW_SECTOR_BYTES 2064U +#define XGD1_LOGICAL_SECTOR_BYTES 2048U +#define XGD1_CACHE_FILL_SECTORS 16U +#define XGD1_RAW_ID_MAX_SAMPLES 16U + +typedef struct xgd1_sptd_with_sense_s +{ + SCSI_PASS_THROUGH_DIRECT sptd; + ULONG filler; + UCHAR sense[32]; +} xgd1_sptd_with_sense; + +typedef struct xgd1_cmd_result_s +{ + int ok; + DWORD win32_error; + UCHAR scsi_status; + UCHAR sense_key; + UCHAR asc; + UCHAR ascq; +} xgd1_cmd_result; + +typedef struct xgd1_raw_id_sample_s +{ + uint32_t lba; + const char *label; + uint32_t cache_fill_lba; + uint32_t cache_index; + uint32_t cache_address; + xgd1_cmd_result cache_flush; + xgd1_cmd_result cache_fill; + xgd1_cmd_result memdump_header; + xgd1_cmd_result memdump_edc; + int raw_header_valid; + UCHAR sector_information; + uint32_t layer_number; + uint32_t physical_sector_number; + uint32_t normalized_physical_sector_number; + uint32_t output_lba; + uint32_t expected_layer_number; + uint32_t expected_physical_sector_number; + uint32_t expected_normalized_physical_sector_number; + int layer_matches_expected; + int psn_matches_expected; + int geometry_matches_expected; + char logical_sha1[41]; + unsigned char logical[XGD1_LOGICAL_SECTOR_BYTES]; + unsigned char raw_header[12]; + unsigned char raw_edc[4]; +} xgd1_raw_id_sample; + +typedef struct xgd1_raw_id_state_s +{ + const char *name; + int locked_view; + uint32_t capacity; + int capacity_valid; + int state_verified; + uint32_t sample_count; + uint32_t geometry_validation_failures; + int geometry_verified; + xgd1_raw_id_sample samples[XGD1_RAW_ID_MAX_SAMPLES]; +} xgd1_raw_id_state; + +typedef struct xgd1_probe_point_s +{ + uint32_t lba; + const char *label; +} xgd1_probe_point; + +static const xgd1_probe_point xgd1_locked_points[] = { + {0U, "locked_logical_start"}, + {31U, "locked_early_lba_31"}, + {32U, "locked_early_lba_32"}, + {XGD1_VIDEO_L0_SECTORS - 1U, "video_l0_last"}, + {XGD1_VIDEO_L0_SECTORS, "video_l1_first"}, + {XGD1_VIDEO_TOTAL_SECTORS - 1U, "locked_video_last"} +}; + +static const xgd1_probe_point xgd1_unlocked_points[] = { + {0U, "unlocked_source_start"}, + {XGD1_XISO_LEADIN_SECTORS - 1U, "unlocked_leadin_last"}, + {XGD1_GAME_SOURCE_START_LBA, "xdfs_source_start"}, + {XGD1_GAME_SOURCE_START_LBA + 1U, "xdfs_source_next"}, + {(XGD1_REDUMP_LAYER_BREAK_LBA - XGD1_GAME_OUTPUT_START_LBA) - 1U, "mapped_layer_break_source_minus_one"}, + {XGD1_REDUMP_LAYER_BREAK_LBA - XGD1_GAME_OUTPUT_START_LBA, "mapped_layer_break_source"}, + {REDUMP_SECTORS - 1U, "unlocked_source_last"} +}; + +static char xgd1_drive_letter_from_device(const char *device) +{ + if (!device || !device[0]) return 0; + if (device[0] && device[1] == ':') return device[0]; + if (device[0] == '\\' && device[1] == '\\' && + device[2] == '.' && device[3] == '\\' && + device[4] && device[5] == ':') + return device[4]; + return device[0]; +} + +static int xgd1_capacity_is_locked(uint32_t sectors) +{ + return sectors == XGD1_VIDEO_TOTAL_SECTORS; +} + +static int xgd1_capacity_is_game(uint32_t sectors) +{ + return sectors == REDUMP_SECTORS; +} + +static uint32_t xgd1_refresh_ready_capacity(HANDLE hDevice) +{ + RefreshVolume(hDevice); + Sleep(2000); + EnsureDriveReady(hDevice, 30000); + return GetTotalSectors(hDevice); +} + +static void xgd1_parse_sense(const UCHAR *sense, + UCHAR *sense_key, + UCHAR *asc, + UCHAR *ascq) +{ + UCHAR response; + + if (sense_key) *sense_key = 0; + if (asc) *asc = 0; + if (ascq) *ascq = 0; + if (!sense) + return; + + response = (UCHAR)(sense[0] & 0x7f); + if (response == 0x70 || response == 0x71) + { + if (sense_key) *sense_key = (UCHAR)(sense[2] & 0x0f); + if (asc) *asc = sense[12]; + if (ascq) *ascq = sense[13]; + } + else if (response == 0x72 || response == 0x73) + { + if (sense_key) *sense_key = (UCHAR)(sense[1] & 0x0f); + if (asc) *asc = sense[2]; + if (ascq) *ascq = sense[3]; + } +} + +static void xgd1_sha1(const unsigned char *data, + uint32_t data_size, + char out_sha1[41]) +{ + SHA1_CTX ctx; + unsigned char digest[20]; + uint32_t i; + + if (!out_sha1) + return; + out_sha1[0] = '\0'; + if (!data || data_size == 0) + return; + + SHA1_Init(&ctx); + SHA1_Update(&ctx, data, data_size); + SHA1_Final(digest, &ctx); + + for (i = 0; i < 20U; ++i) + sprintf(out_sha1 + (i * 2U), "%02x", digest[i]); + out_sha1[40] = '\0'; +} + +static void xgd1_cmd_result_init(xgd1_cmd_result *result) +{ + if (result) + memset(result, 0, sizeof(*result)); +} + +static void xgd1_execute_data_in(HANDLE hDevice, + const UCHAR *cdb, + UCHAR cdb_length, + unsigned char *buffer, + DWORD buffer_length, + DWORD timeout_seconds, + xgd1_cmd_result *result) +{ + xgd1_sptd_with_sense packet; + DWORD bytes_returned; + BOOL ioctl_ok; + + xgd1_cmd_result_init(result); + if (!result || !cdb || !buffer || buffer_length == 0U) + return; + + memset(buffer, 0, buffer_length); + memset(&packet, 0, sizeof(packet)); + + packet.sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT); + packet.sptd.CdbLength = cdb_length; + packet.sptd.SenseInfoLength = sizeof(packet.sense); + packet.sptd.DataIn = SCSI_IOCTL_DATA_IN; + packet.sptd.DataTransferLength = buffer_length; + packet.sptd.TimeOutValue = timeout_seconds; + packet.sptd.DataBuffer = buffer; + packet.sptd.SenseInfoOffset = offsetof(xgd1_sptd_with_sense, sense); + memcpy(packet.sptd.Cdb, cdb, cdb_length); + + bytes_returned = 0; + ioctl_ok = DeviceIoControl(hDevice, + IOCTL_SCSI_PASS_THROUGH_DIRECT, + &packet, + sizeof(packet), + &packet, + sizeof(packet), + &bytes_returned, + NULL); + + result->win32_error = ioctl_ok ? ERROR_SUCCESS : GetLastError(); + result->scsi_status = packet.sptd.ScsiStatus; + xgd1_parse_sense(packet.sense, + &result->sense_key, + &result->asc, + &result->ascq); + result->ok = (ioctl_ok && packet.sptd.ScsiStatus == 0) ? 1 : 0; +} + +static void xgd1_execute_no_data(HANDLE hDevice, + const UCHAR *cdb, + UCHAR cdb_length, + DWORD timeout_seconds, + xgd1_cmd_result *result) +{ + xgd1_sptd_with_sense packet; + DWORD bytes_returned; + BOOL ioctl_ok; + + xgd1_cmd_result_init(result); + if (!result || !cdb) + return; + + memset(&packet, 0, sizeof(packet)); + packet.sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT); + packet.sptd.CdbLength = cdb_length; + packet.sptd.SenseInfoLength = sizeof(packet.sense); + packet.sptd.DataIn = SCSI_IOCTL_DATA_UNSPECIFIED; + packet.sptd.DataTransferLength = 0; + packet.sptd.TimeOutValue = timeout_seconds; + packet.sptd.DataBuffer = NULL; + packet.sptd.SenseInfoOffset = offsetof(xgd1_sptd_with_sense, sense); + memcpy(packet.sptd.Cdb, cdb, cdb_length); + + bytes_returned = 0; + ioctl_ok = DeviceIoControl(hDevice, + IOCTL_SCSI_PASS_THROUGH_DIRECT, + &packet, + sizeof(packet), + &packet, + sizeof(packet), + &bytes_returned, + NULL); + + result->win32_error = ioctl_ok ? ERROR_SUCCESS : GetLastError(); + result->scsi_status = packet.sptd.ScsiStatus; + xgd1_parse_sense(packet.sense, + &result->sense_key, + &result->asc, + &result->ascq); + result->ok = (ioctl_ok && packet.sptd.ScsiStatus == 0) ? 1 : 0; +} + +static void xgd1_set_lba(UCHAR *cdb, uint32_t lba) +{ + cdb[2] = (UCHAR)((lba >> 24) & 0xff); + cdb[3] = (UCHAR)((lba >> 16) & 0xff); + cdb[4] = (UCHAR)((lba >> 8) & 0xff); + cdb[5] = (UCHAR)(lba & 0xff); +} + +static void xgd1_build_memdump_cdb(UCHAR cdb[12], + uint32_t address, + uint32_t length) +{ + memset(cdb, 0, 12); + cdb[0] = 0xE7; + cdb[1] = 0x48; + cdb[2] = 0x49; + cdb[3] = 0x54; + cdb[4] = 0x01; + cdb[6] = (UCHAR)((address >> 24) & 0xff); + cdb[7] = (UCHAR)((address >> 16) & 0xff); + cdb[8] = (UCHAR)((address >> 8) & 0xff); + cdb[9] = (UCHAR)(address & 0xff); + cdb[10] = (UCHAR)((length >> 8) & 0xff); + cdb[11] = (UCHAR)(length & 0xff); +} + +static void xgd1_set_expected_geometry(int locked_view, + xgd1_raw_id_sample *sample) +{ + uint32_t output_lba; + uint32_t layer_zero_last_psn; + uint32_t normalized_psn; + uint32_t expected_normalized_psn; + uint32_t expected_layer; + uint32_t expected_raw_psn; + + if (!sample) + return; + + if (locked_view) + { + if (sample->lba < XGD1_VIDEO_L0_SECTORS) + output_lba = sample->lba; + else + output_lba = XGD1_VIDEO_L1_OUTPUT_START_LBA + + (sample->lba - XGD1_VIDEO_L0_SECTORS); + } + else + { + output_lba = XGD1_GAME_OUTPUT_START_LBA + sample->lba; + } + + layer_zero_last_psn = 0x30000U + XGD1_REDUMP_LAYER_BREAK_LBA - 1U; + if (output_lba < XGD1_REDUMP_LAYER_BREAK_LBA) + { + expected_layer = 0U; + expected_normalized_psn = 0x30000U + output_lba; + expected_raw_psn = expected_normalized_psn; + } + else + { + expected_layer = 1U; + expected_normalized_psn = layer_zero_last_psn - + (output_lba - XGD1_REDUMP_LAYER_BREAK_LBA); + expected_raw_psn = (~expected_normalized_psn) & 0x00ffffffU; + } + + normalized_psn = (sample->layer_number != 0U) + ? ((~sample->physical_sector_number) & 0x00ffffffU) + : sample->physical_sector_number; + + sample->output_lba = output_lba; + sample->normalized_physical_sector_number = normalized_psn; + sample->expected_layer_number = expected_layer; + sample->expected_physical_sector_number = expected_raw_psn; + sample->expected_normalized_physical_sector_number = expected_normalized_psn; + sample->layer_matches_expected = + (sample->layer_number == expected_layer) ? 1 : 0; + sample->psn_matches_expected = + (sample->physical_sector_number == expected_raw_psn) ? 1 : 0; + sample->geometry_matches_expected = + (sample->layer_matches_expected && sample->psn_matches_expected) ? 1 : 0; +} + + +static void xgd1_capture_sample(HANDLE hDevice, + uint32_t capacity, + int locked_view, + const xgd1_probe_point *point, + xgd1_raw_id_sample *sample) +{ + unsigned char *read_buffer; + UCHAR flush_cdb[12]; + UCHAR read_cdb[12]; + UCHAR e7_cdb[12]; + uint32_t fill_lba; + uint32_t index; + uint32_t address; + + if (!sample || !point) + return; + + memset(sample, 0, sizeof(*sample)); + sample->lba = point->lba; + sample->label = point->label; + + if (capacity < XGD1_CACHE_FILL_SECTORS || point->lba >= capacity) + { + sample->cache_flush.win32_error = ERROR_INVALID_PARAMETER; + return; + } + + fill_lba = point->lba & ~(XGD1_CACHE_FILL_SECTORS - 1U); + if (fill_lba > capacity - XGD1_CACHE_FILL_SECTORS) + fill_lba = capacity - XGD1_CACHE_FILL_SECTORS; + index = point->lba - fill_lba; + address = XGD1_RAW_ID_CACHE_BASE + index * XGD1_RAW_SECTOR_BYTES; + + sample->cache_fill_lba = fill_lba; + sample->cache_index = index; + sample->cache_address = address; + + memset(flush_cdb, 0, sizeof(flush_cdb)); + flush_cdb[0] = 0xA8; + flush_cdb[1] = 0x08; + xgd1_set_lba(flush_cdb, fill_lba); + xgd1_execute_no_data(hDevice, + flush_cdb, + 12, + 15, + &sample->cache_flush); + if (!sample->cache_flush.ok) + return; + + read_buffer = (unsigned char *)VirtualAlloc( + NULL, + XGD1_CACHE_FILL_SECTORS * XGD1_LOGICAL_SECTOR_BYTES, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE); + if (!read_buffer) + { + sample->cache_fill.win32_error = ERROR_NOT_ENOUGH_MEMORY; + return; + } + + memset(read_cdb, 0, sizeof(read_cdb)); + read_cdb[0] = 0xA8; + xgd1_set_lba(read_cdb, fill_lba); + read_cdb[9] = (UCHAR)XGD1_CACHE_FILL_SECTORS; + read_cdb[10] = 0x80; + + xgd1_execute_data_in(hDevice, + read_cdb, + 12, + read_buffer, + XGD1_CACHE_FILL_SECTORS * XGD1_LOGICAL_SECTOR_BYTES, + 15, + &sample->cache_fill); + + if (sample->cache_fill.ok) + { + memcpy(sample->logical, + read_buffer + index * XGD1_LOGICAL_SECTOR_BYTES, + XGD1_LOGICAL_SECTOR_BYTES); + xgd1_sha1(sample->logical, + XGD1_LOGICAL_SECTOR_BYTES, + sample->logical_sha1); + + xgd1_build_memdump_cdb(e7_cdb, address, 12U); + xgd1_execute_data_in(hDevice, + e7_cdb, + 12, + sample->raw_header, + 12U, + 15, + &sample->memdump_header); + + xgd1_build_memdump_cdb(e7_cdb, address + 2060U, 4U); + xgd1_execute_data_in(hDevice, + e7_cdb, + 12, + sample->raw_edc, + 4U, + 15, + &sample->memdump_edc); + + if (sample->memdump_header.ok && sample->memdump_edc.ok) + { + sample->raw_header_valid = 1; + sample->sector_information = sample->raw_header[0]; + sample->layer_number = (uint32_t)(sample->sector_information & 1U); + sample->physical_sector_number = + ((uint32_t)sample->raw_header[1] << 16) | + ((uint32_t)sample->raw_header[2] << 8) | + (uint32_t)sample->raw_header[3]; + xgd1_set_expected_geometry(locked_view, sample); + } + } + + VirtualFree(read_buffer, 0, MEM_RELEASE); +} + +static void xgd1_capture_state(HANDLE hDevice, + xgd1_raw_id_state *state, + const xgd1_probe_point *points, + uint32_t point_count) +{ + uint32_t i; + + if (!state || !points || !state->state_verified) + return; + if (point_count > XGD1_RAW_ID_MAX_SAMPLES) + point_count = XGD1_RAW_ID_MAX_SAMPLES; + + state->sample_count = point_count; + state->geometry_validation_failures = 0U; + for (i = 0; i < point_count; ++i) + { + printf("[XGD1-RAW-ID] %s: cache-flush, aligned READ(12), and HIT 0xE7 header/EDC capture for LBA %u (%s).\n", + state->name ? state->name : "state", + points[i].lba, + points[i].label ? points[i].label : "unlabeled"); + xgd1_capture_sample(hDevice, + state->capacity, + state->locked_view, + &points[i], + &state->samples[i]); + if (!state->samples[i].cache_flush.ok || + !state->samples[i].cache_fill.ok || + !state->samples[i].raw_header_valid || + !state->samples[i].geometry_matches_expected) + state->geometry_validation_failures++; + } + + state->geometry_verified = + (state->state_verified && + state->sample_count > 0U && + state->geometry_validation_failures == 0U) ? 1 : 0; +} + +static void xgd1_json_string(FILE *f, const char *s) +{ + const unsigned char *p; + + fputc('"', f); + if (s) + { + for (p = (const unsigned char *)s; *p; ++p) + { + 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 xgd1_json_hex(FILE *f, + const unsigned char *data, + uint32_t size) +{ + uint32_t i; + + fputc('"', f); + for (i = 0; i < size; ++i) + fprintf(f, "%02x", data[i]); + fputc('"', f); +} + +static void xgd1_write_cmd_result(FILE *f, + const xgd1_cmd_result *r) +{ + fprintf(f, "{\"ok\":%s,\"win32_error\":%lu,\"scsi_status\":%u," + "\"sense_key\":%u,\"asc\":%u,\"ascq\":%u}", + (r && r->ok) ? "true" : "false", + (unsigned long)(r ? r->win32_error : 0), + (unsigned int)(r ? r->scsi_status : 0), + (unsigned int)(r ? r->sense_key : 0), + (unsigned int)(r ? r->asc : 0), + (unsigned int)(r ? r->ascq : 0)); +} + +static void xgd1_write_sample(FILE *f, + const xgd1_raw_id_sample *s, + int comma) +{ + fprintf(f, " {\n"); + fprintf(f, " \"lba\": %u,\n", s->lba); + fprintf(f, " \"label\": "); + xgd1_json_string(f, s->label ? s->label : ""); + fprintf(f, ",\n"); + fprintf(f, " \"cache_fill_lba\": %u,\n", s->cache_fill_lba); + fprintf(f, " \"cache_index\": %u,\n", s->cache_index); + fprintf(f, " \"cache_address\": \"0x%08x\",\n", s->cache_address); + fprintf(f, " \"cache_flush\": "); + xgd1_write_cmd_result(f, &s->cache_flush); + fprintf(f, ",\n"); + fprintf(f, " \"cache_fill\": "); + xgd1_write_cmd_result(f, &s->cache_fill); + fprintf(f, ",\n"); + fprintf(f, " \"memdump_header\": "); + xgd1_write_cmd_result(f, &s->memdump_header); + fprintf(f, ",\n"); + fprintf(f, " \"memdump_edc\": "); + xgd1_write_cmd_result(f, &s->memdump_edc); + fprintf(f, ",\n"); + fprintf(f, " \"raw_header_valid\": %s,\n", + s->raw_header_valid ? "true" : "false"); + if (s->raw_header_valid) + { + fprintf(f, " \"sector_information\": %u,\n", + (unsigned int)s->sector_information); + fprintf(f, " \"layer_number\": %u,\n", s->layer_number); + fprintf(f, " \"physical_sector_number\": %u,\n", + s->physical_sector_number); + fprintf(f, " \"normalized_physical_sector_number\": %u,\n", + s->normalized_physical_sector_number); + fprintf(f, " \"output_lba\": %u,\n", s->output_lba); + fprintf(f, " \"expected_layer_number\": %u,\n", + s->expected_layer_number); + fprintf(f, " \"expected_physical_sector_number\": %u,\n", + s->expected_physical_sector_number); + fprintf(f, " \"expected_normalized_physical_sector_number\": %u,\n", + s->expected_normalized_physical_sector_number); + fprintf(f, " \"layer_matches_expected\": %s,\n", + s->layer_matches_expected ? "true" : "false"); + fprintf(f, " \"psn_matches_expected\": %s,\n", + s->psn_matches_expected ? "true" : "false"); + fprintf(f, " \"geometry_matches_expected\": %s,\n", + s->geometry_matches_expected ? "true" : "false"); + fprintf(f, " \"logical_sha1\": \"%s\",\n", s->logical_sha1); + fprintf(f, " \"raw_header_hex\": "); + xgd1_json_hex(f, s->raw_header, 12U); + fprintf(f, ",\n"); + fprintf(f, " \"raw_edc_hex\": "); + xgd1_json_hex(f, s->raw_edc, 4U); + fprintf(f, ",\n"); + fprintf(f, " \"logical_data_hex\": "); + xgd1_json_hex(f, s->logical, XGD1_LOGICAL_SECTOR_BYTES); + fprintf(f, "\n"); + } + else + { + fprintf(f, " \"sector_information\": null,\n"); + fprintf(f, " \"layer_number\": null,\n"); + fprintf(f, " \"physical_sector_number\": null,\n"); + fprintf(f, " \"normalized_physical_sector_number\": null,\n"); + fprintf(f, " \"output_lba\": null,\n"); + fprintf(f, " \"expected_layer_number\": null,\n"); + fprintf(f, " \"expected_physical_sector_number\": null,\n"); + fprintf(f, " \"expected_normalized_physical_sector_number\": null,\n"); + fprintf(f, " \"layer_matches_expected\": false,\n"); + fprintf(f, " \"psn_matches_expected\": false,\n"); + fprintf(f, " \"geometry_matches_expected\": false,\n"); + fprintf(f, " \"logical_sha1\": null,\n"); + fprintf(f, " \"raw_header_hex\": \"\",\n"); + fprintf(f, " \"raw_edc_hex\": \"\",\n"); + fprintf(f, " \"logical_data_hex\": \"\"\n"); + } + fprintf(f, " }%s\n", comma ? "," : ""); +} + +static void xgd1_write_state(FILE *f, + const xgd1_raw_id_state *state, + int comma) +{ + uint32_t i; + + fprintf(f, " {\n"); + fprintf(f, " \"name\": "); + xgd1_json_string(f, state->name ? state->name : ""); + fprintf(f, ",\n"); + fprintf(f, " \"capacity_valid\": %s,\n", + state->capacity_valid ? "true" : "false"); + fprintf(f, " \"capacity_sectors\": %u,\n", state->capacity); + fprintf(f, " \"state_verified\": %s,\n", + state->state_verified ? "true" : "false"); + fprintf(f, " \"geometry_validation_failures\": %u,\n", + state->geometry_validation_failures); + fprintf(f, " \"geometry_verified\": %s,\n", + state->geometry_verified ? "true" : "false"); + fprintf(f, " \"samples\": [\n"); + for (i = 0; i < state->sample_count; ++i) + xgd1_write_sample(f, + &state->samples[i], + (i + 1U < state->sample_count)); + fprintf(f, " ]\n"); + fprintf(f, " }%s\n", comma ? "," : ""); +} + +static int xgd1_write_report(const char *report_path, + const char *device, + uint32_t entry_capacity, + int entry_game_view, + int volume_lock_ever, + const xgd1_raw_id_state *locked_state, + const xgd1_raw_id_state *unlocked_state, + int restore_attempted, + uint32_t restored_capacity, + int restore_verified) +{ + const char *final_path; + char tmp_path[MAX_PATH * 4]; + FILE *f; + int fd; + BOOL moved; + uint32_t validation_failures; + int geometry_resolved; + + final_path = (report_path && report_path[0]) + ? report_path + : "xgd1_raw_id_probe.json"; + + validation_failures = + (locked_state ? locked_state->geometry_validation_failures : 0U) + + (unlocked_state ? unlocked_state->geometry_validation_failures : 0U); + geometry_resolved = + (locked_state && locked_state->geometry_verified && + unlocked_state && unlocked_state->geometry_verified) ? 1 : 0; + + if (strlen(final_path) + 32U >= sizeof(tmp_path)) + { + printf("[XGD1-RAW-ID] Fatal: report path is too long.\n"); + return 0; + } + + sprintf(tmp_path, + "%s.tmp.%lu.%lu", + final_path, + (unsigned long)GetCurrentProcessId(), + (unsigned long)GetTickCount()); + + f = fopen(tmp_path, "wb"); + if (!f) + { + printf("[XGD1-RAW-ID] Fatal: could not create temporary report %s.\n", + tmp_path); + return 0; + } + + fprintf(f, "{\n"); + fprintf(f, " \"schema_version\": %u,\n", + XGD1_RAW_ID_PROBE_SCHEMA_VERSION); + fprintf(f, " \"producer\": \"friidump-0.5.3.13\",\n"); + fprintf(f, " \"probe\": \"original_xbox_xgd1_cache_aligned_raw_id_geometry\",\n"); + fprintf(f, " \"device\": "); + xgd1_json_string(f, device ? device : ""); + fprintf(f, ",\n"); + fprintf(f, " \"cache\": {\n"); + fprintf(f, " \"memdump_command\": \"E7 48 49 54 01\",\n"); + fprintf(f, " \"base_address\": \"0x%08x\",\n", + XGD1_RAW_ID_CACHE_BASE); + fprintf(f, " \"raw_sector_stride\": %u,\n", + XGD1_RAW_SECTOR_BYTES); + fprintf(f, " \"cache_flush_command\": \"READ(12), byte1=0x08, zero transfer length\",\n"); + fprintf(f, " \"cache_fill_command\": \"READ(12) streaming, 16 sectors, block-aligned LBA\",\n"); + fprintf(f, " \"memdump_regions\": \"12-byte ID/IED/CPR_MAI header plus 4-byte EDC only\",\n"); + fprintf(f, " \"raw_user_data_compared_to_logical\": false,\n"); + fprintf(f, " \"raw_user_data_note\": \"The proven Method 8 path replaces the drive-cache user field with READ(12) data before unscrambling; equality is not expected and is not a geometry test.\"\n"); + fprintf(f, " },\n"); + fprintf(f, " \"geometry_model\": {\n"); + fprintf(f, " \"dvd_start_psn\": 196608,\n"); + fprintf(f, " \"game_output_start_lba\": %u,\n", XGD1_GAME_OUTPUT_START_LBA); + fprintf(f, " \"layer_break_lba\": %u,\n", XGD1_REDUMP_LAYER_BREAK_LBA); + fprintf(f, " \"layer_zero_last_normalized_psn\": %u,\n", + 0x30000U + XGD1_REDUMP_LAYER_BREAK_LBA - 1U); + fprintf(f, " \"layer_one_raw_psn_is_24bit_complement\": true\n"); + fprintf(f, " },\n"); + fprintf(f, " \"safety\": {\n"); + fprintf(f, " \"read_only_sector_commands\": true,\n"); + fprintf(f, " \"authentication_handshake_used\": true,\n"); + fprintf(f, " \"tray_cycle_used\": true,\n"); + fprintf(f, " \"firmware_write_used\": false,\n"); + fprintf(f, " \"flash_command_used\": false\n"); + fprintf(f, " },\n"); + fprintf(f, " \"entry\": {\n"); + fprintf(f, " \"capacity_sectors\": %u,\n", entry_capacity); + fprintf(f, " \"game_view\": %s,\n", + entry_game_view ? "true" : "false"); + fprintf(f, " \"classification\": \"%s\"\n", + entry_game_view ? "unlocked_game" : "locked_video_or_unknown"); + fprintf(f, " },\n"); + fprintf(f, " \"volume_lock_acquired_at_least_once\": %s,\n", + volume_lock_ever ? "true" : "false"); + fprintf(f, " \"required_states_verified\": %s,\n", + (locked_state && locked_state->state_verified && + unlocked_state && unlocked_state->state_verified) + ? "true" : "false"); + fprintf(f, " \"geometry_validation_failures\": %u,\n", + validation_failures); + fprintf(f, " \"physical_geometry_resolved\": %s,\n", + geometry_resolved ? "true" : "false"); + fprintf(f, " \"states\": [\n"); + xgd1_write_state(f, locked_state, 1); + xgd1_write_state(f, unlocked_state, 0); + fprintf(f, " ],\n"); + fprintf(f, " \"restoration\": {\n"); + fprintf(f, " \"attempted\": %s,\n", + restore_attempted ? "true" : "false"); + fprintf(f, " \"capacity_sectors\": %u,\n", restored_capacity); + fprintf(f, " \"verified\": %s\n", + restore_verified ? "true" : "false"); + fprintf(f, " },\n"); + fprintf(f, " \"interpretation_boundary\": {\n"); + fprintf(f, " \"physical_geometry_resolved\": %s,\n", + geometry_resolved ? "true" : "false"); + fprintf(f, " \"pregame_filler_content_resolved\": false,\n"); + fprintf(f, " \"postgame_filler_content_resolved\": false,\n"); + fprintf(f, " \"note\": \"A complete cache-aligned raw-ID match can validate the logical-to-physical placement used by the current reconstruction. It does not make inaccessible filler sectors readable and does not establish exact filler bytes.\"\n"); + fprintf(f, " }\n"); + fprintf(f, "}\n"); + + if (fflush(f) != 0) + { + fclose(f); + DeleteFileA(tmp_path); + return 0; + } + + fd = _fileno(f); + if (fd < 0 || _commit(fd) != 0) + { + fclose(f); + DeleteFileA(tmp_path); + return 0; + } + + if (fclose(f) != 0) + { + DeleteFileA(tmp_path); + return 0; + } + + moved = MoveFileExA(tmp_path, + final_path, + MOVEFILE_REPLACE_EXISTING | + MOVEFILE_WRITE_THROUGH); + if (!moved) + { + printf("[XGD1-RAW-ID] Fatal: could not atomically publish report %s (error %lu).\n", + final_path, + (unsigned long)GetLastError()); + DeleteFileA(tmp_path); + return 0; + } + + printf("[XGD1-RAW-ID] Atomic report written: %s\n", final_path); + return 1; +} + +static int xgd1_raw_id_probe_core(HANDLE hDevice, + const char *device, + const char *report_path, + BOOL owns_handle) +{ + xgd1_raw_id_state locked_state; + xgd1_raw_id_state unlocked_state; + uint32_t entry_capacity; + uint32_t restored_capacity; + int entry_game_view; + int volume_lock_acquired; + int volume_lock_ever; + int restore_attempted; + int restore_verified; + int states_verified; + DWORD bytes_returned; + int report_ok; + + memset(&locked_state, 0, sizeof(locked_state)); + memset(&unlocked_state, 0, sizeof(unlocked_state)); + locked_state.name = "locked_video"; + locked_state.locked_view = 1; + unlocked_state.name = "unlocked_game"; + unlocked_state.locked_view = 0; + entry_capacity = 0; + restored_capacity = 0; + entry_game_view = 0; + volume_lock_acquired = 0; + volume_lock_ever = 0; + restore_attempted = 0; + restore_verified = 0; + states_verified = 0; + bytes_returned = 0; + + xbox_ref_log_open_for_target(report_path, + xgd1_drive_letter_from_device(device)); + printf("[XGD1-RAW-ID] Starting cache-aligned raw-sector ID geometry probe.\n"); + printf("[XGD1-RAW-ID] Requires modified GDR-8050L firmware with HIT 0xE7 memdump support.\n"); + printf("[XGD1-RAW-ID] No ISO or firmware-write command will be issued.\n"); + + if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) + { + printf("[XGD1-RAW-ID] Fatal: invalid drive handle.\n"); + if (owns_handle) + xbox_ref_log_close(); + return 1; + } + + if (!EnsureDriveReady(hDevice, 30000)) + { + printf("[XGD1-RAW-ID] Fatal: media did not become ready.\n"); + if (owns_handle) + xbox_ref_log_close(); + return 1; + } + + entry_capacity = GetTotalSectors(hDevice); + entry_game_view = xgd1_capacity_is_game(entry_capacity) ? 1 : 0; + printf("[XGD1-RAW-ID] Entry READ CAPACITY: %u sectors (%s).\n", + entry_capacity, + entry_game_view ? "game view" : "locked/video or unknown view"); + + DeviceIoControl(hDevice, + FSCTL_UNLOCK_VOLUME, + NULL, + 0, + NULL, + 0, + &bytes_returned, + NULL); + + printf("[XGD1-RAW-ID] Cycling tray to establish locked/video view.\n"); + AutomateTrayCycle(hDevice); + locked_state.capacity = xgd1_refresh_ready_capacity(hDevice); + locked_state.capacity_valid = locked_state.capacity != 0U; + locked_state.state_verified = + (locked_state.capacity_valid && + xgd1_capacity_is_locked(locked_state.capacity)) ? 1 : 0; + printf("[XGD1-RAW-ID] Locked/video READ CAPACITY: %u sectors; verified=%s.\n", + locked_state.capacity, + locked_state.state_verified ? "yes" : "no"); + + if (DeviceIoControl(hDevice, + FSCTL_LOCK_VOLUME, + NULL, + 0, + NULL, + 0, + &bytes_returned, + NULL)) + { + volume_lock_acquired = 1; + volume_lock_ever = 1; + printf("[XGD1-RAW-ID] Windows volume lock acquired.\n"); + } + else + { + printf("[XGD1-RAW-ID][WARN] Windows volume lock was not acquired.\n"); + } + + SetDriveSpeedMax(hDevice); + xgd1_capture_state(hDevice, + &locked_state, + xgd1_locked_points, + (uint32_t)(sizeof(xgd1_locked_points) / + sizeof(xgd1_locked_points[0]))); + + printf("[XGD1-RAW-ID] Applying the full Xbox handshake to establish game view.\n"); + UnlockDrive(hDevice); + unlocked_state.capacity = xgd1_refresh_ready_capacity(hDevice); + unlocked_state.capacity_valid = unlocked_state.capacity != 0U; + unlocked_state.state_verified = + (unlocked_state.capacity_valid && + xgd1_capacity_is_game(unlocked_state.capacity)) ? 1 : 0; + printf("[XGD1-RAW-ID] Unlocked/game READ CAPACITY: %u sectors; verified=%s.\n", + unlocked_state.capacity, + unlocked_state.state_verified ? "yes" : "no"); + + xgd1_capture_state(hDevice, + &unlocked_state, + xgd1_unlocked_points, + (uint32_t)(sizeof(xgd1_unlocked_points) / + sizeof(xgd1_unlocked_points[0]))); + + if (volume_lock_acquired) + { + DeviceIoControl(hDevice, + FSCTL_UNLOCK_VOLUME, + NULL, + 0, + NULL, + 0, + &bytes_returned, + NULL); + volume_lock_acquired = 0; + } + + if (!entry_game_view && entry_capacity != 0U) + { + restore_attempted = 1; + printf("[XGD1-RAW-ID] Restoring entry locked/video state with a final tray cycle.\n"); + AutomateTrayCycle(hDevice); + restored_capacity = xgd1_refresh_ready_capacity(hDevice); + restore_verified = + (restored_capacity != 0U && + xgd1_capacity_is_locked(restored_capacity)) ? 1 : 0; + } + else + { + restored_capacity = GetTotalSectors(hDevice); + restore_verified = + (entry_game_view && + xgd1_capacity_is_game(restored_capacity)) ? 1 : 0; + } + + states_verified = + (locked_state.state_verified && + unlocked_state.state_verified) ? 1 : 0; + + report_ok = xgd1_write_report(report_path, + device, + entry_capacity, + entry_game_view, + volume_lock_ever, + &locked_state, + &unlocked_state, + restore_attempted, + restored_capacity, + restore_verified); + + if (owns_handle) + { + printf("[XGD1-RAW-ID] Issuing STOP UNIT / spin-down after probe... "); + printf("%s\n", StopDriveUnit(hDevice) ? "OK" : "FAILED"); + CloseDrive(hDevice); + xbox_ref_log_close(); + } + + if (!report_ok) + return 1; + if (!states_verified) + { + printf("[XGD1-RAW-ID] Probe report is partial because one or more states were not verified.\n"); + return 1; + } + if (!locked_state.geometry_verified || !unlocked_state.geometry_verified) + { + printf("[XGD1-RAW-ID] Geometry validation failed: locked failures=%u, unlocked failures=%u.\n", + locked_state.geometry_validation_failures, + unlocked_state.geometry_validation_failures); + return 1; + } + + printf("[XGD1-RAW-ID] Probe complete. All selected raw IDs match the current XGD1 output geometry.\n"); + return 0; +} + +int xbox_ref_xgd1_raw_id_probe_with_handle(void *native_handle, + const char *device, + const char *report_path) +{ + return xgd1_raw_id_probe_core((HANDLE)native_handle, + device, + report_path, + FALSE); +} + +int xbox_ref_xgd1_raw_id_probe(const char *device, + const char *report_path) +{ + char drive_letter; + HANDLE hDevice; + + drive_letter = xgd1_drive_letter_from_device(device); + if (!drive_letter) + { + printf("[XGD1-RAW-ID] Fatal: could not parse drive letter from device.\n"); + return 1; + } + + hDevice = OpenDrive(drive_letter); + if (hDevice == INVALID_HANDLE_VALUE) + { + printf("[XGD1-RAW-ID] Fatal: cannot open drive %c:. Run as Administrator.\n", + drive_letter); + return 1; + } + + return xgd1_raw_id_probe_core(hDevice, + device, + report_path, + TRUE); +} + +#else + +int xbox_ref_xgd1_raw_id_probe_with_handle(void *native_handle, + const char *device, + const char *report_path) +{ + (void)native_handle; + (void)device; + (void)report_path; + return 1; +} + +int xbox_ref_xgd1_raw_id_probe(const char *device, + const char *report_path) +{ + (void)device; + (void)report_path; + return 1; +} + +#endif diff --git a/msvc32_friidump.rsp b/msvc32_friidump.rsp index 71ee8e9..ad9c2e3 100644 --- a/msvc32_friidump.rsp +++ b/msvc32_friidump.rsp @@ -36,6 +36,7 @@ libfriidump\vanilla_2064.c libfriidump\vanilla_2384.c libfriidump\win32compat.c libfriidump/xbox_ref_bridge.c +libfriidump/xbox_xgd1_raw_id_probe.c libfriidump/xbox_ref/utils.c libfriidump/xbox_ref/unlock.c libfriidump/xbox_ref/rc4.c diff --git a/src/friidump.c b/src/friidump.c index e30b553..292f658 100644 --- a/src/friidump.c +++ b/src/friidump.c @@ -28,6 +28,7 @@ #include "dumper.h" #include "unscrambler.h" #include "xbox_ref/xbox_ref_log.h" +#include "xbox_ref_bridge.h" #include "redump_dat.h" #ifdef WIN32 @@ -77,6 +78,7 @@ static redump_verify_result g_redump_result; 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 void friidump_summary_copy(char *dst, size_t dst_size, const char *src) { if (!dst || dst_size == 0) @@ -195,7 +197,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.10" +#define PACKAGE_VERSION "0.5.3.15" #ifdef WIN32 @@ -245,6 +247,10 @@ struct { char *redump_dat_dir; char *redump_report; bool no_redump_verify; + bool xgd1_layout_probe; + char *xgd1_layout_probe_report; + bool xgd1_raw_id_probe; + char *xgd1_raw_id_probe_report; } options; @@ -267,6 +273,8 @@ static char friidump_drive_letter_from_device(const char *device) { static const char *friidump_requested_output_target(void) { + if (options.xgd1_raw_id_probe_report && options.xgd1_raw_id_probe_report[0]) return options.xgd1_raw_id_probe_report; + if (options.xgd1_layout_probe_report && options.xgd1_layout_probe_report[0]) return options.xgd1_layout_probe_report; if (options.iso_out && options.iso_out[0]) return options.iso_out; if (options.xiso_out && options.xiso_out[0]) return options.xiso_out; if (options.raw_out && options.raw_out[0]) return options.raw_out; @@ -486,6 +494,59 @@ void welcome (void) { +static void friidump_init_executable_dir(const char *argv0) { + char path[1024]; + size_t length = 0; + char *slash; + char *backslash; + char *separator; + + g_executable_dir[0] = '\0'; + path[0] = '\0'; + +#ifdef WIN32 + { + DWORD result = GetModuleFileNameA(NULL, path, (DWORD)sizeof(path)); + if (result > 0 && result < sizeof(path)) { + path[result] = '\0'; + length = (size_t)result; + } + } +#else +#if defined(__linux__) + { + ssize_t result = readlink("/proc/self/exe", path, sizeof(path) - 1); + if (result > 0 && (size_t)result < sizeof(path)) { + path[result] = '\0'; + length = (size_t)result; + } + } +#endif + if (length == 0 && argv0 && argv0[0] && + (strchr(argv0, '/') || strchr(argv0, '\\'))) { + char *resolved = realpath(argv0, path); + if (resolved) + length = strlen(path); + } +#endif + + if (length == 0) + return; + + slash = strrchr(path, '/'); + backslash = strrchr(path, '\\'); + separator = slash; + if (backslash && (!separator || backslash > separator)) + separator = backslash; + + if (!separator) + return; + + *separator = '\0'; + if (path[0]) + snprintf(g_executable_dir, sizeof(g_executable_dir), "%s", path); +} + static const char *friidump_redump_dat_basename(disc_type type_id) { switch (type_id) { case DISC_TYPE_GAMECUBE: @@ -673,7 +734,7 @@ static void friidump_verify_redump_values(disc_type type_id, redump_verify_result_init(&g_redump_result); g_redump_attempted = false; representation_note = (type_id == DISC_TYPE_XBOX) - ? "XGD1 acquisition success and exact Redump hash identity are separate claims; documented synthetic reconstruction ranges may prevent an exact match." + ? "XGD1 acquisition success and exact Redump hash identity are separate claims; unresolved zero-filled pregame/postgame content may prevent an exact match." : ""; if (options.no_redump_verify) { @@ -699,8 +760,11 @@ static void friidump_verify_redump_values(disc_type type_id, return; } - dat_dir = (options.redump_dat_dir && options.redump_dat_dir[0]) ? options.redump_dat_dir : "redump_dat"; - snprintf(dat_path, sizeof(dat_path), "%s/%s", dat_dir, basename); + dat_dir = (options.redump_dat_dir && options.redump_dat_dir[0]) + ? options.redump_dat_dir + : NULL; + redump_resolve_dat_path(dat_dir, g_executable_dir, basename, + dat_path, sizeof(dat_path)); if (output_size == 0) output_size = friidump_file_size(output_path); @@ -901,9 +965,13 @@ 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: redump_dat)\n" + " (default: executable-relative redump_dat, then current directory)\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" " -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" @@ -962,6 +1030,8 @@ bool optparse (int argc, char **argv) { {"redump-dat-dir", 1, 0, 1006}, {"redump-report", 1, 0, 1007}, {"no-redump-verify", 0, 0, 1008}, + {"xgd1-layout-probe", 1, 0, 1009}, + {"xgd1-raw-id-probe", 1, 0, 1010}, #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'}, @@ -1010,6 +1080,10 @@ bool optparse (int argc, char **argv) { options.redump_dat_dir = NULL; options.redump_report = NULL; options.no_redump_verify = false; + options.xgd1_layout_probe = false; + options.xgd1_layout_probe_report = NULL; + options.xgd1_raw_id_probe = false; + options.xgd1_raw_id_probe_report = NULL; do { #ifdef DEBUG @@ -1165,6 +1239,16 @@ bool optparse (int argc, char **argv) { case 1008: options.no_redump_verify = true; break; + case 1009: + options.xgd1_layout_probe = true; + my_strdup (options.xgd1_layout_probe_report, optarg); + options.disctype = DISC_TYPE_XBOX; + break; + case 1010: + options.xgd1_raw_id_probe = true; + my_strdup (options.xgd1_raw_id_probe_report, optarg); + options.disctype = DISC_TYPE_XBOX; + break; case -1: break; default: @@ -1179,6 +1263,9 @@ bool optparse (int argc, char **argv) { fprintf (stderr, "WARNING: Extra parameters ignored\n"); } + if (options.xgd1_layout_probe || options.xgd1_raw_id_probe) + options.disctype = DISC_TYPE_XBOX; + /* Sanity checks... */ out = false; if (!options.device && !options.raw_in) { @@ -1192,6 +1279,13 @@ bool optparse (int argc, char **argv) { fprintf (stderr, "The -r, -i and -X options cannot be used together with -a.\n"); } else if (options.xiso_requested && (options.raw_out || options.iso_requested)) { fprintf (stderr, "The -X/--xiso option is a separate Xbox output mode and cannot be combined with -r or -i.\n"); + } else if ((options.xgd1_layout_probe || options.xgd1_raw_id_probe) && + (options.autodump || options.raw_in || options.raw_out || + options.iso_requested || options.xiso_requested || + options.allmethods || options.hlds_e7_scan || + 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 { /* Specified options seem to make sense */ out = true; @@ -1212,7 +1306,7 @@ int dologic (disc *d, progstats *stats) { u_int32_t current_sector = 0; xbox_forced = (options.disctype == DISC_TYPE_XBOX); - xbox_output_requested = xbox_forced || options.xiso_requested; + xbox_output_requested = xbox_forced || options.xiso_requested || options.xgd1_layout_probe || options.xgd1_raw_id_probe; dump_attempted = false; @@ -1327,6 +1421,67 @@ int dologic (disc *d, progstats *stats) { fprintf (stderr, "OK\n"); } + if (options.xgd1_layout_probe || options.xgd1_raw_id_probe) { +#ifdef WIN32 + int media_rc; + int media_sense_key; + int media_asc; + int media_ascq; + int probe_status; + bool stop_ok; + const char *probe_name; + + probe_name = options.xgd1_raw_id_probe ? "XGD1 raw-sector ID" : "XGD1 logical-boundary"; + + if (!disc_is_xbox_challenge_drive (d)) { + fprintf (stderr, + "%s probe currently supports only the GDR-8050L challenge-handshake profile.\n", + probe_name); + return false; + } + + if (options.xgd1_raw_id_probe && + !(disc_get_hlds_e7_type (d) == 44 || + disc_get_hlds_e7_type (d) == 45 || + disc_get_hlds_e7_type (d) == 442 || + disc_get_hlds_e7_type (d) == 443 || + disc_get_hlds_e7_type (d) == 445)) { + fprintf (stderr, + "--xgd1-raw-id-probe requires the modified GDR-8050L HIT 0xE7 memdump profile.\n"); + return false; + } + + fprintf (stderr, "\nChecking for ready Xbox media before %s probing... ", probe_name); + media_rc = disc_media_preflight (d, 15000, &media_sense_key, &media_asc, &media_ascq); + if (media_rc <= 0) { + fprintf (stderr, + "Failed (sense %02X/%02X/%02X). Insert the disc, wait for spin-up, close AutoPlay/File Explorer, and retry.\n", + media_sense_key, media_asc, media_ascq); + return false; + } + fprintf (stderr, "OK\n"); + + gettimeofday (&(stats -> start_time), NULL); + if (options.xgd1_raw_id_probe) + probe_status = xbox_ref_xgd1_raw_id_probe_with_handle ( + disc_get_native_handle (d), disc_get_device (d), options.xgd1_raw_id_probe_report); + else + probe_status = xbox_ref_xgd1_layout_probe_with_handle ( + disc_get_native_handle (d), disc_get_device (d), options.xgd1_layout_probe_report); + + fprintf (stderr, "Issuing STOP UNIT / spin-down after %s probe... ", probe_name); + stop_ok = disc_stop_unit (d, false); + fprintf (stderr, "%s\n", stop_ok ? "OK" : "Failed"); + gettimeofday (&(stats -> end_time), NULL); + fprintf (stderr, "%s probe status: %s\n", probe_name, probe_status == 0 ? "OK" : "FAILED"); + friidump_summary_reset(); + return probe_status == 0; +#else + fprintf (stderr, "XGD1 probe modes are available only in the Windows build.\n"); + return false; +#endif + } + if (options.hlds_e7_scan) { out = disc_hlds_e7_scan (d, options.hlds_e7_scan_log, options.hlds_e7_scan_dump_prefix); fprintf (stderr, "Issuing STOP UNIT / spin-down after HLDS 0xE7 scan... "); @@ -1679,6 +1834,7 @@ int main (int argc, char *argv[]) { /* First of all... */ drop_euid (); + friidump_init_executable_dir((argc > 0) ? argv[0] : NULL); xbox_ref_log_open_for_target(NULL, 0); welcome (); @@ -1758,6 +1914,8 @@ int main (int argc, char *argv[]) { my_free (options.raw_out); my_free (options.raw_in); my_free (options.hlds_profile_report); + my_free (options.xgd1_layout_probe_report); + my_free (options.xgd1_raw_id_probe_report); } if (xbox_ref_log_path()) diff --git a/src/redump_dat.c b/src/redump_dat.c index 6231679..028aa13 100644 --- a/src/redump_dat.c +++ b/src/redump_dat.c @@ -5,6 +5,93 @@ #include #include +static int redump_path_exists(const char *path) { + FILE *f; + if (!path || !path[0]) + return 0; + f = fopen(path, "rb"); + if (!f) + return 0; + fclose(f); + return 1; +} + +static int redump_join_path( + const char *dir, + const char *child_dir, + const char *basename, + char *out_path, + size_t out_path_size +) { + int written; + + if (!dir || !dir[0] || !basename || !basename[0] || + !out_path || out_path_size == 0) + return 0; + + if (child_dir && child_dir[0]) + written = snprintf(out_path, out_path_size, "%s/%s/%s", + dir, child_dir, basename); + else + written = snprintf(out_path, out_path_size, "%s/%s", + dir, basename); + + if (written < 0 || (size_t)written >= out_path_size) { + out_path[0] = '\0'; + return 0; + } + + return 1; +} + +int redump_resolve_dat_path( + const char *explicit_dir, + const char *executable_dir, + const char *basename, + char *out_path, + size_t out_path_size +) { + char candidate[1024]; + + if (!basename || !basename[0] || !out_path || out_path_size == 0) + return 0; + + out_path[0] = '\0'; + + if (explicit_dir && explicit_dir[0]) { + if (!redump_join_path(explicit_dir, NULL, basename, + out_path, out_path_size)) + return 0; + return redump_path_exists(out_path); + } + + if (executable_dir && executable_dir[0] && + redump_join_path(executable_dir, "redump_dat", basename, + candidate, sizeof(candidate))) { + if (redump_path_exists(candidate)) { + 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); + } + + 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); + } + + return 0; +} + static int redump_ascii_equal(const char *a, const char *b) { unsigned char ca, cb; if (!a || !b) diff --git a/src/redump_dat.h b/src/redump_dat.h index 39919ed..475f5c2 100644 --- a/src/redump_dat.h +++ b/src/redump_dat.h @@ -49,6 +49,26 @@ const char *redump_field_status_string(redump_field_status status); const char *redump_verify_overall_string(const redump_verify_result *result); const char *redump_verify_confidence_string(const redump_verify_result *result); 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. + * + * 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, + const char *executable_dir, + const char *basename, + char *out_path, + size_t out_path_size +); + int redump_verify_dat_file( const char *dat_path, uint64_t image_size, diff --git a/tests/test_redump_dat_resolver.c b/tests/test_redump_dat_resolver.c new file mode 100644 index 0000000..275c9b0 --- /dev/null +++ b/tests/test_redump_dat_resolver.c @@ -0,0 +1,61 @@ +#include "redump_dat.h" + +#include +#include +#include + +static const char *optional_arg(const char *arg) { + if (!arg || strcmp(arg, "-") == 0) + return NULL; + return arg; +} + +int main(int argc, char **argv) { + const char *explicit_dir; + const char *executable_dir; + const char *basename; + const char *expected_path; + int expected_found; + int found; + char resolved[1024]; + + if (argc != 6) { + fprintf(stderr, + "Usage: %s " + " \n", + argc > 0 ? argv[0] : "test_redump_dat_resolver"); + return 64; + } + + explicit_dir = optional_arg(argv[1]); + executable_dir = optional_arg(argv[2]); + basename = argv[3]; + expected_path = argv[4]; + expected_found = atoi(argv[5]) ? 1 : 0; + + memset(resolved, 0, sizeof(resolved)); + found = redump_resolve_dat_path( + explicit_dir, + executable_dir, + basename, + resolved, + sizeof(resolved) + ) ? 1 : 0; + + printf("resolved=%s\n", resolved); + printf("found=%d\n", found); + + if (found != expected_found) { + fprintf(stderr, "Found-state mismatch: expected %d, observed %d\n", + expected_found, found); + return 1; + } + + if (strcmp(resolved, expected_path) != 0) { + fprintf(stderr, "Path mismatch:\n expected: %s\n observed: %s\n", + expected_path, resolved); + return 2; + } + + return 0; +}