]> FriiDump Source - friidump.git/blob - libfriidump/xbox_ref/utils.c
Harden GCC-4243N A102 HLDS E7 profile
[friidump.git] / libfriidump / xbox_ref / utils.c
1 #include "utils.h"
2 #include "scsi_structs.h"
3 #include "xbe_cert.h"
4 #include "unlock.h"
5 #include "xbox_ref_log.h"
6 #include "../xbox_ref_bridge.h"
7
8 #include <time.h>
9 #include <windows.h>
10 #include <winioctl.h>
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <ctype.h>
14 #include <string.h>
15 #include <errno.h>
16 #include <io.h>
17 #include <ntddstor.h>
18
19 #include <wincrypt.h>
20 #pragma comment(lib, "advapi32.lib")
21
22 #ifndef PROV_RSA_AES
23 #define PROV_RSA_AES 24
24 #endif
25 #ifndef ALG_SID_SHA_256
26 #define ALG_SID_SHA_256 12
27 #endif
28 #ifndef CALG_SHA_256
29 #define CALG_SHA_256 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_256)
30 #endif
31
32 #define printf xbox_ref_printf
33
34 #define GDR_8163B OL23
35
36 /* The copied Xbox dumper is single-threaded.  FriiDump installs one
37  * per-invocation cancellation callback before entering the copied path and
38  * clears it during cleanup. */
39 static int (*g_xbox_dump_cancel)(void *cancel_data) = NULL;
40 static void *g_xbox_dump_cancel_data = NULL;
41 static xbox_ref_dump_result *g_xbox_dump_cancel_result = NULL;
42
43 static BOOL XboxDumpCancelRequested(void)
44 {
45     if (!g_xbox_dump_cancel || !g_xbox_dump_cancel(g_xbox_dump_cancel_data))
46         return FALSE;
47
48     if (g_xbox_dump_cancel_result)
49         g_xbox_dump_cancel_result->cancelled = 1;
50
51     return TRUE;
52 }
53
54 HANDLE OpenDrive(char driveLetter)
55 {
56     char devicePath[16];
57     snprintf(devicePath, sizeof(devicePath), "\\\\.\\%c:", driveLetter);
58
59     HANDLE hDevice = CreateFileA(devicePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
60     return hDevice;
61 }
62
63 void CloseDrive(HANDLE hDevice)
64 {
65     if (hDevice && hDevice != INVALID_HANDLE_VALUE)
66         CloseHandle(hDevice);
67 }
68
69 int IsDiscPresent(HANDLE hDevice)
70 {
71     DWORD bytesReturned;
72     return DeviceIoControl(hDevice, IOCTL_STORAGE_CHECK_VERIFY, NULL, 0, NULL, 0, &bytesReturned, NULL);
73 }
74
75 void ControlTray(HANDLE hDevice, BOOL eject)
76 {
77     SCSI_PASS_THROUGH_DIRECT sptd;
78     DWORD returned;
79     memset(&sptd, 0, sizeof(sptd));
80
81     sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
82     sptd.CdbLength = 6;
83     sptd.TimeOutValue = 10;
84     sptd.Cdb[0] = 0x1B; // START STOP UNIT
85
86     if (eject)
87     {
88         printf("Software Ejecting tray...\n");
89         sptd.Cdb[4] = 0x02; // Power Action: Eject
90     }
91     else
92     {
93         printf("Software Closing tray...\n");
94         sptd.Cdb[4] = 0x03; // Power Action: Load
95     }
96     if (DeviceIoControl(hDevice, IOCTL_SCSI_PASS_THROUGH_DIRECT, &sptd, sizeof(sptd), &sptd, sizeof(sptd), &returned, NULL))
97     {
98         printf("Tray %s successful.\n", eject ? "eject" : "close");
99     }
100     else
101     {
102         DWORD err = GetLastError();
103         printf("Failed to %s tray. Error: %lu\n", eject ? "eject" : "close", err);
104
105         if (err == ERROR_ACCESS_DENIED)
106         {
107             printf("Hint: Ensure no other program is locking the drive.\n");
108         }
109     }
110 }
111
112 BOOL TestUnitReady(HANDLE hDevice)
113 {
114     SCSI_PASS_THROUGH_DIRECT sptd;
115     DWORD returned;
116     memset(&sptd, 0, sizeof(sptd));
117
118     sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
119     sptd.CdbLength = 6;
120     sptd.TimeOutValue = 10;
121     sptd.DataTransferLength = 0;
122     sptd.DataBuffer = NULL;
123
124     // TEST UNIT READY.  This is our practical poll for "ready/spun up".
125     // Many drives do not expose a literal spindle-state bit to normal host software;
126     // after STOP UNIT, TEST UNIT READY should fail until the unit is ready again.
127     sptd.Cdb[0] = 0x00;
128
129     if (!DeviceIoControl(hDevice,
130                          IOCTL_SCSI_PASS_THROUGH_DIRECT,
131                          &sptd,
132                          sizeof(sptd),
133                          &sptd,
134                          sizeof(sptd),
135                          &returned,
136                          NULL))
137     {
138         return FALSE;
139     }
140
141     return (sptd.ScsiStatus == 0);
142 }
143
144 BOOL StartDriveUnit(HANDLE hDevice)
145 {
146     SCSI_PASS_THROUGH_DIRECT sptd;
147     DWORD returned;
148     memset(&sptd, 0, sizeof(sptd));
149
150     sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
151     sptd.CdbLength = 6;
152     sptd.TimeOutValue = 30;
153     sptd.DataTransferLength = 0;
154     sptd.DataBuffer = NULL;
155
156     // START STOP UNIT, START=1, LOEJ=0.
157     // This requests spin-up/start without ejecting/loading the tray.
158     sptd.Cdb[0] = 0x1B;
159     sptd.Cdb[4] = 0x01;
160
161     printf("Sending SCSI START UNIT / spin-up command...\n");
162
163     if (DeviceIoControl(hDevice,
164                         IOCTL_SCSI_PASS_THROUGH_DIRECT,
165                         &sptd,
166                         sizeof(sptd),
167                         &sptd,
168                         sizeof(sptd),
169                         &returned,
170                         NULL))
171     {
172         printf("SCSI START UNIT / spin-up command accepted.\n");
173         return TRUE;
174     }
175
176     {
177         DWORD err = GetLastError();
178         printf("[WARN] SCSI START UNIT / spin-up failed. Error: %lu\n", err);
179         return FALSE;
180     }
181 }
182
183 BOOL EnsureDriveReady(HANDLE hDevice, DWORD timeoutMs)
184 {
185     DWORD startTick = GetTickCount();
186     BOOL startIssued = FALSE;
187
188     printf("Polling drive readiness with TEST UNIT READY...\n");
189
190     for (;;)
191     {
192         if (TestUnitReady(hDevice))
193         {
194             printf("Drive reports ready.\n");
195             return TRUE;
196         }
197
198         if (!startIssued)
199         {
200             printf("Drive is not ready/spun up yet; requesting START UNIT.\n");
201             StartDriveUnit(hDevice);
202             startIssued = TRUE;
203         }
204
205         if ((GetTickCount() - startTick) >= timeoutMs)
206         {
207             printf("[WARN] Drive did not report ready within %lu ms.\n", (unsigned long)timeoutMs);
208             printf("       Continuing may fail if the unit is still spun down or still reading lead-in.\n");
209             return FALSE;
210         }
211
212         Sleep(1000);
213     }
214 }
215
216 BOOL StopDriveUnit(HANDLE hDevice)
217 {
218     SCSI_PASS_THROUGH_DIRECT sptd;
219     DWORD returned;
220     memset(&sptd, 0, sizeof(sptd));
221
222     sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
223     sptd.CdbLength = 6;
224     sptd.TimeOutValue = 30;
225     sptd.DataTransferLength = 0;
226     sptd.DataBuffer = NULL;
227
228     // START STOP UNIT, START=0, LOEJ=0.
229     // This requests a normal stop/spin-down without ejecting or loading the tray.
230     sptd.Cdb[0] = 0x1B;
231     sptd.Cdb[4] = 0x00;
232
233     printf("Sending SCSI STOP UNIT / spin-down command...\n");
234
235     if (DeviceIoControl(hDevice,
236                         IOCTL_SCSI_PASS_THROUGH_DIRECT,
237                         &sptd,
238                         sizeof(sptd),
239                         &sptd,
240                         sizeof(sptd),
241                         &returned,
242                         NULL))
243     {
244         printf("SCSI STOP UNIT / spin-down successful.\n");
245         return TRUE;
246     }
247
248     {
249         DWORD err = GetLastError();
250         printf("[WARN] SCSI STOP UNIT / spin-down failed. Error: %lu\n", err);
251         printf("       Dump output has already been finalized; this only affects drive spin state.\n");
252         return FALSE;
253     }
254 }
255
256 void AutomateTrayCycle(HANDLE hDevice)
257 {
258     ControlTray(hDevice, TRUE);
259     Sleep(3000); // Give the tray time to fully extend
260
261     // --- CLOSE ---
262     ControlTray(hDevice, FALSE);
263     printf("Waiting for disc spin-up/readiness after tray close...\n");
264     if (EnsureDriveReady(hDevice, 45000))
265     {
266         // Small settle period after readiness so the drive can finish lead-in/media-change bookkeeping.
267         Sleep(1500);
268     }
269     else
270     {
271         // Preserve the old conservative behavior if TEST UNIT READY polling never succeeds.
272         printf("[WARN] Falling back to fixed 10s post-close settle delay.\n");
273         Sleep(10000);
274     }
275 }
276
277 BOOL SetDriveSpeedMax(HANDLE hDevice)
278 {
279     SCSI_PASS_THROUGH_DIRECT sptd = {0};
280     sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
281     sptd.PathId = 0;
282     sptd.TargetId = 1;
283     sptd.Lun = 0;
284     sptd.CdbLength = 12; // 12-byte CDB for 0xBB
285     sptd.DataIn = SCSI_IOCTL_DATA_OUT;
286     sptd.TimeOutValue = 10;
287     sptd.DataBuffer = NULL;
288     sptd.DataTransferLength = 0;
289
290     // CDB 0xBB: [0] Opcode, [2-3] Read Speed, [4-5] Write Speed
291     sptd.Cdb[0] = 0xBB;
292     sptd.Cdb[2] = 0xFF; // MSB
293     sptd.Cdb[3] = 0xFF; // LSB
294     sptd.Cdb[4] = 0xFF; // MSB
295     sptd.Cdb[5] = 0xFF; // LSB
296
297     DWORD returned;
298     return DeviceIoControl(hDevice, IOCTL_SCSI_PASS_THROUGH_DIRECT,
299                            &sptd, sizeof(sptd), &sptd, sizeof(sptd),
300                            &returned, NULL);
301 }
302
303 void ForceMediaRefresh(HANDLE hDevice)
304 {
305     DWORD bytesReturned;
306
307     // Lock the volume so Windows stops background polling
308     DeviceIoControl(hDevice, FSCTL_LOCK_VOLUME, NULL, 0, NULL, 0, &bytesReturned, NULL);
309
310     // Force the storage stack to re-read the Partition Table/Capacity
311     // without sending an Eject command to the hardware.
312     if (DeviceIoControl(hDevice, IOCTL_DISK_UPDATE_PROPERTIES, NULL, 0, NULL, 0, &bytesReturned, NULL))
313     {
314         printf("Windows Partition Stack refreshed silently.\n");
315     }
316
317     // Explicitly dismount to kill the "Video DVD" file system driver (UDFS/ISO9660)
318     DeviceIoControl(hDevice, FSCTL_DISMOUNT_VOLUME, NULL, 0, NULL, 0, &bytesReturned, NULL);
319     Sleep(1000);
320 }
321
322 void HexDump(unsigned char *buffer, uint32_t size)
323 {
324     for (uint32_t i = 0; i < size; i++)
325     {
326         if (i % 16 == 0)
327             printf("\n%04X: ", i);
328         printf("%02X ", buffer[i]);
329     }
330     printf("\n");
331 }
332
333 void outputdata(const uint8_t *buf, uint32_t lines)
334 {
335     for (uint32_t j = 0; j < lines; j++)
336     {
337         for (uint32_t k = 0; k < 16; k++)
338         {
339             uint32_t idx = j * 16 + k;
340             if (k == 8)
341                 printf("- ");
342             printf("%02X ", buf[idx]);
343         }
344         printf("\n");
345     }
346 }
347
348 uint8_t chksum8(const unsigned char *buff, size_t len) {
349     unsigned int sum = 0;
350     for (sum = 0; len != 0; len--)
351         sum += *(buff++);
352     return (uint8_t)sum;
353 }
354
355 void FormatElapsedTime(DWORD dwMilliseconds, char *outStr)
356 {
357     uint32_t totalSeconds = dwMilliseconds / 1000;
358     uint32_t hours = totalSeconds / 3600;
359     uint32_t minutes = (totalSeconds % 3600) / 60;
360     uint32_t seconds = totalSeconds % 60;
361
362     sprintf(outStr, "%02u:%02u:%02u", hours, minutes, seconds);
363 }
364
365 void PrintFormattedCapacity(unsigned char *scsibuffer)
366 {
367     // The first 4 bytes are the Last Logical Block Address (Big Endian)
368     uint32_t maxLBA = (scsibuffer[0] << 24) | (scsibuffer[1] << 16) |
369                       (scsibuffer[2] << 8) | scsibuffer[3];
370
371     // The next 4 bytes are the Block Length (Big Endian)
372     uint32_t blockLen = (scsibuffer[4] << 24) | (scsibuffer[5] << 16) |
373                         (scsibuffer[6] << 8) | scsibuffer[7];
374
375     // Total bytes = (MaxLBA + 1) * BlockLen
376     // Use double for the math to avoid 32-bit integer overflow
377     double totalBytes = (double)(maxLBA + 1) * blockLen;
378     double totalGB = totalBytes / (1024.0 * 1024.0 * 1024.0);
379
380     printf("--------------------------------------------\n");
381     printf("Drive Capacity Details:\n");
382     printf("  Total Sectors: %u\n", maxLBA + 1);
383     printf("  Sector Size:   %u bytes\n", blockLen);
384     printf("  Total Size:    %.2f GB\n", totalGB);
385     printf("--------------------------------------------\n");
386 }
387
388 void ListOpticalDrives()
389 {
390     DWORD drives = GetLogicalDrives();
391     char rootPath[] = "A:\\";
392     char devicePath[] = "\\\\.\\A:";
393     BYTE buffer[1024];
394
395     printf("%-5s %-12s %-18s %-15s %s\n", "ID", "Vendor", "Model", "Volume Label", "Status");
396     printf("-------------------------------------------------------------------------------\n");
397
398     uint8_t driveCount = 0;
399     for (int i = 0; i < 26; i++)
400     {
401         if (drives & (1 << i))
402         {
403             rootPath[0] = 'A' + i;
404
405             if (GetDriveTypeA(rootPath) == DRIVE_CDROM)
406             {
407                 devicePath[4] = 'A' + i;
408
409                 // 1. Get Hardware Info (Vendor/Model)
410                 char vendorStr[16] = "Generic";
411                 char productStr[21] = "Unknown";
412
413                 HANDLE h = CreateFileA(devicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE,
414                                        NULL, OPEN_EXISTING, 0, NULL);
415
416                 if (h != INVALID_HANDLE_VALUE)
417                 {
418                     STORAGE_PROPERTY_QUERY query = {0};
419                     query.PropertyId = StorageDeviceProperty;
420                     query.QueryType = PropertyStandardQuery;
421                     DWORD bytes;
422
423                     if (DeviceIoControl(h, IOCTL_STORAGE_QUERY_PROPERTY, &query, sizeof(query),
424                                         buffer, sizeof(buffer), &bytes, NULL))
425                     {
426                         PSTORAGE_DEVICE_DESCRIPTOR desc = (PSTORAGE_DEVICE_DESCRIPTOR)buffer;
427                         if (desc->VendorIdOffset)
428                             strcpy(vendorStr, (char *)(buffer + desc->VendorIdOffset));
429                         if (desc->ProductIdOffset)
430                             strcpy(productStr, (char *)(buffer + desc->ProductIdOffset));
431                     }
432                     CloseHandle(h);
433                 }
434
435                 // 2. Get Volume Info (Disc Label)
436                 char volumeName[MAX_PATH + 1] = {0};
437                 char statusStr[20] = "No Disc";
438
439                 if (GetVolumeInformationA(rootPath, volumeName, sizeof(volumeName),
440                                           NULL, NULL, NULL, NULL, 0))
441                 {
442                     if (strlen(volumeName) == 0)
443                         strcpy(volumeName, "[No Label]");
444                     strcpy(statusStr, "Ready");
445                 }
446
447                 printf("  %c:   %-12.12s %-18.18s %-15.15s %s\n",
448                        rootPath[0], vendorStr, productStr, volumeName, statusStr);
449                 driveCount++;
450             }
451         }
452     }
453     if (driveCount == 0)
454         printf("No optical drives found.\n");
455     printf("-------------------------------------------------------------------------------\n");
456     printf("Total Optical Drives Found: %u\n", driveCount);
457 }
458
459 uint32_t GetTotalSectors(HANDLE hDevice)
460 {
461     typedef struct _SCSI_PASS_THROUGH_WITH_BUFFERS
462     {
463         SCSI_PASS_THROUGH spt;
464         unsigned char ucDataBuf[8]; // Buffer for the 8-byte READ CAPACITY result
465     } SCSI_PASS_THROUGH_WITH_BUFFERS;
466
467     SCSI_PASS_THROUGH_WITH_BUFFERS sptwb = {0};
468
469     sptwb.spt.Length = sizeof(SCSI_PASS_THROUGH);
470     sptwb.spt.CdbLength = 10; // READ CAPACITY (10) is a 10-byte command
471     sptwb.spt.DataIn = SCSI_IOCTL_DATA_IN;
472     sptwb.spt.DataTransferLength = 8;
473     sptwb.spt.TimeOutValue = 2; // 2 second timeout
474     sptwb.spt.DataBufferOffset = offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS, ucDataBuf);
475
476     // CDB 0x25 = READ CAPACITY (10)
477     sptwb.spt.Cdb[0] = 0x25;
478
479     DWORD bytesReturned;
480     if (DeviceIoControl(hDevice, IOCTL_SCSI_PASS_THROUGH,
481                         &sptwb, sizeof(sptwb),
482                         &sptwb, sizeof(sptwb),
483                         &bytesReturned, NULL))
484     {
485
486         // Extract Max LBA (Big Endian) from the first 4 bytes
487         uint32_t maxLBA = (sptwb.ucDataBuf[0] << 24) |
488                           (sptwb.ucDataBuf[1] << 16) |
489                           (sptwb.ucDataBuf[2] << 8) |
490                           sptwb.ucDataBuf[3];
491
492         return (maxLBA + 1);
493     }
494
495     return 0; // Return 0 on failure
496 }
497
498 uint32_t GetXboxPhysicalSectors(HANDLE hDevice)
499 {
500     SCSI_PASS_THROUGH_DIRECT sptd = {0};
501     unsigned char buffer[2048] = {0};
502     DWORD bytesReturned;
503
504     sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
505     sptd.CdbLength = 12;
506     sptd.DataIn = SCSI_IOCTL_DATA_IN;
507     sptd.DataTransferLength = 2048;
508     sptd.TimeOutValue = 10;
509     sptd.DataBuffer = buffer;
510
511     // READ DVD STRUCTURE (0xAD)
512     sptd.Cdb[0] = 0xAD;
513     sptd.Cdb[7] = 0x00; // Format: Physical Format Information
514     sptd.Cdb[8] = 0x08; // Allocation Length (MSB)
515     sptd.Cdb[9] = 0x00; // Allocation Length (LSB)
516
517     if (DeviceIoControl(hDevice, IOCTL_SCSI_PASS_THROUGH_DIRECT, &sptd, sizeof(sptd), &sptd, sizeof(sptd), &bytesReturned, NULL))
518     {
519         // Bytes 13-15 of the PFI contain the End LBA of the data area
520         uint32_t endLba = (buffer[13] << 16) | (buffer[14] << 8) | buffer[15];
521
522         // For Xbox discs, we add 1 to the End LBA to get the total count
523         // and add the 32 sectors of lead-in padding we manually create.
524         return endLba + 1;
525     }
526
527     // Fallback for Dual Layer if command fails
528     return 3431264;
529 }
530
531 // Forces Windows to re-evaluate the drive without ejecting the tray
532 void RefreshVolume(HANDLE hDevice)
533 {
534     DWORD bytesReturned;
535     printf("Refreshing Volume Stack (Quiet Mode)...\n");
536     // Only update properties; do NOT dismount as it resets the GDR-8163B state.
537     DeviceIoControl(hDevice, IOCTL_DISK_UPDATE_PROPERTIES, NULL, 0, NULL, 0, &bytesReturned, NULL);
538     Sleep(1000); // Essential for the firmware to re-index after the OS check
539 }
540
541 void ListDirectoryRecursive(HANDLE hDevice, uint32_t lba, uint32_t size, int level)
542 {
543     if (size == 0 || level > 10)
544         return; // Prevent infinite recursion
545
546     uint32_t sectorsToRead = (size + 2047) / 2048;
547     unsigned char *dirBuffer = (unsigned char *)VirtualAlloc(NULL, sectorsToRead * 2048, MEM_COMMIT, PAGE_READWRITE);
548     if (!dirBuffer)
549         return;
550
551     if (ScsiReadSectors(hDevice, lba, (uint16_t)sectorsToRead, dirBuffer))
552     {
553         uint32_t offset = 0;
554         while (offset < size)
555         {
556             XDFS_DIR_ENTRY *entry = (XDFS_DIR_ENTRY *)&dirBuffer[offset];
557
558             // --- SANITY CHECK 1: End of Table ---
559             // If FileNameLength is 0 or 0xFF, we've hit the padding/end of the list.
560             if (entry->FileNameLength == 0 || entry->FileNameLength == 0xFF)
561                 break;
562
563             // --- SANITY CHECK 2: Buffer Overflow ---
564             // Ensure the entry doesn't claim to exist past our allocated buffer.
565             if (offset + 14 + entry->FileNameLength > size)
566                 break;
567
568             // --- SANITY CHECK 3: Character Validation ---
569             // If the first character isn't a printable ASCII, it's a glitch entry.
570             if (entry->FileName[0] < 32 || entry->FileName[0] > 126)
571                 break;
572
573             // Indentation
574             for (int i = 0; i < level; i++)
575                 printf("  ");
576
577             // Branch Visual
578             if (entry->Attributes & 0x10)
579             {
580                 printf("[DIR] ");
581             }
582             else
583             {
584                 printf(" |-- ");
585             }
586
587             // Print Filename safely
588             for (int i = 0; i < entry->FileNameLength; i++)
589             {
590                 char c = entry->FileName[i];
591                 if (c >= 32 && c <= 126)
592                     printf("%c", c);
593                 else
594                     printf("?"); // Replace glitches with a placeholder
595             }
596
597             if (!(entry->Attributes & 0x10))
598             {
599                 printf(" (%u bytes)", entry->FileSize);
600             }
601             printf("\n");
602
603             // RECURSION: Only dive if it's a valid directory LBA
604             if ((entry->Attributes & 0x10) && entry->StartLBA > 0x100)
605             {
606                 ListDirectoryRecursive(hDevice, entry->StartLBA, entry->FileSize, level + 1);
607             }
608
609             // Move to next entry (4-byte alignment)
610             uint32_t nextOffset = (14 + entry->FileNameLength + 3) & ~3;
611
612             // If the calculation gives us 0, we're stuck in an infinite loop; break.
613             if (nextOffset == 0)
614                 break;
615             offset += nextOffset;
616         }
617     }
618
619     VirtualFree(dirBuffer, 0, MEM_RELEASE);
620 }
621
622 void ReadXboxGameDir(HANDLE hDevice)
623 {
624     // Single buffer for the Volume Descriptor read
625     unsigned char *sectorBuffer = (unsigned char *)VirtualAlloc(NULL, 2048, MEM_COMMIT, PAGE_READWRITE);
626     if (!sectorBuffer)
627         return;
628
629     // Read XDFS Volume Descriptor at Sector 0x20
630     if (!ScsiReadSectors(hDevice, 0x20, 1, sectorBuffer))
631     {
632         printf("Error: Could not read XDFS Volume Descriptor.\n");
633         VirtualFree(sectorBuffer, 0, MEM_RELEASE);
634         return;
635     }
636
637     // Map the descriptor and extract root location/size
638     XDFS_VOLUME_DESCRIPTOR *vol = (XDFS_VOLUME_DESCRIPTOR *)sectorBuffer;
639     uint32_t rootLba = vol->RootLBA;
640     uint32_t rootSize = vol->RootSize;
641
642     // We no longer need this buffer once we have the Root LBA/Size
643     VirtualFree(sectorBuffer, 0, MEM_RELEASE);
644
645     // Draw the recursive tree
646     printf("\n--- XDFS FILE SYSTEM TREE ---\n");
647
648     if (rootLba > 0)
649     {
650         ListDirectoryRecursive(hDevice, rootLba, rootSize, 0);
651     }
652     else
653     {
654         printf("Error: Invalid Root LBA.\n");
655     }
656
657     printf("------------------------------\n");
658 }
659
660 void SanitizeFilename(char *filename)
661 {
662     if (!filename || filename[0] == '\0')
663         return;
664
665     int readIndex = 0;
666     int writeIndex = 0;
667     int lastWasSpace = 1; // Using 1 for true to trim leading spaces
668
669     while (filename[readIndex] != '\0')
670     {
671         unsigned char c = (unsigned char)filename[readIndex];
672
673         // Whitelist: Only allow Letters (isalnum) and Spaces
674         // This strips ! ' ? : " / \ | * < > and non-printable characters
675         if (isalnum(c) || c == ' ')
676         {
677
678             // Collapse Multiple Spaces
679             if (c == ' ')
680             {
681                 if (!lastWasSpace)
682                 {
683                     filename[writeIndex++] = ' ';
684                     lastWasSpace = 1;
685                 }
686             }
687             else
688             {
689                 // It's a letter or number, write it normally
690                 filename[writeIndex++] = c;
691                 lastWasSpace = 0;
692             }
693         }
694         readIndex++;
695     }
696
697     // Null-terminate the new shorter string
698     filename[writeIndex] = '\0';
699
700     // Remove trailing space if one exists
701     if (writeIndex > 0 && filename[writeIndex - 1] == ' ')
702     {
703         filename[writeIndex - 1] = '\0';
704     }
705 }
706
707 BOOL ScsiReadSectors(HANDLE hDevice, uint32_t lba, uint16_t count, unsigned char *buffer)
708 {
709     SCSI_PASS_THROUGH_DIRECT sptd = {0};
710     DWORD bytesReturned;
711
712     sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
713     sptd.CdbLength = 10;
714     sptd.DataIn = 1;
715     sptd.DataTransferLength = count * 2048;
716     sptd.TimeOutValue = 30;
717     sptd.DataBuffer = buffer;
718
719     sptd.Cdb[0] = 0x28; // READ(10)
720     sptd.Cdb[2] = (lba >> 24) & 0xFF;
721     sptd.Cdb[3] = (lba >> 16) & 0xFF;
722     sptd.Cdb[4] = (lba >> 8) & 0xFF;
723     sptd.Cdb[5] = lba & 0xFF;
724     sptd.Cdb[7] = (unsigned char)((count >> 8) & 0xFF);
725     sptd.Cdb[8] = (unsigned char)(count & 0xFF);
726
727     return DeviceIoControl(hDevice, IOCTL_SCSI_PASS_THROUGH_DIRECT, &sptd, sizeof(sptd), &sptd, sizeof(sptd), &bytesReturned, NULL);
728 }
729
730 XboxGameInfo GetXboxGameInfo(HANDLE hDevice)
731 {
732     XboxGameInfo info;
733     memset(&info, 0, sizeof(XboxGameInfo));
734     unsigned char sectorBuffer[2048];
735
736     // Get Volume Descriptor (LBA 0x20)
737     if (!ScsiReadSectors(hDevice, 0x20, 1, sectorBuffer))
738         return info;
739
740     // Verify XDFS Magic "XGD2" or "MICROSOFT*XBOX*MEDIA"
741     if (memcmp(sectorBuffer, "MICROSOFT", 9) != 0)
742     {
743         return (XboxGameInfo){.TitleName = "Not_XDFS"};
744     }
745
746     uint32_t rootLba = *(uint32_t *)&sectorBuffer[0x14];
747     uint32_t rootSize = *(uint32_t *)&sectorBuffer[0x18];
748
749     uint32_t rawVolumeSize = *(uint32_t *)&sectorBuffer[0x1C];
750     // Assign to the 64-bit member (cast to ensure no weird sign extension)
751     info.TotalSizeBytes = (uint64_t)rawVolumeSize;
752
753     // Read Root Directory (Scanning multiple sectors for default.xbe)
754     uint32_t sectorsToRead = (rootSize + 2047) / 2048;
755     for (uint32_t s = 0; s < sectorsToRead; s++)
756     {
757         unsigned char dirBuffer[2048];
758         if (!ScsiReadSectors(hDevice, rootLba + s, 1, dirBuffer))
759             break;
760
761         uint32_t offset = 0;
762         while (offset < 2030)
763         {
764             uint16_t leftNode = *(uint16_t *)&dirBuffer[offset];
765             if (leftNode == 0xFFFF)
766                 break; // End of directory
767
768             uint32_t startLba = *(uint32_t *)&dirBuffer[offset + 4];
769             uint8_t nameLen = dirBuffer[offset + 13];
770             char *name = (char *)&dirBuffer[offset + 14];
771
772             if (nameLen == 0)
773                 break;
774
775             // Match "default.xbe"
776             if (nameLen == 11 && _strnicmp(name, "default.xbe", 11) == 0)
777             {
778                 unsigned char xbeHeader[2048];
779                 if (ScsiReadSectors(hDevice, startLba, 1, xbeHeader))
780                 {
781
782                     if (*(uint32_t *)xbeHeader != 0x48454258)
783                         break; // "XBEH"
784
785                     // 4. Locate Certificate
786                     uint32_t baseVA = *(uint32_t *)&xbeHeader[0x104];
787                     uint32_t certVA = *(uint32_t *)&xbeHeader[0x118];
788                     uint32_t fileOffset = certVA - baseVA;
789
790                     // Certificate might be in a later sector of the XBE file
791                     uint32_t certSector = startLba + (fileOffset / 2048);
792                     uint32_t innerOff = (fileOffset % 2048);
793
794                     unsigned char certBuffer[2048];
795                     if (ScsiReadSectors(hDevice, certSector, 1, certBuffer))
796                     {
797
798                         // Populate the Struct from the Certificate
799                         info.TitleId = *(uint32_t *)&certBuffer[innerOff + 0x008];
800                         info.AllowedMedia = *(uint32_t *)&certBuffer[innerOff + 0x09C];
801                         info.GameRegion = *(uint32_t *)&certBuffer[innerOff + 0x0A0];
802                         info.GameRatings = *(uint32_t *)&certBuffer[innerOff + 0x0A4];
803                         info.DiscNumber = *(uint32_t *)&certBuffer[innerOff + 0x0A8];
804                         info.Version = *(uint32_t *)&certBuffer[innerOff + 0x0AC];
805
806                         // Convert UTF-16 Title Name (at 0x00C) to ASCII
807                         for (int i = 0; i < 40; i++)
808                         {
809                             char c = certBuffer[innerOff + 0x00C + (i * 2)];
810                             if (c == 0)
811                                 break;
812                             info.TitleName[i] = c;
813                         }
814
815                         info.Success = 1;
816                         return info;
817                     }
818                 }
819             }
820             offset += (14 + nameLen + 3) & ~3; // XDFS Alignment
821         }
822     }
823
824     return info; // Success will be 0 if we never found default.xbe or failed to read the cert
825 }
826
827 void DisplayXboxGameInfo(XboxGameInfo info)
828 {
829     if (!info.Success)
830     {
831         printf("Error: Could not retrieve Xbox game information.\n");
832         return;
833     }
834
835     printf("\n--- Xbox Game Information ---\n");
836     printf("Title Name:    %s\n", info.TitleName);
837     printf("Title ID:      0x%08X\n", info.TitleId);
838     printf("Version:       %u\n", info.Version);
839     printf("Disc Number:   %u\n", info.DiscNumber);
840
841     // Decode Regions
842     printf("Regions:       ");
843     if (info.GameRegion & XB_REGION_MANUFACTURING)
844         printf("[Manufacturing] ");
845     if (info.GameRegion & XB_REGION_US_CANADA)
846         printf("North America ");
847     if (info.GameRegion & XB_REGION_JAPAN)
848         printf("Japan ");
849     if (info.GameRegion & XB_REGION_EUROPE_AU_NZ)
850         printf("Europe/AU ");
851     if (info.GameRegion & XB_REGION_REST_OF_WORLD)
852         printf("Rest of World ");
853
854     // If everything is set (0x7FFFFFFF or 0xFFFFFFFF), it's Region Free
855     if ((info.GameRegion & 0x7FFFFFFF) == 0x7FFFFFFF)
856     {
857         printf("(Region Free)");
858     }
859     else if (info.GameRegion == 0)
860     {
861         printf("None (Locked)");
862     }
863     printf("\n");
864
865     // Decode Media Types
866     printf("Allowed Media: ");
867     if (info.AllowedMedia & XB_MEDIA_HARD_DRIVE)
868         printf("HDD ");
869     if (info.AllowedMedia & XB_MEDIA_DVD_X2)
870         printf("Xbox_DVD ");
871     if (info.AllowedMedia & XB_MEDIA_DVD_5_RO)
872         printf("DVD-5 ");
873     if (info.AllowedMedia & XB_MEDIA_DVD_9_RO)
874         printf("DVD-9 ");
875     if (info.AllowedMedia & XB_MEDIA_CD)
876         printf("CD ");
877     if (info.AllowedMedia & XB_MEDIA_DONGLE)
878         printf("Memory_Unit ");
879     printf("\n");
880
881     DisplayXboxRating(info.GameRatings);
882
883     printf("-----------------------------\n");
884 }
885
886 void DisplayXboxRating(uint32_t ratings)
887 {
888     // ESRB (North America) - Byte 0 (Bits 0-7)
889     uint8_t esrb = (uint8_t)(ratings & 0xFF);
890     if (esrb != 0 && esrb != 0xFF)
891     {
892         printf("ESRB Rating:   ");
893         switch (esrb)
894         {
895         case 0x01:
896             printf("EC (Early Childhood)\n");
897             break;
898         case 0x02:
899             printf("E (Everyone)\n");
900             break;
901         case 0x03:
902             printf("K-A (Kids to Adults)\n");
903             break;
904         case 0x04:
905             printf("T (Teen)\n");
906             break;
907         case 0x05:
908             printf("M (Mature)\n");
909             break;
910         case 0x06:
911             printf("AO (Adults Only)\n");
912             break;
913         default:
914             printf("RP (Rating Pending/Unrated)\n");
915             break;
916         }
917     }
918
919     // PEGI (Europe) - Byte 1 (Bits 8-15)
920     uint8_t pegi = (uint8_t)((ratings >> 8) & 0xFF);
921     if (pegi != 0 && pegi != 0xFF)
922     {
923         printf("PEGI Rating:   ");
924         switch (pegi)
925         {
926         case 0x00:
927             printf("3+\n");
928             break;
929         case 0x01:
930             printf("7+\n");
931             break;
932         case 0x02:
933             printf("12+\n");
934             break;
935         case 0x03:
936             printf("16+\n");
937             break;
938         case 0x04:
939             printf("18+\n");
940             break;
941         default:
942             printf("Other (0x%02X)\n", pegi);
943             break;
944         }
945     }
946
947     // CERO (Japan) - Byte 2 (Bits 16-23)
948     uint8_t cero = (uint8_t)((ratings >> 16) & 0xFF);
949     if (cero != 0 && cero != 0xFF)
950     {
951         printf("CERO Rating:   ");
952         switch (cero)
953         {
954         case 0x00:
955             printf("A (All Ages)\n");
956             break;
957         case 0x01:
958             printf("B (12+)\n");
959             break;
960         case 0x02:
961             printf("C (15+)\n");
962             break;
963         case 0x03:
964             printf("D (17+)\n");
965             break;
966         case 0x04:
967             printf("Z (18+ Only)\n");
968             break;
969         default:
970             printf("Other (0x%02X)\n", cero);
971             break;
972         }
973     }
974
975     if ((ratings & 0x00FFFFFF) == 0)
976     {
977         printf("Rating:        None/Unrated\n");
978     }
979 }
980
981 // --- POST-DUMP VERIFICATION ---
982 void PrintGamePartitionHash(const char *filename)
983 {
984     FILE *f = fopen(filename, "rb");
985     __int64 fileBytes;
986     uint32_t startLba = START_LBA_MAGIC;
987     unsigned long long bytesRemaining = 0ULL;
988     unsigned long long totalBytesToHash = 0ULL;
989     unsigned long long bytesDone = 0ULL;
990     unsigned char *vBuf;
991     size_t read;
992     HCRYPTPROV hProv = 0;
993     HCRYPTHASH hHash = 0;
994     BYTE rgbHash[20];
995     DWORD cbHash = 20;
996     char finalHash[41] = {0};
997     DWORD startTick = 0;
998     DWORD lastPrintTick = 0;
999     DWORD nowTick = 0;
1000     DWORD elapsedMs = 0;
1001     DWORD etaMs = 0;
1002     char timeStr[12] = {0};
1003     char etaStr[12] = {0};
1004
1005     if (!f)
1006         return;
1007
1008     if (_fseeki64(f, 0, SEEK_END) != 0)
1009     {
1010         fclose(f);
1011         return;
1012     }
1013     fileBytes = _ftelli64(f);
1014     if (fileBytes < 0)
1015     {
1016         fclose(f);
1017         return;
1018     }
1019
1020     if ((unsigned long long)fileBytes == (unsigned long long)XGD1_FULL_REDUMP_SECTORS * 2048ULL)
1021     {
1022         startLba = XGD1_GAME_OUTPUT_START_LBA;
1023         bytesRemaining = (unsigned long long)REDUMP_SECTORS * 2048ULL;
1024         printf("[HASH] Calculating Game/XISO-region SHA-1 (Redump-style output LBA %u, %u sectors)...\n",
1025                startLba, REDUMP_SECTORS);
1026     }
1027     else
1028     {
1029         startLba = START_LBA_MAGIC;
1030         bytesRemaining = ((unsigned long long)fileBytes > (unsigned long long)startLba * 2048ULL)
1031                          ? ((unsigned long long)fileBytes - (unsigned long long)startLba * 2048ULL)
1032                          : 0ULL;
1033         printf("[HASH] Calculating Game-Partition-Only SHA-1 (legacy contiguous output LBA %u)...\n", startLba);
1034     }
1035
1036     totalBytesToHash = bytesRemaining;
1037     if (bytesRemaining == 0)
1038     {
1039         fclose(f);
1040         return;
1041     }
1042
1043     if (!CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
1044     {
1045         fclose(f);
1046         return;
1047     }
1048     if (!CryptCreateHash(hProv, CALG_SHA1, 0, 0, &hHash))
1049     {
1050         CryptReleaseContext(hProv, 0);
1051         fclose(f);
1052         return;
1053     }
1054
1055     _fseeki64(f, (__int64)startLba * 2048, SEEK_SET);
1056
1057     vBuf = (unsigned char *)malloc(1024 * 1024); // 1MB buffer
1058     if (!vBuf)
1059     {
1060         CryptDestroyHash(hHash);
1061         CryptReleaseContext(hProv, 0);
1062         fclose(f);
1063         return;
1064     }
1065
1066     startTick = GetTickCount();
1067     lastPrintTick = startTick;
1068
1069     while (bytesRemaining > 0 && (read = fread(vBuf, 1, (bytesRemaining > 1024ULL * 1024ULL) ? 1024 * 1024 : (size_t)bytesRemaining, f)) > 0)
1070     {
1071         CryptHashData(hHash, vBuf, (DWORD)read, 0);
1072         bytesRemaining -= read;
1073         bytesDone += (unsigned long long)read;
1074
1075         nowTick = GetTickCount();
1076         if (bytesDone >= totalBytesToHash || (nowTick - lastPrintTick) >= 1000)
1077         {
1078             double percent = ((double)bytesDone / (double)totalBytesToHash) * 100.0;
1079             double mbDone = (double)bytesDone / (1024.0 * 1024.0);
1080             double speed = 0.0;
1081             elapsedMs = nowTick - startTick;
1082             if (elapsedMs > 0)
1083                 speed = mbDone / ((double)elapsedMs / 1000.0);
1084             etaMs = (bytesDone > 0 && elapsedMs > 0)
1085                     ? (DWORD)(((double)elapsedMs / (double)bytesDone) * (double)(totalBytesToHash - bytesDone))
1086                     : 0;
1087             FormatElapsedTime(elapsedMs, timeStr);
1088             FormatElapsedTime(etaMs, etaStr);
1089             xbox_ref_console_printf("\r[HASH] Game/XISO-region: %5.1f%% | %.1f MB | Speed: %.2f MB/s | Time: %s | ETA: %s    ",
1090                    percent,
1091                    mbDone,
1092                    speed,
1093                    timeStr,
1094                    etaStr);
1095             fflush(stdout);
1096             lastPrintTick = nowTick;
1097         }
1098     }
1099
1100     CryptGetHashParam(hHash, HP_HASHVAL, rgbHash, &cbHash, 0);
1101     for (int i = 0; i < 20; i++)
1102         sprintf(&finalHash[i * 2], "%02x", rgbHash[i]);
1103
1104     elapsedMs = GetTickCount() - startTick;
1105     FormatElapsedTime(elapsedMs, timeStr);
1106     if (totalBytesToHash > 0)
1107         xbox_ref_console_printf("\r[HASH] Game/XISO-region: 100.0%% | %.1f MB | Time: %s                         \n",
1108                (double)totalBytesToHash / (1024.0 * 1024.0),
1109                timeStr);
1110     printf("Game/XISO-region SHA-1: %s\n", finalHash);
1111     printf("[OK] Game/XISO-region SHA-1 complete in %s.\n", timeStr);
1112
1113     free(vBuf);
1114     CryptDestroyHash(hHash);
1115     CryptReleaseContext(hProv, 0);
1116     fclose(f);
1117 }
1118
1119 void GetMediaID(HANDLE hDevice, char *outMediaId)
1120 {
1121     SCSI_PASS_THROUGH_DIRECT sptd = {0};
1122     unsigned char buffer[2048] = {0};
1123     DWORD bytesReturned;
1124
1125     sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
1126     sptd.CdbLength = 12;
1127     sptd.DataIn = SCSI_IOCTL_DATA_IN;
1128     sptd.DataTransferLength = 2048;
1129     sptd.TimeOutValue = 5;
1130     sptd.DataBuffer = buffer;
1131
1132     sptd.Cdb[0] = 0xAD; // READ DVD STRUCTURE
1133     sptd.Cdb[7] = 0x04; // Format: Disc Manufacturing Information (DMI)
1134     sptd.Cdb[8] = 0x08;
1135     sptd.Cdb[9] = 0x00;
1136
1137     if (DeviceIoControl(hDevice, IOCTL_SCSI_PASS_THROUGH_DIRECT, &sptd, sizeof(sptd), &sptd, sizeof(sptd), &bytesReturned, NULL))
1138     {
1139         // The Media ID is typically 32 bytes starting at offset 4 in the DMI
1140         // Offset 8 is where "MS11..." usually starts on Xbox discs
1141         // We'll grab 16 characters to be safe
1142         int writePos = 0;
1143         for (int i = 8; i < 24; i++)
1144         {
1145             // Only add alphanumeric characters to keep the filename clean
1146             if (isalnum(buffer[i]))
1147             {
1148                 outMediaId[writePos++] = buffer[i];
1149             }
1150         }
1151         outMediaId[writePos] = '\0'; // Null terminate the string
1152     }
1153     else
1154     {
1155         strcpy(outMediaId, "UNKNOWN_ID");
1156     }
1157 }
1158
1159 void GetDiscMetadata(HANDLE hDevice, uint32_t *totalSectors, bool *isDualLayer, XboxGameInfo *gameInfo)
1160 {
1161     SCSI_PASS_THROUGH_DIRECT sptd = {0};
1162     unsigned char buffer[2048] = {0};
1163     DWORD bytesReturned;
1164
1165     sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
1166     sptd.CdbLength = 12;
1167     sptd.DataIn = SCSI_IOCTL_DATA_IN;
1168     sptd.DataTransferLength = 2048;
1169     sptd.TimeOutValue = 10;
1170     sptd.DataBuffer = buffer;
1171
1172     sptd.Cdb[0] = 0xAD; // READ DVD STRUCTURE
1173     sptd.Cdb[7] = 0x00; // Physical Format Information
1174     sptd.Cdb[8] = 0x08; // 2048 bytes
1175     sptd.Cdb[9] = 0x00;
1176
1177     if (DeviceIoControl(hDevice, IOCTL_SCSI_PASS_THROUGH_DIRECT, &sptd, sizeof(sptd), &sptd, sizeof(sptd), &bytesReturned, NULL))
1178     {
1179
1180         // Byte 12: bits 5-6 (Number of Layers)
1181         // 0x20 = 00100000 (Two layers), 0x00 = 00000000 (One layer)
1182         unsigned char layerInfo = (buffer[12] >> 5) & 0x03;
1183         *isDualLayer = (layerInfo > 0);
1184
1185         // Bytes 13-15: End LBA
1186         uint32_t endLba = (buffer[13] << 16) | (buffer[14] << 8) | buffer[15];
1187
1188         // XBOX SANITY CHECK
1189         // If the drive reports a value much larger than a standard Xbox Dual Layer (3.4M sectors)
1190         // it means the drive is reporting the raw DVD-9 limit. We must cap it.
1191         if (endLba > ((uint32_t)REDUMP_SECTORS - 1))
1192         {
1193             printf("[!] Drive reported Raw DVD-9 geometry. Normalizing to Xbox Dual Layer...\n");
1194             if (gameInfo->TotalSizeBytes < LAYER_BREAK)
1195             {
1196                 printf("[!] Info: Game partition size is smaller than expected for a Dual Layer disc.\n");
1197             }
1198             *totalSectors = REDUMP_SECTORS;
1199             *isDualLayer = true;
1200         }
1201         else
1202         {
1203             printf("[!] Drive reported Raw DVD-5 geometry. Normalizing to Xbox Single Layer...\n");
1204             *totalSectors = endLba + 1;
1205             *isDualLayer = (endLba > (uint32_t)LAYER_THRESHOLD); // Standard threshold for SL vs DL
1206         }
1207     }
1208     else
1209     {
1210         // Fallback safety
1211         *isDualLayer = true;
1212         *totalSectors = REDUMP_SECTORS;
1213         printf("Media Info: Could not read PFI. Defaulting to Dual Layer.\n");
1214     }
1215 }
1216
1217 uint32_t GetGamePartitionSize(HANDLE hDevice, uint32_t totalDiscSectors, XDFS_VOLUME_DESCRIPTOR *vol)
1218 {
1219     uint32_t sectorsToRead = 0;
1220     if (totalDiscSectors > 3300000) 
1221     {
1222         // DUAL LAYER (XGD2) Calculation:
1223         // LBA 1,913,920 is the physical end of the usable XDFS area on retail DVD-9s.
1224         uint32_t xgd2EndLba = 1913920;
1225         sectorsToRead = xgd2EndLba - vol->RootLBA;
1226     }
1227     else
1228     {
1229         // SINGLE LAYER (XGD1 / Homebrew) Calculation:
1230         // On single layer discs, the header's VolumeSize is trustworthy.
1231         sectorsToRead = vol->VolumeSize / 2048;
1232     }
1233     return sectorsToRead;
1234 }
1235
1236 static BOOL ProbeXboxVolumeAt(HANDLE hDevice, uint32_t lba)
1237 {
1238     unsigned char sector[2048] = {0};
1239     return ScsiReadSectors(hDevice, lba, 1, sector) && memcmp(sector, "MICROSOFT", 9) == 0;
1240 }
1241
1242 static uint32_t DetectXboxVolumeStart(HANDLE hDevice)
1243 {
1244     if (ProbeXboxVolumeAt(hDevice, START_LBA_MAGIC))
1245         return START_LBA_MAGIC;
1246
1247     if (ProbeXboxVolumeAt(hDevice, 0x20))
1248         return 0x20;
1249
1250     return 0xFFFFFFFFu;
1251 }
1252
1253 static void GetDirectoryForPath(const char *filename, char *outDir, DWORD outDirSize)
1254 {
1255     DWORD len;
1256     char fullPath[MAX_PATH];
1257     char *filePart = NULL;
1258
1259     if (!outDir || outDirSize == 0)
1260         return;
1261
1262     outDir[0] = '\0';
1263
1264     if (!filename || filename[0] == '\0')
1265     {
1266         GetCurrentDirectoryA(outDirSize, outDir);
1267         return;
1268     }
1269
1270     len = GetFullPathNameA(filename, (DWORD)sizeof(fullPath), fullPath, &filePart);
1271     if (len == 0 || len >= sizeof(fullPath))
1272     {
1273         GetCurrentDirectoryA(outDirSize, outDir);
1274         return;
1275     }
1276
1277     if (filePart && filePart > fullPath)
1278     {
1279         size_t dirLen = (size_t)(filePart - fullPath);
1280         if (dirLen >= outDirSize)
1281             dirLen = outDirSize - 1;
1282         memcpy(outDir, fullPath, dirLen);
1283         outDir[dirLen] = '\0';
1284     }
1285     else
1286     {
1287         GetCurrentDirectoryA(outDirSize, outDir);
1288     }
1289 }
1290
1291 static BOOL FileExistsAndSize(const char *filename, unsigned long long *sizeOut)
1292 {
1293     WIN32_FILE_ATTRIBUTE_DATA fad;
1294
1295     if (sizeOut)
1296         *sizeOut = 0ULL;
1297
1298     if (!filename || !GetFileAttributesExA(filename, GetFileExInfoStandard, &fad))
1299         return FALSE;
1300
1301     if (fad.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
1302         return FALSE;
1303
1304     if (sizeOut)
1305     {
1306         ULARGE_INTEGER size;
1307         size.HighPart = fad.nFileSizeHigh;
1308         size.LowPart = fad.nFileSizeLow;
1309         *sizeOut = size.QuadPart;
1310     }
1311
1312     return TRUE;
1313 }
1314
1315 static BOOL CheckOutputFreeSpace(const char *filename, unsigned long long expectedBytes, const char *label)
1316 {
1317     char dir[MAX_PATH];
1318     ULARGE_INTEGER freeToCaller;
1319     ULARGE_INTEGER totalBytes;
1320     ULARGE_INTEGER totalFree;
1321     unsigned long long existingBytes = 0ULL;
1322     unsigned long long effectiveFree;
1323     unsigned long long margin;
1324
1325     if (expectedBytes == 0)
1326         return TRUE;
1327
1328     GetDirectoryForPath(filename, dir, (DWORD)sizeof(dir));
1329
1330     if (!GetDiskFreeSpaceExA(dir[0] ? dir : NULL, &freeToCaller, &totalBytes, &totalFree))
1331     {
1332         DWORD err = GetLastError();
1333         printf("\n[WARN] Could not check free space for output path '%s' (GetDiskFreeSpaceEx error %lu).\n", filename, err);
1334         printf("       Continuing, but write errors will still be caught during the dump.\n");
1335         return TRUE;
1336     }
1337
1338     FileExistsAndSize(filename, &existingBytes);
1339
1340     // If overwriting an existing output on the same volume, its current bytes can be
1341     // reclaimed by fopen(..., "wb"). This avoids rejecting a valid replacement run.
1342     effectiveFree = freeToCaller.QuadPart + existingBytes;
1343
1344     // Add a small safety margin for sidecar/profile files and filesystem metadata.
1345     // Keep this modest so overwriting an existing full raw ISO still passes.
1346     margin = 64ULL * 1024ULL * 1024ULL;
1347
1348     printf("[%s] Free-space preflight for '%s':\n", label ? label : "OUTPUT", filename);
1349     printf("       Required output bytes: %llu\n", expectedBytes);
1350     printf("       Safety margin:         %llu\n", margin);
1351     printf("       Free to caller:        %llu\n", (unsigned long long)freeToCaller.QuadPart);
1352     if (existingBytes)
1353         printf("       Existing output bytes: %llu (counted as reclaimable overwrite space)\n", existingBytes);
1354     printf("       Effective available:   %llu\n", effectiveFree);
1355
1356     if (effectiveFree < expectedBytes + margin)
1357     {
1358         printf("\n[FATAL] Not enough free disk space for %s.\n", label ? label : "output");
1359         printf("        Required + margin: %llu bytes\n", expectedBytes + margin);
1360         printf("        Effective free:    %llu bytes\n", effectiveFree);
1361         printf("        Free space can change while dumping; free extra space and rerun.\n");
1362         return FALSE;
1363     }
1364
1365     return TRUE;
1366 }
1367
1368 static BOOL WriteOutputBytes(FILE *outFile,
1369                              const void *data,
1370                              size_t bytesToWrite,
1371                              const char *phaseName,
1372                              uint32_t sourceLba,
1373                              uint32_t outputLba)
1374 {
1375     size_t written;
1376
1377     if (!outFile || !data || bytesToWrite == 0)
1378         return bytesToWrite == 0;
1379
1380     written = fwrite(data, 1, bytesToWrite, outFile);
1381     if (written != bytesToWrite)
1382     {
1383         printf("\n[FATAL] Output write failed during %s range.\n", phaseName ? phaseName : "dump");
1384         printf("        Source LBA: %u | Output LBA: %u\n", sourceLba, outputLba);
1385         printf("        Requested:  %llu bytes\n", (unsigned long long)bytesToWrite);
1386         printf("        Written:    %llu bytes\n", (unsigned long long)written);
1387         if (errno)
1388             printf("        errno:      %d (%s)\n", errno, strerror(errno));
1389         printf("        This commonly means another program consumed free space after preflight,\n");
1390         printf("        the destination volume filled up, or the destination became unavailable.\n");
1391         return FALSE;
1392     }
1393
1394     if (ferror(outFile))
1395     {
1396         printf("\n[FATAL] Output stream error during %s range at output LBA %u.\n",
1397                phaseName ? phaseName : "dump", outputLba);
1398         if (errno)
1399             printf("        errno: %d (%s)\n", errno, strerror(errno));
1400         return FALSE;
1401     }
1402
1403     return TRUE;
1404 }
1405
1406 static BOOL FlushAndCommitOutput(FILE *outFile, const char *label)
1407 {
1408     int fd;
1409
1410     if (!outFile)
1411         return FALSE;
1412
1413     if (fflush(outFile) != 0)
1414     {
1415         printf("\n[FATAL] fflush failed for %s output.\n", label ? label : "dump");
1416         if (errno)
1417             printf("        errno: %d (%s)\n", errno, strerror(errno));
1418         return FALSE;
1419     }
1420
1421     fd = _fileno(outFile);
1422     if (fd >= 0 && _commit(fd) != 0)
1423     {
1424         printf("\n[FATAL] _commit failed for %s output. The OS may not have accepted all buffered data.\n",
1425                label ? label : "dump");
1426         if (errno)
1427             printf("        errno: %d (%s)\n", errno, strerror(errno));
1428         return FALSE;
1429     }
1430
1431     return TRUE;
1432 }
1433
1434 static BOOL DumpSectorRangeWithRetry(HANDLE hDevice,
1435                                      FILE *outFile,
1436                                      HCRYPTHASH hHash,
1437                                      uint32_t sourceStartLba,
1438                                      uint32_t sectorsToRead,
1439                                      uint32_t outputBaseLba,
1440                                      const char *phaseName)
1441 {
1442     const uint32_t batchSize = 32;
1443     unsigned char *buffer = NULL;
1444     uint32_t sectorsDone = 0;
1445     DWORD startTime = GetTickCount();
1446     char timeStr[12] = {0};
1447     char etaStr[12] = {0};
1448
1449     if (sectorsToRead == 0)
1450         return TRUE;
1451
1452     buffer = (unsigned char *)VirtualAlloc(NULL, batchSize * 2048, MEM_COMMIT, PAGE_READWRITE);
1453     if (!buffer)
1454     {
1455         printf("\n[FATAL] Could not allocate dump buffer for %s range.\n", phaseName);
1456         return FALSE;
1457     }
1458
1459     printf("\n--- STARTING %s RANGE ---\n", phaseName);
1460     printf("Source LBA: %u | Output LBA: %u | Sectors: %u\n", sourceStartLba, outputBaseLba, sectorsToRead);
1461
1462     while (sectorsDone < sectorsToRead)
1463     {
1464         uint32_t currentLba;
1465
1466         if (XboxDumpCancelRequested())
1467         {
1468             printf("\n[CANCEL] User cancellation requested during %s at output LBA %u.\n",
1469                    phaseName, outputBaseLba + sectorsDone);
1470             VirtualFree(buffer, 0, MEM_RELEASE);
1471             return FALSE;
1472         }
1473
1474         currentLba = sourceStartLba + sectorsDone;
1475         const char *currentLayerStr = "L0";
1476         uint32_t burstLimit = batchSize;
1477         uint32_t toRead;
1478         BOOL success = FALSE;
1479
1480         if ((outputBaseLba + sectorsDone) >= LAYER_BREAK)
1481             currentLayerStr = "L1";
1482
1483         // Layer-boundary safety: do not let one READ(10) span the Xbox layer break.
1484         if (sectorsDone == 0 || (outputBaseLba + sectorsDone) == LAYER_BREAK)
1485         {
1486             burstLimit = 1;
1487         }
1488         else if ((outputBaseLba + sectorsDone) < LAYER_BREAK &&
1489                  (outputBaseLba + sectorsDone + batchSize) > LAYER_BREAK)
1490         {
1491             burstLimit = LAYER_BREAK - (outputBaseLba + sectorsDone);
1492         }
1493
1494         toRead = (sectorsToRead - sectorsDone > burstLimit) ? burstLimit : (sectorsToRead - sectorsDone);
1495
1496         if ((outputBaseLba + sectorsDone) == LAYER_BREAK)
1497         {
1498             printf("\n[INFO] Redump/XGD1 output layer break at LBA %u. Reducing burst size to 1 sector for safety.\n", LAYER_BREAK);
1499         }
1500
1501         for (int retry = 0; retry <= MAX_RETRIES; retry++)
1502         {
1503             if (XboxDumpCancelRequested())
1504             {
1505                 printf("\n[CANCEL] User cancellation requested during %s retry at output LBA %u.\n",
1506                        phaseName, outputBaseLba + sectorsDone);
1507                 VirtualFree(buffer, 0, MEM_RELEASE);
1508                 return FALSE;
1509             }
1510
1511             if (ScsiReadSectors(hDevice, currentLba, (uint16_t)toRead, buffer))
1512             {
1513                 if (!WriteOutputBytes(outFile, buffer, (size_t)toRead * 2048U, phaseName, currentLba, outputBaseLba + sectorsDone))
1514                 {
1515                     VirtualFree(buffer, 0, MEM_RELEASE);
1516                     return FALSE;
1517                 }
1518                 CryptHashData(hHash, buffer, toRead * 2048, 0);
1519                 sectorsDone += toRead;
1520                 success = TRUE;
1521                 break;
1522             }
1523
1524             Sleep(500);
1525         }
1526
1527         if (!success)
1528         {
1529             unsigned char *smallBuffer = NULL;
1530             printf("\n[!] Batch failed in %s range at source LBA %u. Recovering sectors individually.\n", phaseName, currentLba);
1531
1532             smallBuffer = (unsigned char *)VirtualAlloc(NULL, 2048, MEM_COMMIT, PAGE_READWRITE);
1533             if (!smallBuffer)
1534             {
1535                 printf("\n[FATAL] Could not allocate single-sector recovery buffer.\n");
1536                 VirtualFree(buffer, 0, MEM_RELEASE);
1537                 return FALSE;
1538             }
1539
1540             for (uint32_t i = 0; i < toRead; i++)
1541             {
1542                 BOOL sectorSuccess = FALSE;
1543
1544                 for (int sRetry = 0; sRetry <= MAX_RETRIES; sRetry++)
1545                 {
1546                     if (XboxDumpCancelRequested())
1547                     {
1548                         printf("\n[CANCEL] User cancellation requested during %s sector recovery at output LBA %u.\n",
1549                                phaseName, outputBaseLba + sectorsDone);
1550                         VirtualFree(smallBuffer, 0, MEM_RELEASE);
1551                         VirtualFree(buffer, 0, MEM_RELEASE);
1552                         return FALSE;
1553                     }
1554
1555                     if (ScsiReadSectors(hDevice, currentLba + i, 1, smallBuffer))
1556                     {
1557                         if (!WriteOutputBytes(outFile, smallBuffer, 2048, phaseName, currentLba + i, outputBaseLba + sectorsDone))
1558                         {
1559                             VirtualFree(smallBuffer, 0, MEM_RELEASE);
1560                             VirtualFree(buffer, 0, MEM_RELEASE);
1561                             return FALSE;
1562                         }
1563                         CryptHashData(hHash, smallBuffer, 2048, 0);
1564                         sectorsDone++;
1565                         sectorSuccess = TRUE;
1566                         break;
1567                     }
1568
1569                     Sleep(500);
1570                 }
1571
1572                 if (!sectorSuccess)
1573                 {
1574                     printf("\n[FATAL] Unrecoverable %s sector at source LBA %u. Output hash is invalid.\n", phaseName, currentLba + i);
1575                     VirtualFree(smallBuffer, 0, MEM_RELEASE);
1576                     VirtualFree(buffer, 0, MEM_RELEASE);
1577                     return FALSE;
1578                 }
1579             }
1580
1581             VirtualFree(smallBuffer, 0, MEM_RELEASE);
1582         }
1583
1584         if (sectorsDone > 0)
1585         {
1586             DWORD elapsedMs = GetTickCount() - startTime;
1587             uint32_t sectorsLeft = sectorsToRead - sectorsDone;
1588             DWORD etaMs = (DWORD)(((double)elapsedMs / sectorsDone) * sectorsLeft);
1589             float percent = ((float)sectorsDone / sectorsToRead) * 100.0f;
1590             float mbDone = (float)(outputBaseLba + sectorsDone) * 2048 / 1024 / 1024;
1591             float speed = (elapsedMs > 0) ? (((float)sectorsDone * 2048 / 1024 / 1024) / (elapsedMs / 1000.0f)) : 0.0f;
1592
1593             FormatElapsedTime(elapsedMs, timeStr);
1594             FormatElapsedTime(etaMs, etaStr);
1595
1596             xbox_ref_console_printf("\rProgress [%s/%s]: %3.1f%% | %.1f MB | Speed: %.2f MB/s | sourceLba: %u | outputLba: %u | Time: %s | ETA: %s    ",
1597                    phaseName, currentLayerStr, percent, mbDone, speed, currentLba, outputBaseLba + sectorsDone, timeStr, etaStr);
1598             fflush(stdout);
1599         }
1600     }
1601
1602     printf("\n[OK] Completed %s range.\n", phaseName);
1603     VirtualFree(buffer, 0, MEM_RELEASE);
1604     return TRUE;
1605 }
1606
1607
1608
1609
1610 static BOOL WriteZeroSectorsOutput(FILE *outFile,
1611                                    HCRYPTHASH hHash,
1612                                    uint32_t sectorCount,
1613                                    uint32_t outputBaseLba,
1614                                    const char *phaseName)
1615 {
1616     const uint32_t batchSectors = 32;
1617     unsigned char *zeroBuffer = NULL;
1618     uint32_t sectorsDone = 0;
1619     DWORD startTime = GetTickCount();
1620     char timeStr[12] = {0};
1621     char etaStr[12] = {0};
1622
1623     if (sectorCount == 0)
1624         return TRUE;
1625
1626     zeroBuffer = (unsigned char *)VirtualAlloc(NULL, batchSectors * 2048, MEM_COMMIT, PAGE_READWRITE);
1627     if (!zeroBuffer)
1628     {
1629         printf("\n[FATAL] Could not allocate zero-fill buffer for %s range.\n", phaseName ? phaseName : "padding");
1630         return FALSE;
1631     }
1632     memset(zeroBuffer, 0, batchSectors * 2048);
1633
1634     printf("\n--- STARTING %s RANGE ---\n", phaseName ? phaseName : "ZERO");
1635     printf("Output LBA: %u | Sectors: %u | Fill: synthetic zero-fill (not drive-captured)\n", outputBaseLba, sectorCount);
1636
1637     while (sectorsDone < sectorCount)
1638     {
1639         uint32_t toWrite;
1640
1641         if (XboxDumpCancelRequested())
1642         {
1643             printf("\n[CANCEL] User cancellation requested during %s at output LBA %u.\n",
1644                    phaseName ? phaseName : "ZERO", outputBaseLba + sectorsDone);
1645             VirtualFree(zeroBuffer, 0, MEM_RELEASE);
1646             return FALSE;
1647         }
1648
1649         toWrite = (sectorCount - sectorsDone > batchSectors) ? batchSectors : (sectorCount - sectorsDone);
1650         uint32_t outputLba = outputBaseLba + sectorsDone;
1651
1652         if (!WriteOutputBytes(outFile, zeroBuffer, (size_t)toWrite * 2048U, phaseName ? phaseName : "ZERO", 0, outputLba))
1653         {
1654             VirtualFree(zeroBuffer, 0, MEM_RELEASE);
1655             return FALSE;
1656         }
1657         if (hHash)
1658             CryptHashData(hHash, zeroBuffer, toWrite * 2048, 0);
1659
1660         sectorsDone += toWrite;
1661
1662         if (sectorsDone > 0)
1663         {
1664             DWORD elapsedMs = GetTickCount() - startTime;
1665             uint32_t sectorsLeft = sectorCount - sectorsDone;
1666             DWORD etaMs = (DWORD)(((double)elapsedMs / sectorsDone) * sectorsLeft);
1667             float percent = ((float)sectorsDone / sectorCount) * 100.0f;
1668             float mbDone = (float)(outputBaseLba + sectorsDone) * 2048 / 1024 / 1024;
1669             float speed = (elapsedMs > 0) ? (((float)sectorsDone * 2048 / 1024 / 1024) / (elapsedMs / 1000.0f)) : 0.0f;
1670
1671             FormatElapsedTime(elapsedMs, timeStr);
1672             FormatElapsedTime(etaMs, etaStr);
1673             xbox_ref_console_printf("\rProgress [%s]: %3.1f%% | %.1f MB | Speed: %.2f MB/s | outputLba: %u | Time: %s | ETA: %s    ",
1674                    phaseName ? phaseName : "ZERO", percent, mbDone, speed, outputBaseLba + sectorsDone, timeStr, etaStr);
1675             fflush(stdout);
1676         }
1677     }
1678
1679     printf("\n[OK] Completed %s range.\n", phaseName ? phaseName : "ZERO");
1680     VirtualFree(zeroBuffer, 0, MEM_RELEASE);
1681     return TRUE;
1682 }
1683
1684 static BOOL ReadSectorsToMemory(HANDLE hDevice,
1685                                 uint32_t sourceStartLba,
1686                                 uint32_t sectorCount,
1687                                 unsigned char *outBuffer,
1688                                 const char *phaseName)
1689 {
1690     const uint32_t batchSectors = 32;
1691     uint32_t sectorsDone = 0;
1692
1693     if (sectorCount == 0)
1694         return TRUE;
1695     if (!outBuffer)
1696         return FALSE;
1697
1698     printf("[RAW] Capturing %s to memory: source LBA %u..%u (%u sectors).\n",
1699            phaseName ? phaseName : "sector range",
1700            sourceStartLba,
1701            sourceStartLba + sectorCount - 1,
1702            sectorCount);
1703
1704     while (sectorsDone < sectorCount)
1705     {
1706         uint32_t toRead = (sectorCount - sectorsDone > batchSectors) ? batchSectors : (sectorCount - sectorsDone);
1707         uint32_t currentLba = sourceStartLba + sectorsDone;
1708         BOOL success = FALSE;
1709
1710         for (int retry = 0; retry <= MAX_RETRIES; retry++)
1711         {
1712             if (ScsiReadSectors(hDevice, currentLba, (uint16_t)toRead, outBuffer + ((size_t)sectorsDone * 2048U)))
1713             {
1714                 sectorsDone += toRead;
1715                 success = TRUE;
1716                 break;
1717             }
1718             Sleep(500);
1719         }
1720
1721         if (!success)
1722         {
1723             printf("\n[FATAL] Could not capture %s at source LBA %u.\n", phaseName ? phaseName : "sector range", currentLba);
1724             return FALSE;
1725         }
1726     }
1727
1728     return TRUE;
1729 }
1730
1731 static BOOL WriteMemorySectorsOutput(FILE *outFile,
1732                                      HCRYPTHASH hHash,
1733                                      const unsigned char *buffer,
1734                                      uint32_t sectorCount,
1735                                      uint32_t outputBaseLba,
1736                                      const char *phaseName)
1737 {
1738     const uint32_t batchSectors = 32;
1739     uint32_t sectorsDone = 0;
1740
1741     if (sectorCount == 0)
1742         return TRUE;
1743     if (!buffer)
1744         return FALSE;
1745
1746     printf("\n--- STARTING %s RANGE ---\n", phaseName ? phaseName : "MEMORY");
1747     printf("Output LBA: %u | Sectors: %u | Source: captured memory\n", outputBaseLba, sectorCount);
1748
1749     while (sectorsDone < sectorCount)
1750     {
1751         uint32_t toWrite = (sectorCount - sectorsDone > batchSectors) ? batchSectors : (sectorCount - sectorsDone);
1752         uint32_t outputLba = outputBaseLba + sectorsDone;
1753         const unsigned char *src = buffer + ((size_t)sectorsDone * 2048U);
1754
1755         if (!WriteOutputBytes(outFile, src, (size_t)toWrite * 2048U, phaseName ? phaseName : "MEMORY", 0, outputLba))
1756             return FALSE;
1757         if (hHash)
1758             CryptHashData(hHash, src, toWrite * 2048, 0);
1759
1760         sectorsDone += toWrite;
1761     }
1762
1763     printf("[OK] Completed %s range.\n", phaseName ? phaseName : "MEMORY");
1764     return TRUE;
1765 }
1766
1767 typedef struct _XboxDvdSidecarCapture
1768 {
1769     BOOL hasLockedCapacity;
1770     BOOL hasLockedModeSense3E;
1771     BOOL hasUnlockedCapacity;
1772     BOOL hasUnlockedModeSense3E;
1773     BOOL hasAdC0;
1774     BOOL hasPfi;
1775     BOOL hasDmi;
1776
1777     unsigned char lockedCapacity[8];
1778     unsigned char lockedModeSense3E[28];
1779     unsigned char unlockedCapacity[8];
1780     unsigned char unlockedModeSense3E[28];
1781
1782     unsigned char adC0[0x664];
1783     unsigned char pfi[2048];
1784     unsigned char dmi[2048];
1785 } XboxDvdSidecarCapture;
1786
1787 static void StripKnownExtension(const char *filename, char *outBase, size_t outBaseSize)
1788 {
1789     char *dot;
1790     char *slash1;
1791     char *slash2;
1792     char *slash;
1793
1794     if (!outBase || outBaseSize == 0)
1795         return;
1796
1797     outBase[0] = '\0';
1798     if (!filename)
1799         return;
1800
1801     strncpy(outBase, filename, outBaseSize - 1);
1802     outBase[outBaseSize - 1] = '\0';
1803
1804     dot = strrchr(outBase, '.');
1805     slash1 = strrchr(outBase, '\\');
1806     slash2 = strrchr(outBase, '/');
1807     slash = slash1 > slash2 ? slash1 : slash2;
1808
1809     if (dot && (!slash || dot > slash))
1810         *dot = '\0';
1811 }
1812
1813 static void MakeSidecarPath(const char *filename, const char *suffix, char *outPath, size_t outPathSize)
1814 {
1815     char base[MAX_PATH];
1816
1817     if (!outPath || outPathSize == 0)
1818         return;
1819
1820     StripKnownExtension(filename, base, sizeof(base));
1821     snprintf(outPath, outPathSize, "%s%s", base, suffix);
1822     outPath[outPathSize - 1] = '\0';
1823 }
1824
1825 static BOOL WriteBinaryFile(const char *path, const unsigned char *data, size_t len)
1826 {
1827     FILE *f;
1828
1829     if (!path || !data || len == 0)
1830         return FALSE;
1831
1832     f = fopen(path, "wb");
1833     if (!f)
1834         return FALSE;
1835
1836     if (fwrite(data, 1, len, f) != len)
1837     {
1838         fclose(f);
1839         return FALSE;
1840     }
1841
1842     fclose(f);
1843     return TRUE;
1844 }
1845
1846 static void JsonWriteEscapedString(FILE *f, const char *s)
1847 {
1848     fputc('"', f);
1849     if (s)
1850     {
1851         while (*s)
1852         {
1853             unsigned char c = (unsigned char)*s++;
1854             if (c == '"' || c == '\\')
1855             {
1856                 fputc('\\', f);
1857                 fputc(c, f);
1858             }
1859             else if (c == '\n')
1860             {
1861                 fputs("\\n", f);
1862             }
1863             else if (c == '\r')
1864             {
1865                 fputs("\\r", f);
1866             }
1867             else if (c == '\t')
1868             {
1869                 fputs("\\t", f);
1870             }
1871             else if (c < 0x20)
1872             {
1873                 fprintf(f, "\\u%04x", c);
1874             }
1875             else
1876             {
1877                 fputc(c, f);
1878             }
1879         }
1880     }
1881     fputc('"', f);
1882 }
1883
1884 static void JsonWriteHexString(FILE *f, const unsigned char *data, size_t len)
1885 {
1886     fputc('"', f);
1887     if (data)
1888     {
1889         for (size_t i = 0; i < len; i++)
1890             fprintf(f, "%02X", data[i]);
1891     }
1892     fputc('"', f);
1893 }
1894
1895
1896 static const char *PathLeaf(const char *path)
1897 {
1898     const char *slash1;
1899     const char *slash2;
1900
1901     if (!path)
1902         return "";
1903
1904     slash1 = strrchr(path, '\\');
1905     slash2 = strrchr(path, '/');
1906
1907     if (slash1 && slash2)
1908         return (slash1 > slash2 ? slash1 : slash2) + 1;
1909     if (slash1)
1910         return slash1 + 1;
1911     if (slash2)
1912         return slash2 + 1;
1913     return path;
1914 }
1915
1916 static void TrimTrailingSpaces(char *s)
1917 {
1918     size_t len;
1919
1920     if (!s)
1921         return;
1922
1923     len = strlen(s);
1924     while (len > 0 && (s[len - 1] == ' ' || s[len - 1] == '\t'))
1925     {
1926         s[len - 1] = '\0';
1927         len--;
1928     }
1929 }
1930
1931 static void CopyBounded(char *dst, size_t dstSize, const char *src, size_t srcLen)
1932 {
1933     size_t n;
1934
1935     if (!dst || dstSize == 0)
1936         return;
1937
1938     dst[0] = '\0';
1939     if (!src)
1940         return;
1941
1942     n = srcLen;
1943     if (n >= dstSize)
1944         n = dstSize - 1;
1945
1946     memcpy(dst, src, n);
1947     dst[n] = '\0';
1948 }
1949
1950 static void ExtractMediaProfileNames(const char *isoFilename,
1951                                      char *titleHint,
1952                                      size_t titleHintSize,
1953                                      char *mediaId,
1954                                      size_t mediaIdSize)
1955 {
1956     char base[MAX_PATH];
1957     const char *leaf;
1958     const char *openBracket;
1959     const char *closeBracket;
1960
1961     if (titleHint && titleHintSize > 0)
1962         titleHint[0] = '\0';
1963     if (mediaId && mediaIdSize > 0)
1964         mediaId[0] = '\0';
1965
1966     if (!isoFilename)
1967         return;
1968
1969     StripKnownExtension(isoFilename, base, sizeof(base));
1970     leaf = PathLeaf(base);
1971
1972     openBracket = strrchr(leaf, '[');
1973     closeBracket = openBracket ? strchr(openBracket, ']') : NULL;
1974
1975     if (openBracket && closeBracket && closeBracket > openBracket)
1976     {
1977         CopyBounded(titleHint, titleHintSize, leaf, (size_t)(openBracket - leaf));
1978         CopyBounded(mediaId, mediaIdSize, openBracket + 1, (size_t)(closeBracket - openBracket - 1));
1979     }
1980     else
1981     {
1982         CopyBounded(titleHint, titleHintSize, leaf, strlen(leaf));
1983     }
1984
1985     TrimTrailingSpaces(titleHint);
1986 }
1987
1988 static void JsonWriteValidationWarnings(FILE *f, const XboxDvdSidecarCapture *cap, BOOL payloadFilesPresent, BOOL redumpStyleZeroFilledPadding)
1989 {
1990     BOOL wrote = FALSE;
1991
1992     fprintf(f, "[");
1993
1994 #define WRITE_WARNING(w) do { \
1995         if (wrote) fprintf(f, ", "); \
1996         JsonWriteEscapedString(f, (w)); \
1997         wrote = TRUE; \
1998     } while (0)
1999
2000     if (!cap || !cap->hasLockedCapacity)
2001         WRITE_WARNING("missing_locked_read_capacity_10");
2002     if (!cap || !cap->hasLockedModeSense3E)
2003         WRITE_WARNING("missing_locked_mode_sense_3e");
2004     if (!cap || !cap->hasUnlockedCapacity)
2005         WRITE_WARNING("missing_unlocked_read_capacity_10");
2006     if (!cap || !cap->hasUnlockedModeSense3E)
2007         WRITE_WARNING("missing_unlocked_mode_sense_3e");
2008     if (!cap || !cap->hasAdC0)
2009         WRITE_WARNING("missing_ad_c0_payload");
2010     if (!cap || !cap->hasPfi)
2011         WRITE_WARNING("missing_pfi_payload");
2012     if (!cap || !cap->hasDmi)
2013         WRITE_WARNING("missing_dmi_payload");
2014     if (!payloadFilesPresent)
2015         WRITE_WARNING("payload_files_not_fully_present");
2016     if (redumpStyleZeroFilledPadding)
2017         WRITE_WARNING("redump_style_padding_zero_filled_unresolved_content");
2018
2019 #undef WRITE_WARNING
2020
2021     fprintf(f, "]");
2022 }
2023
2024 static BOOL ScsiDataInCommand(HANDLE hDevice,
2025                               const unsigned char *cdb,
2026                               BYTE cdbLen,
2027                               DWORD dataLen,
2028                               unsigned char *buffer)
2029 {
2030     SCSI_PASS_THROUGH_DIRECT sptd;
2031     DWORD bytesReturned = 0;
2032
2033     if (!hDevice || !cdb || !buffer || dataLen == 0 || cdbLen == 0 || cdbLen > 16)
2034         return FALSE;
2035
2036     memset(&sptd, 0, sizeof(sptd));
2037     memset(buffer, 0, dataLen);
2038
2039     sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
2040     sptd.CdbLength = cdbLen;
2041     sptd.DataIn = SCSI_IOCTL_DATA_IN;
2042     sptd.DataTransferLength = dataLen;
2043     sptd.TimeOutValue = 30;
2044     sptd.DataBuffer = buffer;
2045     memcpy(sptd.Cdb, cdb, cdbLen);
2046
2047     return DeviceIoControl(hDevice,
2048                            IOCTL_SCSI_PASS_THROUGH_DIRECT,
2049                            &sptd,
2050                            sizeof(sptd),
2051                            &sptd,
2052                            sizeof(sptd),
2053                            &bytesReturned,
2054                            NULL);
2055 }
2056
2057 static BOOL CaptureReadCapacity10(HANDLE hDevice, unsigned char out8[8])
2058 {
2059     static const unsigned char cdb[10] = {0x25, 0, 0, 0, 0, 0, 0, 0, 0, 0};
2060     return ScsiDataInCommand(hDevice, cdb, 10, 8, out8);
2061 }
2062
2063 static BOOL CaptureModeSense3E(HANDLE hDevice, unsigned char out28[28])
2064 {
2065     static const unsigned char cdb[10] = {0x5A, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00};
2066     return ScsiDataInCommand(hDevice, cdb, 10, 28, out28);
2067 }
2068
2069 static BOOL CaptureReadDvdStructureXboxC0(HANDLE hDevice, unsigned char out1664[0x664])
2070 {
2071     static const unsigned char cdb[12] = {0xAD, 0x00, 0xFF, 0x02, 0xFD, 0xFF, 0xFE, 0x00, 0x06, 0x64, 0x00, 0xC0};
2072     return ScsiDataInCommand(hDevice, cdb, 12, 0x664, out1664);
2073 }
2074
2075 static BOOL CaptureReadDvdStructurePfi(HANDLE hDevice, unsigned char out2048[2048])
2076 {
2077     static const unsigned char cdb[12] = {0xAD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00};
2078     return ScsiDataInCommand(hDevice, cdb, 12, 2048, out2048);
2079 }
2080
2081 static BOOL CaptureReadDvdStructureDmi(HANDLE hDevice, unsigned char out2048[2048])
2082 {
2083     static const unsigned char cdb[12] = {0xAD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x08, 0x00, 0x00, 0x00};
2084     return ScsiDataInCommand(hDevice, cdb, 12, 2048, out2048);
2085 }
2086
2087 static void CaptureLockedSidecarState(HANDLE hDevice, XboxDvdSidecarCapture *cap)
2088 {
2089     if (!cap)
2090         return;
2091
2092     cap->hasLockedCapacity = CaptureReadCapacity10(hDevice, cap->lockedCapacity);
2093     cap->hasLockedModeSense3E = CaptureModeSense3E(hDevice, cap->lockedModeSense3E);
2094
2095     printf("[META] Locked READ CAPACITY: %s\n", cap->hasLockedCapacity ? "captured" : "failed");
2096     printf("[META] Locked MODE SENSE 0x3E: %s\n", cap->hasLockedModeSense3E ? "captured" : "failed");
2097 }
2098
2099 static void CaptureUnlockedSidecarState(HANDLE hDevice, XboxDvdSidecarCapture *cap)
2100 {
2101     if (!cap)
2102         return;
2103
2104     cap->hasUnlockedCapacity = CaptureReadCapacity10(hDevice, cap->unlockedCapacity);
2105     cap->hasUnlockedModeSense3E = CaptureModeSense3E(hDevice, cap->unlockedModeSense3E);
2106     cap->hasAdC0 = CaptureReadDvdStructureXboxC0(hDevice, cap->adC0);
2107     cap->hasPfi = CaptureReadDvdStructurePfi(hDevice, cap->pfi);
2108     cap->hasDmi = CaptureReadDvdStructureDmi(hDevice, cap->dmi);
2109
2110     printf("[META] Unlocked READ CAPACITY: %s\n", cap->hasUnlockedCapacity ? "captured" : "failed");
2111     printf("[META] Unlocked MODE SENSE 0x3E: %s\n", cap->hasUnlockedModeSense3E ? "captured" : "failed");
2112     printf("[META] READ DVD STRUCTURE Xbox C0 block: %s\n", cap->hasAdC0 ? "captured" : "failed");
2113     printf("[META] READ DVD STRUCTURE PFI: %s\n", cap->hasPfi ? "captured" : "failed");
2114     printf("[META] READ DVD STRUCTURE DMI: %s\n", cap->hasDmi ? "captured" : "failed");
2115 }
2116
2117 static uint32_t CapacitySectorsFromReadCapacity10(const unsigned char data[8])
2118 {
2119     uint32_t maxLba;
2120
2121     if (!data)
2122         return 0;
2123
2124     maxLba = ((uint32_t)data[0] << 24) |
2125              ((uint32_t)data[1] << 16) |
2126              ((uint32_t)data[2] << 8) |
2127              ((uint32_t)data[3]);
2128     return maxLba + 1;
2129 }
2130
2131
2132 static uint32_t NormalizeRawIsoTargetSectors(uint32_t reportedSectors, BOOL isDualLayer)
2133 {
2134     // Option 1 targets a Redump-style reconstructed 2048-byte-sector image.
2135     // The GDR-8050L's unlocked READ CAPACITY reports the game/XISO view length
2136     // (3,431,264 sectors), while the full Original Xbox/XGD1 reconstructed image
2137     // is larger (3,820,880 sectors) because it also includes video L0/L1 and
2138     // padding around the game region.
2139     if (isDualLayer || reportedSectors > LAYER_THRESHOLD)
2140         return XGD1_FULL_REDUMP_SECTORS;
2141
2142     return reportedSectors;
2143 }
2144
2145 static unsigned long long GetFileSizeBytes64(const char *filename)
2146 {
2147     FILE *f;
2148     __int64 pos;
2149
2150     if (!filename)
2151         return 0ULL;
2152
2153     f = fopen(filename, "rb");
2154     if (!f)
2155         return 0ULL;
2156
2157     if (_fseeki64(f, 0, SEEK_END) != 0)
2158     {
2159         fclose(f);
2160         return 0ULL;
2161     }
2162
2163     pos = _ftelli64(f);
2164     fclose(f);
2165
2166     if (pos < 0)
2167         return 0ULL;
2168
2169     return (unsigned long long)pos;
2170 }
2171
2172 static BOOL VerifyOutputByteCount(const char *filename, unsigned long long expectedBytes, const char *label)
2173 {
2174     unsigned long long actualBytes = GetFileSizeBytes64(filename);
2175
2176     if (expectedBytes == 0)
2177         return TRUE;
2178
2179     if (actualBytes != expectedBytes)
2180     {
2181         printf("\n[FATAL] %s byte-count mismatch.\n", label ? label : "Output");
2182         printf("        Expected: %llu bytes\n", expectedBytes);
2183         printf("        Actual:   %llu bytes\n", actualBytes);
2184         printf("        Refusing to mark this dump complete.\n");
2185         return FALSE;
2186     }
2187
2188     printf("[OK] %s byte count verified: %llu bytes.\n", label ? label : "Output", actualBytes);
2189     return TRUE;
2190 }
2191
2192 static void JsonWriteNull(FILE *f)
2193 {
2194     fprintf(f, "null");
2195 }
2196
2197 static void HexBytesToString(const BYTE *bytes, DWORD byteCount, char *outHex, size_t outHexSize)
2198 {
2199     DWORD i;
2200
2201     if (!outHex || outHexSize == 0)
2202         return;
2203
2204     outHex[0] = '\0';
2205     if (!bytes || outHexSize < ((size_t)byteCount * 2U + 1U))
2206         return;
2207
2208     for (i = 0; i < byteCount; i++)
2209         sprintf(&outHex[i * 2], "%02x", bytes[i]);
2210 }
2211
2212 static DWORD Crc32Update(DWORD crc, const unsigned char *buf, size_t len)
2213 {
2214     static DWORD table[256];
2215     static BOOL tableReady = FALSE;
2216     size_t i;
2217
2218     if (!tableReady)
2219     {
2220         DWORD n;
2221         for (n = 0; n < 256; n++)
2222         {
2223             DWORD c = n;
2224             int k;
2225             for (k = 0; k < 8; k++)
2226                 c = (c & 1U) ? (0xEDB88320U ^ (c >> 1)) : (c >> 1);
2227             table[n] = c;
2228         }
2229         tableReady = TRUE;
2230     }
2231
2232     for (i = 0; i < len; i++)
2233         crc = table[(crc ^ buf[i]) & 0xFFU] ^ (crc >> 8);
2234
2235     return crc;
2236 }
2237
2238 static BOOL CalculateFileHashes(const char *filename,
2239                                 char *outCrc32,
2240                                 size_t outCrc32Size,
2241                                 char *outMd5,
2242                                 size_t outMd5Size,
2243                                 char *outSha1,
2244                                 size_t outSha1Size,
2245                                 char *outSha256,
2246                                 size_t outSha256Size)
2247 {
2248     FILE *f;
2249     unsigned char *buf;
2250     HCRYPTPROV hProv = 0;
2251     HCRYPTHASH hMd5 = 0;
2252     HCRYPTHASH hSha1 = 0;
2253     HCRYPTHASH hSha256 = 0;
2254     DWORD crc = 0xFFFFFFFFU;
2255     BOOL ok = FALSE;
2256     size_t readBytes;
2257     unsigned long long totalBytes = 0ULL;
2258     unsigned long long doneBytes = 0ULL;
2259     DWORD startTick = 0;
2260     DWORD lastPrintTick = 0;
2261     DWORD nowTick = 0;
2262     DWORD elapsedMs = 0;
2263     DWORD etaMs = 0;
2264     char timeStr[12] = {0};
2265     char etaStr[12] = {0};
2266
2267     if (outCrc32 && outCrc32Size) outCrc32[0] = '\0';
2268     if (outMd5 && outMd5Size) outMd5[0] = '\0';
2269     if (outSha1 && outSha1Size) outSha1[0] = '\0';
2270     if (outSha256 && outSha256Size) outSha256[0] = '\0';
2271
2272     if (!filename)
2273         return FALSE;
2274
2275     totalBytes = GetFileSizeBytes64(filename);
2276
2277     f = fopen(filename, "rb");
2278     if (!f)
2279         return FALSE;
2280
2281     buf = (unsigned char *)malloc(1024 * 1024);
2282     if (!buf)
2283     {
2284         fclose(f);
2285         return FALSE;
2286     }
2287
2288     if (!CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT) &&
2289         !CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
2290         goto cleanup;
2291     if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hMd5))
2292         goto cleanup;
2293     if (!CryptCreateHash(hProv, CALG_SHA1, 0, 0, &hSha1))
2294         goto cleanup;
2295     if (!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hSha256))
2296         goto cleanup;
2297
2298     startTick = GetTickCount();
2299     lastPrintTick = startTick;
2300     printf("[HASH] Calculating full-file CRC32/MD5/SHA-1/SHA-256 for %s (%llu bytes)...\n",
2301            filename,
2302            totalBytes);
2303
2304     while ((readBytes = fread(buf, 1, 1024 * 1024, f)) > 0)
2305     {
2306         crc = Crc32Update(crc, buf, readBytes);
2307         if (!CryptHashData(hMd5, buf, (DWORD)readBytes, 0))
2308             goto cleanup;
2309         if (!CryptHashData(hSha1, buf, (DWORD)readBytes, 0))
2310             goto cleanup;
2311         if (!CryptHashData(hSha256, buf, (DWORD)readBytes, 0))
2312             goto cleanup;
2313
2314         doneBytes += (unsigned long long)readBytes;
2315         nowTick = GetTickCount();
2316         if (totalBytes > 0 && (doneBytes >= totalBytes || (nowTick - lastPrintTick) >= 1000))
2317         {
2318             double percent = ((double)doneBytes / (double)totalBytes) * 100.0;
2319             double mbDone = (double)doneBytes / (1024.0 * 1024.0);
2320             double speed = 0.0;
2321             unsigned long long bytesLeft = totalBytes - doneBytes;
2322
2323             elapsedMs = nowTick - startTick;
2324             if (elapsedMs > 0)
2325                 speed = mbDone / ((double)elapsedMs / 1000.0);
2326             etaMs = (doneBytes > 0 && elapsedMs > 0)
2327                     ? (DWORD)(((double)elapsedMs / (double)doneBytes) * (double)bytesLeft)
2328                     : 0;
2329             FormatElapsedTime(elapsedMs, timeStr);
2330             FormatElapsedTime(etaMs, etaStr);
2331             xbox_ref_console_printf("\r[HASH] Full-file: %5.1f%% | %.1f MB | Speed: %.2f MB/s | Time: %s | ETA: %s    ",
2332                    percent,
2333                    mbDone,
2334                    speed,
2335                    timeStr,
2336                    etaStr);
2337             fflush(stdout);
2338             lastPrintTick = nowTick;
2339         }
2340     }
2341
2342     if (ferror(f))
2343         goto cleanup;
2344
2345     elapsedMs = GetTickCount() - startTick;
2346     FormatElapsedTime(elapsedMs, timeStr);
2347     if (totalBytes > 0)
2348         xbox_ref_console_printf("\r[HASH] Full-file: 100.0%% | %.1f MB | Time: %s                         \n",
2349                (double)totalBytes / (1024.0 * 1024.0),
2350                timeStr);
2351     printf("[OK] Full-file CRC32/MD5/SHA-1/SHA-256 complete in %s.\n", timeStr);
2352
2353     crc ^= 0xFFFFFFFFU;
2354     if (outCrc32 && outCrc32Size >= 9)
2355         sprintf(outCrc32, "%08x", crc);
2356
2357     if (outMd5 && outMd5Size >= 33)
2358     {
2359         BYTE md5Bytes[16];
2360         DWORD md5Len = sizeof(md5Bytes);
2361         if (!CryptGetHashParam(hMd5, HP_HASHVAL, md5Bytes, &md5Len, 0))
2362             goto cleanup;
2363         HexBytesToString(md5Bytes, md5Len, outMd5, outMd5Size);
2364     }
2365
2366     if (outSha1 && outSha1Size >= 41)
2367     {
2368         BYTE sha1Bytes[20];
2369         DWORD sha1Len = sizeof(sha1Bytes);
2370         if (!CryptGetHashParam(hSha1, HP_HASHVAL, sha1Bytes, &sha1Len, 0))
2371             goto cleanup;
2372         HexBytesToString(sha1Bytes, sha1Len, outSha1, outSha1Size);
2373     }
2374
2375     if (outSha256 && outSha256Size >= 65)
2376     {
2377         BYTE sha256Bytes[32];
2378         DWORD sha256Len = sizeof(sha256Bytes);
2379         if (!CryptGetHashParam(hSha256, HP_HASHVAL, sha256Bytes, &sha256Len, 0))
2380             goto cleanup;
2381         HexBytesToString(sha256Bytes, sha256Len, outSha256, outSha256Size);
2382     }
2383
2384     ok = TRUE;
2385
2386 cleanup:
2387     if (!ok && startTick)
2388     {
2389         elapsedMs = GetTickCount() - startTick;
2390         FormatElapsedTime(elapsedMs, timeStr);
2391         printf("\n[WARN] Full-file CRC32/MD5/SHA-1/SHA-256 calculation failed after %s.\n", timeStr);
2392     }
2393     if (hSha256) CryptDestroyHash(hSha256);
2394     if (hSha1) CryptDestroyHash(hSha1);
2395     if (hMd5) CryptDestroyHash(hMd5);
2396     if (hProv) CryptReleaseContext(hProv, 0);
2397     free(buf);
2398     fclose(f);
2399     return ok;
2400 }
2401
2402 static void WritePressedDvdRomWriteMediaState(FILE *json, const char *isoFilename, uint32_t totalDiscSectors)
2403 {
2404     if (!json)
2405         return;
2406
2407     fprintf(json, "  \"write_media_state\": {\n");
2408     fprintf(json, "    \"media_class\": \"pressed_dvd_rom\",\n");
2409     fprintf(json, "    \"writable\": false,\n");
2410     fprintf(json, "    \"erasable\": false,\n");
2411     fprintf(json, "    \"finalized\": true,\n");
2412
2413     fprintf(json, "    \"backing_image\": {\n");
2414     fprintf(json, "      \"file\": "); JsonWriteEscapedString(json, isoFilename ? isoFilename : ""); fprintf(json, ",\n");
2415     fprintf(json, "      \"sector_size\": 2048,\n");
2416     fprintf(json, "      \"initial_sector_count\": %u,\n", totalDiscSectors);
2417     fprintf(json, "      \"max_sector_count\": %u,\n", totalDiscSectors);
2418     fprintf(json, "      \"growth_policy\": \"fixed_read_only\"\n");
2419     fprintf(json, "    },\n");
2420
2421     fprintf(json, "    \"sessions\": [\n");
2422     fprintf(json, "      {\n");
2423     fprintf(json, "        \"session_number\": 1,\n");
2424     fprintf(json, "        \"state\": \"closed\",\n");
2425     fprintf(json, "        \"first_track_number\": 1,\n");
2426     fprintf(json, "        \"last_track_number\": 1\n");
2427     fprintf(json, "      }\n");
2428     fprintf(json, "    ],\n");
2429
2430     fprintf(json, "    \"tracks\": [\n");
2431     fprintf(json, "      {\n");
2432     fprintf(json, "        \"track_number\": 1,\n");
2433     fprintf(json, "        \"state\": \"complete\",\n");
2434     fprintf(json, "        \"mode\": \"data\",\n");
2435     fprintf(json, "        \"packet_or_track_mode\": \"pressed_read_only\",\n");
2436     fprintf(json, "        \"start_lba\": 0,\n");
2437     fprintf(json, "        \"next_writable_lba\": "); JsonWriteNull(json); fprintf(json, ",\n");
2438     fprintf(json, "        \"free_blocks\": 0,\n");
2439     fprintf(json, "        \"written_blocks\": %u\n", totalDiscSectors);
2440     fprintf(json, "      }\n");
2441     fprintf(json, "    ],\n");
2442
2443     fprintf(json, "    \"unwritten_read_policy\": \"not_applicable_read_only_media\",\n");
2444     fprintf(json, "    \"flush_policy\": \"read_only_noop\"\n");
2445     fprintf(json, "  },\n");
2446 }
2447
2448 static BOOL WriteXboxDvdMediaProfileFile(const char *isoFilename,
2449                                          const XboxDvdSidecarCapture *cap,
2450                                          uint32_t totalDiscSectors,
2451                                          BOOL isDualLayer,
2452                                          uint32_t videoSectors,
2453                                          uint32_t gameSourceLba,
2454                                          uint32_t gameSectors,
2455                                          const char *isoSha1,
2456                                          const char *isoMd5,
2457                                          const char *isoCrc32,
2458                                          const char *isoSha256,
2459                                          const char *adC0Path,
2460                                          const char *pfiPath,
2461                                          const char *dmiPath,
2462                                          BOOL payloadFilesPresent)
2463 {
2464     char profilePath[MAX_PATH];
2465     char titleHint[256];
2466     char mediaId[64];
2467     BOOL redumpStyle;
2468     FILE *json;
2469
2470     if (!isoFilename || !cap)
2471         return FALSE;
2472
2473     MakeSidecarPath(isoFilename, ".media.json", profilePath, sizeof(profilePath));
2474     ExtractMediaProfileNames(isoFilename, titleHint, sizeof(titleHint), mediaId, sizeof(mediaId));
2475     redumpStyle = (totalDiscSectors == XGD1_FULL_REDUMP_SECTORS && gameSectors == REDUMP_SECTORS);
2476
2477     json = fopen(profilePath, "wb");
2478     if (!json)
2479         return FALSE;
2480
2481     fprintf(json, "{\n");
2482     fprintf(json, "  \"format\": \"xdvd-media-profile\",\n");
2483     fprintf(json, "  \"version\": 1,\n");
2484     fprintf(json, "  \"media_id\": "); JsonWriteEscapedString(json, mediaId); fprintf(json, ",\n");
2485     fprintf(json, "  \"title_hint\": "); JsonWriteEscapedString(json, titleHint); fprintf(json, ",\n");
2486     fprintf(json, "  \"image\": {\n");
2487     fprintf(json, "    \"file\": "); JsonWriteEscapedString(json, isoFilename); fprintf(json, ",\n");
2488     fprintf(json, "    \"sector_size\": 2048,\n");
2489     fprintf(json, "    \"sector_count\": %u,\n", totalDiscSectors);
2490     fprintf(json, "    \"byte_count\": %llu,\n", (unsigned long long)totalDiscSectors * 2048ULL);
2491     fprintf(json, "    \"sha1\": "); JsonWriteEscapedString(json, isoSha1 ? isoSha1 : ""); fprintf(json, ",\n");
2492     fprintf(json, "    \"hashes\": {\n");
2493     fprintf(json, "      \"crc32\": "); JsonWriteEscapedString(json, isoCrc32 ? isoCrc32 : ""); fprintf(json, ",\n");
2494     fprintf(json, "      \"md5\": "); JsonWriteEscapedString(json, isoMd5 ? isoMd5 : ""); fprintf(json, ",\n");
2495     fprintf(json, "      \"sha1\": "); JsonWriteEscapedString(json, isoSha1 ? isoSha1 : ""); fprintf(json, ",\n");
2496     fprintf(json, "      \"sha256\": "); JsonWriteEscapedString(json, isoSha256 ? isoSha256 : ""); fprintf(json, "\n");
2497     fprintf(json, "    }\n");
2498     fprintf(json, "  },\n");
2499
2500     {
2501         uint32_t gameOutputLba = redumpStyle ? XGD1_GAME_OUTPUT_START_LBA : videoSectors;
2502
2503         fprintf(json, "  \"layout\": {\n");
2504         fprintf(json, "    \"layout_type\": "); JsonWriteEscapedString(json, redumpStyle ? "original_xbox_xgd1_redump_style_2048" : "contiguous_logical_dump"); fprintf(json, ",\n");
2505         fprintf(json, "    \"layers\": %u,\n", isDualLayer ? 2U : 1U);
2506         fprintf(json, "    \"layer_break_lba\": %u,\n", isDualLayer ? LAYER_BREAK : 0U);
2507         fprintf(json, "    \"video_l0_start_lba\": 0,\n");
2508         fprintf(json, "    \"video_l0_sector_count\": %u,\n", redumpStyle ? XGD1_VIDEO_L0_SECTORS : videoSectors);
2509         fprintf(json, "    \"pregame_padding_start_lba\": %u,\n", redumpStyle ? XGD1_VIDEO_L0_SECTORS : 0U);
2510         fprintf(json, "    \"pregame_padding_sector_count\": %u,\n", redumpStyle ? (XGD1_GAME_OUTPUT_START_LBA - XGD1_VIDEO_L0_SECTORS) : 0U);
2511         fprintf(json, "    \"game_output_start_lba\": %u,\n", gameOutputLba);
2512         fprintf(json, "    \"game_leadin_unlocked_source_start_lba\": %u,\n", redumpStyle ? 0U : gameSourceLba);
2513         fprintf(json, "    \"game_leadin_source_sector_count\": %u,\n", redumpStyle ? XGD1_XISO_LEADIN_SECTORS : 0U);
2514         fprintf(json, "    \"game_leadin_source\": "); JsonWriteEscapedString(json, redumpStyle ? "drive_read10" : "not_applicable"); fprintf(json, ",\n");
2515         fprintf(json, "    \"game_unlocked_source_start_lba\": %u,\n", gameSourceLba);
2516         fprintf(json, "    \"game_unlocked_source_sector_count\": %u,\n", redumpStyle ? XGD1_GAME_SOURCE_SECTORS : gameSectors);
2517         fprintf(json, "    \"game_xiso_leadin_sector_count\": %u,\n", redumpStyle ? XGD1_XISO_LEADIN_SECTORS : 0U);
2518         fprintf(json, "    \"xdfs_volume_lba_within_game_region\": 32,\n");
2519         fprintf(json, "    \"game_sector_count\": %u,\n", gameSectors);
2520         fprintf(json, "    \"postgame_padding_start_lba\": %u,\n", redumpStyle ? (XGD1_GAME_OUTPUT_START_LBA + REDUMP_SECTORS) : 0U);
2521         fprintf(json, "    \"postgame_padding_sector_count\": %u,\n", redumpStyle ? (XGD1_VIDEO_L1_OUTPUT_START_LBA - (XGD1_GAME_OUTPUT_START_LBA + REDUMP_SECTORS)) : 0U);
2522         fprintf(json, "    \"video_l1_start_lba\": %u,\n", redumpStyle ? XGD1_VIDEO_L1_OUTPUT_START_LBA : 0U);
2523         fprintf(json, "    \"video_l1_sector_count\": %u,\n", redumpStyle ? XGD1_VIDEO_L1_SECTORS : 0U);
2524         fprintf(json, "    \"legacy_contiguous_visible_sector_count\": %u,\n", videoSectors);
2525         fprintf(json, "    \"drive_locked_visible_sector_count\": %u,\n", cap->hasLockedCapacity ? CapacitySectorsFromReadCapacity10(cap->lockedCapacity) : 0);
2526         fprintf(json, "    \"drive_reported_unlocked_sector_count\": %u,\n", cap->hasUnlockedCapacity ? CapacitySectorsFromReadCapacity10(cap->unlockedCapacity) : 0);
2527         fprintf(json, "    \"reconstructed_output_sector_count\": %u\n", totalDiscSectors);
2528         fprintf(json, "  },\n");
2529     }
2530
2531     fprintf(json, "  \"reconstruction\": {\n");
2532     fprintf(json, "    \"is_reconstructed_layout\": %s,\n", redumpStyle ? "true" : "false");
2533     fprintf(json, "    \"filler_policy\": "); JsonWriteEscapedString(json, redumpStyle ? "pregame_and_postgame_zero_fill_content_placeholder" : "not_applicable"); fprintf(json, ",\n");
2534     fprintf(json, "    \"filler_geometry_verified\": %s,\n", redumpStyle ? "true" : "false");
2535     fprintf(json, "    \"filler_verified_from_disc\": false,\n");
2536     fprintf(json, "    \"filler_byte_value\": %s,\n", redumpStyle ? "0" : "null");
2537     fprintf(json, "    \"pending_hardware_capture\": false,\n");
2538     fprintf(json, "    \"unresolved_filler_content\": %s,\n", redumpStyle ? "true" : "false");
2539     fprintf(json, "    \"filler_ranges\": [\n");
2540     if (redumpStyle)
2541     {
2542         fprintf(json, "      {\n");
2543         fprintf(json, "        \"name\": \"pregame_padding\",\n");
2544         fprintf(json, "        \"start_lba\": %u,\n", XGD1_VIDEO_L0_SECTORS);
2545         fprintf(json, "        \"sector_count\": %u,\n", XGD1_GAME_OUTPUT_START_LBA - XGD1_VIDEO_L0_SECTORS);
2546         fprintf(json, "        \"source\": \"synthetic_zero_fill\"\n");
2547         fprintf(json, "      },\n");
2548         fprintf(json, "      {\n");
2549         fprintf(json, "        \"name\": \"postgame_padding\",\n");
2550         fprintf(json, "        \"start_lba\": %u,\n", XGD1_GAME_OUTPUT_START_LBA + REDUMP_SECTORS);
2551         fprintf(json, "        \"sector_count\": %u,\n", XGD1_VIDEO_L1_OUTPUT_START_LBA - (XGD1_GAME_OUTPUT_START_LBA + REDUMP_SECTORS));
2552         fprintf(json, "        \"source\": \"synthetic_zero_fill\"\n");
2553         fprintf(json, "      }\n");
2554     }
2555     fprintf(json, "    ],\n");
2556     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");
2557     fprintf(json, "  },\n");
2558
2559     fprintf(json, "  \"dvd_structures\": {\n");
2560     fprintf(json, "    \"ad_c0\": "); JsonWriteEscapedString(json, cap->hasAdC0 ? adC0Path : ""); fprintf(json, ",\n");
2561     fprintf(json, "    \"pfi\": "); JsonWriteEscapedString(json, cap->hasPfi ? pfiPath : ""); fprintf(json, ",\n");
2562     fprintf(json, "    \"dmi\": "); JsonWriteEscapedString(json, cap->hasDmi ? dmiPath : ""); fprintf(json, "\n");
2563     fprintf(json, "  },\n");
2564
2565     fprintf(json, "  \"non_lba_physical_metadata\": {\n");
2566     fprintf(json, "    \"pfi_storage\": \"sidecar_bin\",\n");
2567     fprintf(json, "    \"dmi_storage\": \"sidecar_bin\",\n");
2568     fprintf(json, "    \"lead_in_storage\": \"not_in_iso_stream\",\n");
2569     fprintf(json, "    \"lead_out_storage\": \"not_in_iso_stream\",\n");
2570     fprintf(json, "    \"note\": \"A standard 2048-byte-sector ISO contains READ(10) user-data sectors only. PFI/DMI/lead-in/lead-out are represented by sidecar/profile metadata.\"\n");
2571     fprintf(json, "  },\n");
2572
2573     fprintf(json, "  \"drive_state_observations\": {\n");
2574     fprintf(json, "    \"locked\": {\n");
2575     fprintf(json, "      \"read_capacity_10_hex\": "); JsonWriteHexString(json, cap->hasLockedCapacity ? cap->lockedCapacity : NULL, cap->hasLockedCapacity ? 8 : 0); fprintf(json, ",\n");
2576     fprintf(json, "      \"mode_sense_3e_hex\": "); JsonWriteHexString(json, cap->hasLockedModeSense3E ? cap->lockedModeSense3E : NULL, cap->hasLockedModeSense3E ? 28 : 0); fprintf(json, "\n");
2577     fprintf(json, "    },\n");
2578     fprintf(json, "    \"unlocked\": {\n");
2579     fprintf(json, "      \"read_capacity_10_hex\": "); JsonWriteHexString(json, cap->hasUnlockedCapacity ? cap->unlockedCapacity : NULL, cap->hasUnlockedCapacity ? 8 : 0); fprintf(json, ",\n");
2580     fprintf(json, "      \"mode_sense_3e_hex\": "); JsonWriteHexString(json, cap->hasUnlockedModeSense3E ? cap->unlockedModeSense3E : NULL, cap->hasUnlockedModeSense3E ? 28 : 0); fprintf(json, "\n");
2581     fprintf(json, "    }\n");
2582     fprintf(json, "  },\n");
2583
2584     WritePressedDvdRomWriteMediaState(json, isoFilename, totalDiscSectors);
2585
2586     fprintf(json, "  \"validation\": {\n");
2587     fprintf(json, "    \"byte_count_matches_sector_count\": true,\n");
2588     fprintf(json, "    \"payload_files_present\": %s,\n", payloadFilesPresent ? "true" : "false");
2589     fprintf(json, "    \"warnings\": "); JsonWriteValidationWarnings(json, cap, payloadFilesPresent, redumpStyle); fprintf(json, "\n");
2590     fprintf(json, "  }\n");
2591     fprintf(json, "}\n");
2592
2593     fclose(json);
2594     printf("[META] Wrote media profile: %s\n", profilePath);
2595     return TRUE;
2596 }
2597
2598 static BOOL WriteXboxDvdSidecarFiles(const char *isoFilename,
2599                                       const XboxDvdSidecarCapture *cap,
2600                                       uint32_t totalDiscSectors,
2601                                       BOOL isDualLayer,
2602                                       uint32_t videoSectors,
2603                                       uint32_t gameSourceLba,
2604                                       uint32_t gameSectors,
2605                                       const char *isoSha1,
2606                                       const char *isoMd5,
2607                                       const char *isoCrc32,
2608                                       const char *isoSha256)
2609 {
2610     char jsonPath[MAX_PATH];
2611     char adC0Path[MAX_PATH];
2612     char pfiPath[MAX_PATH];
2613     char dmiPath[MAX_PATH];
2614     FILE *json;
2615     BOOL ok = TRUE;
2616     BOOL payloadFilesPresent = FALSE;
2617     BOOL redumpStyle = FALSE;
2618
2619     if (!isoFilename || !cap)
2620         return FALSE;
2621
2622     MakeSidecarPath(isoFilename, ".xdvd.json", jsonPath, sizeof(jsonPath));
2623     MakeSidecarPath(isoFilename, ".ad_c0.bin", adC0Path, sizeof(adC0Path));
2624     MakeSidecarPath(isoFilename, ".pfi.bin", pfiPath, sizeof(pfiPath));
2625     MakeSidecarPath(isoFilename, ".dmi.bin", dmiPath, sizeof(dmiPath));
2626
2627     if (cap->hasAdC0 && !WriteBinaryFile(adC0Path, cap->adC0, 0x664))
2628         ok = FALSE;
2629     if (cap->hasPfi && !WriteBinaryFile(pfiPath, cap->pfi, 2048))
2630         ok = FALSE;
2631     if (cap->hasDmi && !WriteBinaryFile(dmiPath, cap->dmi, 2048))
2632         ok = FALSE;
2633
2634     payloadFilesPresent = cap->hasAdC0 && cap->hasPfi && cap->hasDmi && ok;
2635     redumpStyle = (totalDiscSectors == XGD1_FULL_REDUMP_SECTORS && gameSectors == REDUMP_SECTORS);
2636
2637     json = fopen(jsonPath, "wb");
2638     if (!json)
2639         return FALSE;
2640
2641     fprintf(json, "{\n");
2642     fprintf(json, "  \"format\": \"xdvd-sidecar\",\n");
2643     fprintf(json, "  \"version\": 1,\n");
2644     fprintf(json, "  \"image\": {\n");
2645     fprintf(json, "    \"file\": "); JsonWriteEscapedString(json, isoFilename); fprintf(json, ",\n");
2646     fprintf(json, "    \"sector_size\": 2048,\n");
2647     fprintf(json, "    \"sector_count\": %u,\n", totalDiscSectors);
2648     fprintf(json, "    \"byte_count\": %llu,\n", (unsigned long long)totalDiscSectors * 2048ULL);
2649     fprintf(json, "    \"sha1\": "); JsonWriteEscapedString(json, isoSha1 ? isoSha1 : ""); fprintf(json, ",\n");
2650     fprintf(json, "    \"hashes\": {\n");
2651     fprintf(json, "      \"crc32\": "); JsonWriteEscapedString(json, isoCrc32 ? isoCrc32 : ""); fprintf(json, ",\n");
2652     fprintf(json, "      \"md5\": "); JsonWriteEscapedString(json, isoMd5 ? isoMd5 : ""); fprintf(json, ",\n");
2653     fprintf(json, "      \"sha1\": "); JsonWriteEscapedString(json, isoSha1 ? isoSha1 : ""); fprintf(json, ",\n");
2654     fprintf(json, "      \"sha256\": "); JsonWriteEscapedString(json, isoSha256 ? isoSha256 : ""); fprintf(json, "\n");
2655     fprintf(json, "    },\n");
2656     {
2657         uint32_t gameOutputLba = redumpStyle ? XGD1_GAME_OUTPUT_START_LBA : videoSectors;
2658
2659         fprintf(json, "    \"layout\": {\n");
2660         fprintf(json, "      \"layout_type\": "); JsonWriteEscapedString(json, redumpStyle ? "original_xbox_xgd1_redump_style_2048" : "contiguous_logical_dump"); fprintf(json, ",\n");
2661         fprintf(json, "      \"legacy_contiguous_visible_start_lba\": 0,\n");
2662         fprintf(json, "      \"legacy_contiguous_visible_sector_count\": %u,\n", redumpStyle ? XGD1_GAME_OUTPUT_START_LBA : videoSectors);
2663         fprintf(json, "      \"drive_locked_visible_sector_count\": %u,\n", cap->hasLockedCapacity ? CapacitySectorsFromReadCapacity10(cap->lockedCapacity) : 0);
2664         fprintf(json, "      \"video_l0_start_lba\": 0,\n");
2665         fprintf(json, "      \"video_l0_sector_count\": %u,\n", redumpStyle ? XGD1_VIDEO_L0_SECTORS : videoSectors);
2666         fprintf(json, "      \"pregame_padding_start_lba\": %u,\n", redumpStyle ? XGD1_VIDEO_L0_SECTORS : 0U);
2667         fprintf(json, "      \"pregame_padding_sector_count\": %u,\n", redumpStyle ? (XGD1_GAME_OUTPUT_START_LBA - XGD1_VIDEO_L0_SECTORS) : 0U);
2668         fprintf(json, "      \"game_output_start_lba\": %u,\n", gameOutputLba);
2669         fprintf(json, "      \"game_leadin_unlocked_source_start_lba\": %u,\n", redumpStyle ? 0U : gameSourceLba);
2670         fprintf(json, "      \"game_leadin_source_sector_count\": %u,\n", redumpStyle ? XGD1_XISO_LEADIN_SECTORS : 0U);
2671         fprintf(json, "      \"game_leadin_source\": "); JsonWriteEscapedString(json, redumpStyle ? "drive_read10" : "not_applicable"); fprintf(json, ",\n");
2672         fprintf(json, "      \"game_unlocked_source_start_lba\": %u,\n", gameSourceLba);
2673         fprintf(json, "      \"game_unlocked_source_sector_count\": %u,\n", redumpStyle ? XGD1_GAME_SOURCE_SECTORS : gameSectors);
2674         fprintf(json, "      \"game_xiso_leadin_sector_count\": %u,\n", redumpStyle ? XGD1_XISO_LEADIN_SECTORS : 0U);
2675         fprintf(json, "      \"xdfs_volume_lba_within_game_region\": 32,\n");
2676         fprintf(json, "      \"game_sector_count\": %u,\n", gameSectors);
2677         fprintf(json, "      \"postgame_padding_start_lba\": %u,\n", redumpStyle ? (XGD1_GAME_OUTPUT_START_LBA + REDUMP_SECTORS) : 0U);
2678         fprintf(json, "      \"postgame_padding_sector_count\": %u,\n", redumpStyle ? (XGD1_VIDEO_L1_OUTPUT_START_LBA - (XGD1_GAME_OUTPUT_START_LBA + REDUMP_SECTORS)) : 0U);
2679         fprintf(json, "      \"video_l1_start_lba\": %u,\n", redumpStyle ? XGD1_VIDEO_L1_OUTPUT_START_LBA : 0U);
2680         fprintf(json, "      \"video_l1_sector_count\": %u,\n", redumpStyle ? XGD1_VIDEO_L1_SECTORS : 0U);
2681         fprintf(json, "      \"layer_break_lba\": %u\n", LAYER_BREAK);
2682         fprintf(json, "    }\n");
2683     }
2684     fprintf(json, "  },\n");
2685
2686     fprintf(json, "  \"disc\": {\n");
2687     fprintf(json, "    \"layers\": %u,\n", isDualLayer ? 2U : 1U);
2688     fprintf(json, "    \"reconstructed_output_sectors\": %u,\n", totalDiscSectors);
2689     fprintf(json, "    \"reconstructed_output_byte_count\": %llu,\n", (unsigned long long)totalDiscSectors * 2048ULL);
2690     fprintf(json, "    \"drive_reported_unlocked_sectors\": %u,\n", cap->hasUnlockedCapacity ? CapacitySectorsFromReadCapacity10(cap->unlockedCapacity) : 0);
2691     fprintf(json, "    \"drive_reported_locked_sectors\": %u,\n", cap->hasLockedCapacity ? CapacitySectorsFromReadCapacity10(cap->lockedCapacity) : 0);
2692     fprintf(json, "    \"unlocked_game_view_sectors\": %u\n", gameSectors);
2693     fprintf(json, "  },\n");
2694
2695     fprintf(json, "  \"drive_states\": {\n");
2696     fprintf(json, "    \"locked\": {\n");
2697     fprintf(json, "      \"read_capacity_10_cdb_hex\": \"25000000000000000000\",\n");
2698     fprintf(json, "      \"read_capacity_10_hex\": "); JsonWriteHexString(json, cap->hasLockedCapacity ? cap->lockedCapacity : NULL, cap->hasLockedCapacity ? 8 : 0); fprintf(json, ",\n");
2699     fprintf(json, "      \"visible_sector_count\": %u,\n", cap->hasLockedCapacity ? CapacitySectorsFromReadCapacity10(cap->lockedCapacity) : 0);
2700     fprintf(json, "      \"mode_sense_3e_cdb_hex\": \"5A003E00000000001C00\",\n");
2701     fprintf(json, "      \"mode_sense_3e_hex\": "); JsonWriteHexString(json, cap->hasLockedModeSense3E ? cap->lockedModeSense3E : NULL, cap->hasLockedModeSense3E ? 28 : 0); fprintf(json, "\n");
2702     fprintf(json, "    },\n");
2703     fprintf(json, "    \"unlocked\": {\n");
2704     fprintf(json, "      \"read_capacity_10_cdb_hex\": \"25000000000000000000\",\n");
2705     fprintf(json, "      \"read_capacity_10_hex\": "); JsonWriteHexString(json, cap->hasUnlockedCapacity ? cap->unlockedCapacity : NULL, cap->hasUnlockedCapacity ? 8 : 0); fprintf(json, ",\n");
2706     fprintf(json, "      \"visible_sector_count\": %u,\n", cap->hasUnlockedCapacity ? CapacitySectorsFromReadCapacity10(cap->unlockedCapacity) : 0);
2707     fprintf(json, "      \"mode_sense_3e_cdb_hex\": \"5A003E00000000001C00\",\n");
2708     fprintf(json, "      \"mode_sense_3e_hex\": "); JsonWriteHexString(json, cap->hasUnlockedModeSense3E ? cap->unlockedModeSense3E : NULL, cap->hasUnlockedModeSense3E ? 28 : 0); fprintf(json, "\n");
2709     fprintf(json, "    }\n");
2710     fprintf(json, "  },\n");
2711
2712     fprintf(json, "  \"scsi_responses\": [\n");
2713     fprintf(json, "    {\n");
2714     fprintf(json, "      \"name\": \"read_dvd_structure_xbox_control_block\",\n");
2715     fprintf(json, "      \"cdb_hex\": \"AD00FF02FDFFFE00066400C0\",\n");
2716     fprintf(json, "      \"data_in\": true,\n");
2717     fprintf(json, "      \"data_len\": 1636,\n");
2718     fprintf(json, "      \"response_file\": "); JsonWriteEscapedString(json, cap->hasAdC0 ? adC0Path : ""); fprintf(json, ",\n");
2719     fprintf(json, "      \"captured\": %s\n", cap->hasAdC0 ? "true" : "false");
2720     fprintf(json, "    },\n");
2721     fprintf(json, "    {\n");
2722     fprintf(json, "      \"name\": \"read_dvd_structure_pfi\",\n");
2723     fprintf(json, "      \"cdb_hex\": \"AD0000000000000008000000\",\n");
2724     fprintf(json, "      \"data_in\": true,\n");
2725     fprintf(json, "      \"data_len\": 2048,\n");
2726     fprintf(json, "      \"response_file\": "); JsonWriteEscapedString(json, cap->hasPfi ? pfiPath : ""); fprintf(json, ",\n");
2727     fprintf(json, "      \"captured\": %s\n", cap->hasPfi ? "true" : "false");
2728     fprintf(json, "    },\n");
2729     fprintf(json, "    {\n");
2730     fprintf(json, "      \"name\": \"read_dvd_structure_dmi\",\n");
2731     fprintf(json, "      \"cdb_hex\": \"AD0000000000000408000000\",\n");
2732     fprintf(json, "      \"data_in\": true,\n");
2733     fprintf(json, "      \"data_len\": 2048,\n");
2734     fprintf(json, "      \"response_file\": "); JsonWriteEscapedString(json, cap->hasDmi ? dmiPath : ""); fprintf(json, ",\n");
2735     fprintf(json, "      \"captured\": %s\n", cap->hasDmi ? "true" : "false");
2736     fprintf(json, "    }\n");
2737     fprintf(json, "  ],\n");
2738
2739     fprintf(json, "  \"auth\": {\n");
2740     fprintf(json, "    \"requires_media_transition\": false,\n");
2741     fprintf(json, "    \"unlock_requires_media_transition\": false,\n");
2742     fprintf(json, "    \"locked_video_view_restore_requires_media_transition\": true,\n");
2743     fprintf(json, "    \"state_detection\": \"READ CAPACITY (10) visible-sector count\",\n");
2744     fprintf(json, "    \"mode_page\": \"0x3E\",\n");
2745     fprintf(json, "    \"challenge_table_source\": \"read_dvd_structure_xbox_control_block\",\n");
2746     fprintf(json, "    \"challenge_table_response_offset\": 774,\n");
2747     fprintf(json, "    \"challenge_table_hash_offset\": 1187,\n");
2748     fprintf(json, "    \"challenge_table_hash_length\": 44\n");
2749     fprintf(json, "  }\n");
2750     fprintf(json, "}\n");
2751
2752     fclose(json);
2753
2754     if (!WriteXboxDvdMediaProfileFile(isoFilename,
2755                                       cap,
2756                                       totalDiscSectors,
2757                                       isDualLayer,
2758                                       videoSectors,
2759                                       gameSourceLba,
2760                                       gameSectors,
2761                                       isoSha1,
2762                                       isoMd5,
2763                                       isoCrc32,
2764                                       isoSha256,
2765                                       adC0Path,
2766                                       pfiPath,
2767                                       dmiPath,
2768                                       payloadFilesPresent))
2769     {
2770         ok = FALSE;
2771         printf("[WARN] Failed to write XDVD media profile.\n");
2772     }
2773
2774     printf("[META] Wrote XDVD sidecar: %s\n", jsonPath);
2775     if (cap->hasAdC0) printf("[META] Wrote Xbox control block: %s\n", adC0Path);
2776     if (cap->hasPfi) printf("[META] Wrote PFI: %s\n", pfiPath);
2777     if (cap->hasDmi) printf("[META] Wrote DMI: %s\n", dmiPath);
2778
2779     return ok;
2780 }
2781
2782 BOOL DumpXboxGameDisc(HANDLE hDevice, const char *filename, char xisoFormat,
2783                       uint32_t totalDiscSectors, bool isDualLayer,
2784                       bool EjectOnSuccess,
2785                       int (*cancel)(void *cancel_data),
2786                       void *cancel_data,
2787                       xbox_ref_dump_result *result)
2788 {
2789     HCRYPTPROV hProv = 0;
2790     HCRYPTHASH hHash = 0;
2791     FILE *outFile = NULL;
2792     BYTE rgbHash[20];
2793     char sha1String[41] = {0};
2794     char md5String[33] = {0};
2795     char crc32String[9] = {0};
2796     char fileSha1String[41] = {0};
2797     char fileSha256String[65] = {0};
2798     BOOL dumpOk = FALSE;
2799     XboxDvdSidecarCapture sidecarCapture;
2800     BOOL rawSidecarAvailable = FALSE;
2801     uint32_t rawVideoSectors = START_LBA_MAGIC;
2802     uint32_t rawGameSourceLba = 0xFFFFFFFFu;
2803     uint32_t rawGameSectors = 0;
2804     uint32_t rawTargetSectors = 0;
2805     unsigned long long expectedOutputBytes = 0ULL;
2806     const char *outputLabel = "Output";
2807     DWORD operationStartTick = GetTickCount();
2808     DWORD cbHash = 20;
2809     char operationTimeStr[12] = {0};
2810
2811     memset(&sidecarCapture, 0, sizeof(sidecarCapture));
2812
2813     g_xbox_dump_cancel = cancel;
2814     g_xbox_dump_cancel_data = cancel_data;
2815     g_xbox_dump_cancel_result = result;
2816
2817     if (result) {
2818         result->attempted = 1;
2819         result->mode = xisoFormat;
2820         if (filename && filename[0]) {
2821             strncpy(result->output_path, filename, sizeof(result->output_path) - 1);
2822             result->output_path[sizeof(result->output_path) - 1] = '\0';
2823         }
2824     }
2825
2826     if (totalDiscSectors == 0)
2827         totalDiscSectors = GetTotalSectors(hDevice);
2828     if (totalDiscSectors == 0)
2829         totalDiscSectors = REDUMP_SECTORS;
2830
2831     if (XboxDumpCancelRequested())
2832     {
2833         printf("\n[CANCEL] User cancellation requested before Xbox output preparation.\n");
2834         goto cleanup;
2835     }
2836
2837     rawTargetSectors = NormalizeRawIsoTargetSectors(totalDiscSectors, isDualLayer);
2838
2839     if (!CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
2840         goto cleanup;
2841     if (!CryptCreateHash(hProv, CALG_SHA1, 0, 0, &hHash))
2842         goto cleanup;
2843
2844     EnsureDriveReady(hDevice, 30000);
2845     SetDriveSpeedMax(hDevice);
2846
2847     if (xisoFormat == '1')
2848     {
2849         DWORD bytesReturned;
2850         uint32_t gameSourceLba = 0xFFFFFFFFu;
2851         uint32_t videoSectors = START_LBA_MAGIC;
2852         uint32_t gameSectors = 0;
2853         BOOL lockedViewIsAlreadyXdfs = FALSE;
2854
2855         outputLabel = "RAW ISO";
2856         expectedOutputBytes = (unsigned long long)rawTargetSectors * 2048ULL;
2857         if (result)
2858             result->output_sectors = rawTargetSectors;
2859
2860         if (rawTargetSectors != totalDiscSectors)
2861         {
2862             printf("[RAW] Drive-reported unlocked sectors %u normalized to Redump-style output target %u.\n",
2863                    totalDiscSectors, rawTargetSectors);
2864         }
2865
2866         printf("[RAW] Full-disc target: %u sectors (%llu bytes).\n",
2867                rawTargetSectors,
2868                expectedOutputBytes);
2869         if (rawTargetSectors > LAYER_BREAK)
2870         {
2871             printf("[RAW] Expected Redump/XGD1 layer break at output LBA %u.\n", LAYER_BREAK);
2872         }
2873         printf("[RAW] Media-transition-preserving mode is enabled.\n");
2874
2875         if (!CheckOutputFreeSpace(filename, expectedOutputBytes, outputLabel))
2876             goto cleanup;
2877
2878         outFile = fopen(filename, "wb");
2879         if (!outFile)
2880         {
2881             printf("\n[FATAL] Could not create output file '%s'.\n", filename);
2882             if (errno)
2883                 printf("        errno: %d (%s)\n", errno, strerror(errno));
2884             goto cleanup;
2885         }
2886
2887         // Raw mode must capture the visible/video view first. The caller normally reaches this
2888         // point after the drive has already been authenticated for metadata, so reset the drive
2889         // state with a real media transition before reading LBA 0.
2890         DeviceIoControl(hDevice, FSCTL_UNLOCK_VOLUME, NULL, 0, NULL, 0, &bytesReturned, NULL);
2891
2892         printf("[RAW] Cycling tray to restore locked/video view before dumping sector 0.\n");
2893         AutomateTrayCycle(hDevice);
2894         RefreshVolume(hDevice);
2895         SetDriveSpeedMax(hDevice);
2896
2897         DeviceIoControl(hDevice, FSCTL_LOCK_VOLUME, NULL, 0, NULL, 0, &bytesReturned, NULL);
2898
2899         CaptureLockedSidecarState(hDevice, &sidecarCapture);
2900
2901         lockedViewIsAlreadyXdfs = ProbeXboxVolumeAt(hDevice, 0x20);
2902
2903         if (lockedViewIsAlreadyXdfs && rawTargetSectors > START_LBA_MAGIC)
2904         {
2905             printf("\n[FATAL] Option 1 requires a full raw/video-front source image.\n");
2906             printf("        This source exposes XDFS at LBA 0x20 after the media-reset step,\n");
2907             printf("        which looks like an XISO/game-partition view, not a full raw disc view.\n");
2908             printf("        Use option 2 for this source, or mount/create a 7.29 GiB Redump-style option-1 ISO.\n");
2909             goto cleanup;
2910         }
2911
2912         if (rawTargetSectors <= START_LBA_MAGIC)
2913         {
2914             printf("[RAW] Non-retail-sized source; dumping visible LBA 0..%u directly.\n",
2915                    rawTargetSectors - 1);
2916             rawVideoSectors = rawTargetSectors;
2917             rawGameSourceLba = 0;
2918             rawGameSectors = 0;
2919             CaptureUnlockedSidecarState(hDevice, &sidecarCapture);
2920             rawSidecarAvailable = TRUE;
2921             dumpOk = DumpSectorRangeWithRetry(hDevice, outFile, hHash, 0, rawTargetSectors, 0, "RAW");
2922         }
2923         else if (rawTargetSectors == XGD1_FULL_REDUMP_SECTORS)
2924         {
2925             unsigned char *videoL1Buffer = NULL;
2926             uint32_t detectedXdfsLba;
2927             uint32_t pregamePaddingSectors = XGD1_GAME_OUTPUT_START_LBA - XGD1_VIDEO_L0_SECTORS;
2928             uint32_t postgamePaddingSectors = XGD1_VIDEO_L1_OUTPUT_START_LBA - (XGD1_GAME_OUTPUT_START_LBA + REDUMP_SECTORS);
2929
2930             printf("[RAW] Using Original Xbox/XGD1 Redump-style 2048-byte-sector layout.\n");
2931             printf("[RAW] Layout: VIDEO_L0=%u sectors, pregame zero-fill padding=%u sectors, game/XISO=%u sectors, postgame zero-fill padding=%u sectors, VIDEO_L1=%u sectors.\n",
2932                    XGD1_VIDEO_L0_SECTORS,
2933                    pregamePaddingSectors,
2934                    REDUMP_SECTORS,
2935                    postgamePaddingSectors,
2936                    XGD1_VIDEO_L1_SECTORS);
2937             printf("[RAW] Note: filler/padding ranges are synthetic zero-fill placeholders until readable from hardware.\n");
2938             printf("[RAW] Note: PFI/DMI/lead-in/lead-out are MMC/physical metadata, not READ(10) user-data sectors; they are emitted in sidecar/profile files.\n");
2939
2940             videoL1Buffer = (unsigned char *)VirtualAlloc(NULL, XGD1_VIDEO_L1_SECTORS * 2048U, MEM_COMMIT, PAGE_READWRITE);
2941             if (!videoL1Buffer)
2942             {
2943                 printf("\n[FATAL] Could not allocate VIDEO_L1 capture buffer.\n");
2944                 goto cleanup;
2945             }
2946
2947             // The locked-visible Xbox video ISO is 6,992 sectors.  In the Redump-style
2948             // image, its L0 portion is placed at the beginning and its L1 tail is placed
2949             // at the end of the reconstructed image.  Capture the L1 tail while the drive
2950             // is still in the locked/video state, before authenticating for the game view.
2951             if (!ReadSectorsToMemory(hDevice,
2952                                      XGD1_VIDEO_L0_SECTORS,
2953                                      XGD1_VIDEO_L1_SECTORS,
2954                                      videoL1Buffer,
2955                                      "VIDEO-L1 tail"))
2956             {
2957                 VirtualFree(videoL1Buffer, 0, MEM_RELEASE);
2958                 goto cleanup;
2959             }
2960
2961             printf("[RAW] Writing video L0 from locked source LBA 0..%u.\n", XGD1_VIDEO_L0_SECTORS - 1);
2962             if (!DumpSectorRangeWithRetry(hDevice, outFile, hHash, 0, XGD1_VIDEO_L0_SECTORS, 0, "VIDEO-L0"))
2963             {
2964                 VirtualFree(videoL1Buffer, 0, MEM_RELEASE);
2965                 goto cleanup;
2966             }
2967
2968             if (!WriteZeroSectorsOutput(outFile, hHash, pregamePaddingSectors, XGD1_VIDEO_L0_SECTORS, "PREGAME-PAD"))
2969             {
2970                 VirtualFree(videoL1Buffer, 0, MEM_RELEASE);
2971                 goto cleanup;
2972             }
2973
2974             printf("[RAW] Re-applying full Xbox handshake for unlocked game/XISO view.\n");
2975             UnlockDrive(hDevice);
2976             RefreshVolume(hDevice);
2977             Sleep(2000);
2978             EnsureDriveReady(hDevice, 30000);
2979             SetDriveSpeedMax(hDevice);
2980             CaptureUnlockedSidecarState(hDevice, &sidecarCapture);
2981             rawSidecarAvailable = TRUE;
2982
2983             detectedXdfsLba = DetectXboxVolumeStart(hDevice);
2984             if (detectedXdfsLba == 0xFFFFFFFFu)
2985             {
2986                 printf("\n[FATAL] Could not locate XDFS after unlock at LBA 0x20 or 0x%X.\n", START_LBA_MAGIC);
2987                 VirtualFree(videoL1Buffer, 0, MEM_RELEASE);
2988                 goto cleanup;
2989             }
2990             if (detectedXdfsLba != 0x20)
2991             {
2992                 printf("[WARN] XDFS was detected at unlocked source LBA %u, not the expected XISO header LBA 32.\n", detectedXdfsLba);
2993             }
2994
2995             // FriiDump 0.5.3.13 cache-aligned raw-ID validation proved that
2996             // unlocked source LBA 0..31 is the physical 32-sector game-region
2997             // lead-in and that XDVDFS begins at source LBA 32. Preserve the
2998             // drive-captured lead-in instead of synthesizing zero sectors.
2999             gameSourceLba = XGD1_GAME_SOURCE_START_LBA;
3000             gameSectors = REDUMP_SECTORS;
3001             rawVideoSectors = XGD1_GAME_OUTPUT_START_LBA;
3002             rawGameSourceLba = gameSourceLba;
3003             rawGameSectors = gameSectors;
3004
3005             printf("[RAW] Capturing %u-sector game lead-in from unlocked source LBA 0..%u at output LBA %u.\n",
3006                    XGD1_XISO_LEADIN_SECTORS,
3007                    XGD1_XISO_LEADIN_SECTORS - 1,
3008                    XGD1_GAME_OUTPUT_START_LBA);
3009             if (!DumpSectorRangeWithRetry(hDevice,
3010                                           outFile,
3011                                           hHash,
3012                                           0,
3013                                           XGD1_XISO_LEADIN_SECTORS,
3014                                           XGD1_GAME_OUTPUT_START_LBA,
3015                                           "GAME-XISO-LEADIN"))
3016             {
3017                 VirtualFree(videoL1Buffer, 0, MEM_RELEASE);
3018                 goto cleanup;
3019             }
3020
3021             printf("[RAW] Writing unlocked XDFS/game data from source LBA %u for %u sectors at output LBA %u.\n",
3022                    XGD1_GAME_SOURCE_START_LBA,
3023                    XGD1_GAME_SOURCE_SECTORS,
3024                    XGD1_GAME_OUTPUT_START_LBA + XGD1_XISO_LEADIN_SECTORS);
3025             if (!DumpSectorRangeWithRetry(hDevice,
3026                                           outFile,
3027                                           hHash,
3028                                           XGD1_GAME_SOURCE_START_LBA,
3029                                           XGD1_GAME_SOURCE_SECTORS,
3030                                           XGD1_GAME_OUTPUT_START_LBA + XGD1_XISO_LEADIN_SECTORS,
3031                                           "GAME-XISO"))
3032             {
3033                 VirtualFree(videoL1Buffer, 0, MEM_RELEASE);
3034                 goto cleanup;
3035             }
3036
3037             if (!WriteZeroSectorsOutput(outFile, hHash, postgamePaddingSectors, XGD1_GAME_OUTPUT_START_LBA + REDUMP_SECTORS, "POSTGAME-PAD"))
3038             {
3039                 VirtualFree(videoL1Buffer, 0, MEM_RELEASE);
3040                 goto cleanup;
3041             }
3042
3043             if (!WriteMemorySectorsOutput(outFile, hHash, videoL1Buffer, XGD1_VIDEO_L1_SECTORS, XGD1_VIDEO_L1_OUTPUT_START_LBA, "VIDEO-L1"))
3044             {
3045                 VirtualFree(videoL1Buffer, 0, MEM_RELEASE);
3046                 goto cleanup;
3047             }
3048
3049             VirtualFree(videoL1Buffer, 0, MEM_RELEASE);
3050             dumpOk = TRUE;
3051         }
3052         else
3053         {
3054             if (videoSectors > rawTargetSectors)
3055                 videoSectors = rawTargetSectors;
3056             rawVideoSectors = videoSectors;
3057
3058             printf("[RAW] Dumping contiguous visible/video area first: source LBA 0..%u.\n", videoSectors - 1);
3059             if (!DumpSectorRangeWithRetry(hDevice, outFile, hHash, 0, videoSectors, 0, "VIDEO"))
3060                 goto cleanup;
3061
3062             printf("[RAW] Re-applying full Xbox handshake after media transition for hidden game/data area.\n");
3063             UnlockDrive(hDevice);
3064             RefreshVolume(hDevice);
3065             Sleep(2000);
3066             EnsureDriveReady(hDevice, 30000);
3067             SetDriveSpeedMax(hDevice);
3068             CaptureUnlockedSidecarState(hDevice, &sidecarCapture);
3069             rawSidecarAvailable = TRUE;
3070
3071             gameSourceLba = DetectXboxVolumeStart(hDevice);
3072             if (gameSourceLba == 0xFFFFFFFFu)
3073             {
3074                 printf("\n[FATAL] Could not locate XDFS after unlock at LBA 0x20 or 0x%X.\n", START_LBA_MAGIC);
3075                 goto cleanup;
3076             }
3077
3078             gameSectors = rawTargetSectors - videoSectors;
3079             rawGameSourceLba = gameSourceLba;
3080             rawGameSectors = gameSectors;
3081             printf("[RAW] Appending hidden game/data area from unlocked source LBA %u for %u sectors.\n",
3082                    gameSourceLba, gameSectors);
3083             dumpOk = DumpSectorRangeWithRetry(hDevice, outFile, hHash, gameSourceLba, gameSectors, videoSectors, "GAME");
3084         }
3085     }
3086     else if (xisoFormat == '2')
3087     {
3088         uint32_t startLba = 0;
3089         uint32_t sectorsToRead = 0;
3090         unsigned char *sectorBuffer = (unsigned char *)VirtualAlloc(NULL, 2048, MEM_COMMIT, PAGE_READWRITE);
3091         XDFS_VOLUME_DESCRIPTOR *vol = NULL;
3092         uint32_t xgd2EndLba = 1913920;
3093         unsigned char zeroSector[2048] = {0};
3094
3095         if (!sectorBuffer)
3096             goto cleanup;
3097
3098         SetDriveSpeedMax(hDevice);
3099
3100         vol = (XDFS_VOLUME_DESCRIPTOR *)sectorBuffer;
3101
3102         if (ScsiReadSectors(hDevice, START_LBA_MAGIC, 1, sectorBuffer) && memcmp(vol->Identifier, "MICROSOFT", 9) == 0)
3103         {
3104             startLba = START_LBA_MAGIC;
3105             printf("[INFO] XGD2 Game Partition identified at LBA %u\n", startLba);
3106         }
3107         else if (ScsiReadSectors(hDevice, 0x20, 1, sectorBuffer) && memcmp(vol->Identifier, "MICROSOFT", 9) == 0)
3108         {
3109             startLba = 0x20;
3110             printf("[INFO] Standard Game Partition identified at LBA 32\n");
3111         }
3112         else
3113         {
3114             printf("[ERROR] No Xbox Game Partition found. Disc may be non-standard.\n");
3115             VirtualFree(sectorBuffer, 0, MEM_RELEASE);
3116             goto cleanup;
3117         }
3118
3119         if (isDualLayer || totalDiscSectors > 3300000)
3120         {
3121             sectorsToRead = xgd2EndLba - startLba;
3122             printf("[INFO] Dual Layer disc detected. Calculating span across layers...\n");
3123         }
3124         else
3125         {
3126             sectorsToRead = vol->VolumeSize / 2048;
3127             printf("[INFO] Single Layer disc detected. Using header-reported size.\n");
3128         }
3129
3130         outputLabel = "XISO";
3131         expectedOutputBytes = ((unsigned long long)sectorsToRead + 32ULL) * 2048ULL;
3132         if (result)
3133             result->output_sectors = sectorsToRead + 32U;
3134
3135         printf("[SUCCESS] Final XISO target: %u sectors plus 32-sector lead-in (%llu bytes).\n",
3136                sectorsToRead,
3137                expectedOutputBytes);
3138
3139         if (!CheckOutputFreeSpace(filename, expectedOutputBytes, outputLabel))
3140         {
3141             VirtualFree(sectorBuffer, 0, MEM_RELEASE);
3142             goto cleanup;
3143         }
3144
3145         outFile = fopen(filename, "wb");
3146         if (!outFile)
3147         {
3148             printf("\n[FATAL] Could not create output file '%s'.\n", filename);
3149             if (errno)
3150                 printf("        errno: %d (%s)\n", errno, strerror(errno));
3151             VirtualFree(sectorBuffer, 0, MEM_RELEASE);
3152             goto cleanup;
3153         }
3154
3155         VirtualFree(sectorBuffer, 0, MEM_RELEASE);
3156
3157         printf("Writing 64KB XISO lead-in padding...\n");
3158         for (int p = 0; p < 32; p++)
3159         {
3160             if (XboxDumpCancelRequested())
3161             {
3162                 printf("\n[CANCEL] User cancellation requested during XISO lead-in padding at output LBA %d.\n", p);
3163                 goto cleanup;
3164             }
3165
3166             if (!WriteOutputBytes(outFile, zeroSector, 2048, "XISO-PAD", 0, (uint32_t)p))
3167                 goto cleanup;
3168             CryptHashData(hHash, zeroSector, 2048, 0);
3169         }
3170
3171         dumpOk = DumpSectorRangeWithRetry(hDevice, outFile, hHash, startLba, sectorsToRead, 32, "XISO");
3172     }
3173     else
3174     {
3175         printf("[ERROR] Unsupported dump mode '%c'.\n", xisoFormat);
3176         goto cleanup;
3177     }
3178
3179     if (!dumpOk)
3180         goto cleanup;
3181
3182     if (!FlushAndCommitOutput(outFile, outputLabel))
3183     {
3184         dumpOk = FALSE;
3185         goto cleanup;
3186     }
3187
3188     if (fclose(outFile) != 0)
3189     {
3190         printf("\n[FATAL] fclose failed for %s output.\n", outputLabel ? outputLabel : "dump");
3191         if (errno)
3192             printf("        errno: %d (%s)\n", errno, strerror(errno));
3193         outFile = NULL;
3194         dumpOk = FALSE;
3195         goto cleanup;
3196     }
3197     outFile = NULL;
3198
3199     if (!VerifyOutputByteCount(filename, expectedOutputBytes, outputLabel))
3200     {
3201         dumpOk = FALSE;
3202         goto cleanup;
3203     }
3204
3205     FormatElapsedTime(GetTickCount() - operationStartTick, operationTimeStr);
3206     printf("\nDump/write phase complete at elapsed %s. Finalizing hashes...\n", operationTimeStr);
3207
3208     if (CryptGetHashParam(hHash, HP_HASHVAL, rgbHash, &cbHash, 0))
3209     {
3210         HexBytesToString(rgbHash, cbHash, sha1String, sizeof(sha1String));
3211     }
3212
3213     if (CalculateFileHashes(filename, crc32String, sizeof(crc32String), md5String, sizeof(md5String), fileSha1String, sizeof(fileSha1String), fileSha256String, sizeof(fileSha256String)))
3214     {
3215         if (fileSha1String[0] && sha1String[0] && strcmp(fileSha1String, sha1String) != 0)
3216         {
3217             printf("\n[WARN] Streaming SHA-1 differs from file SHA-1. Using file SHA-1 in metadata.\n");
3218             printf("       Streaming SHA-1: %s\n", sha1String);
3219             printf("       File SHA-1:      %s\n", fileSha1String);
3220         }
3221         if (fileSha1String[0])
3222             strcpy(sha1String, fileSha1String);
3223     }
3224     else
3225     {
3226         printf("\n[WARN] Could not calculate CRC32/MD5/SHA-1/SHA-256 from finalized output file.\n");
3227     }
3228
3229     if (result) {
3230         result->output_size = GetFileSizeBytes64(filename);
3231         strncpy(result->crc32, crc32String, sizeof(result->crc32) - 1);
3232         result->crc32[sizeof(result->crc32) - 1] = '\0';
3233         strncpy(result->md5, md5String, sizeof(result->md5) - 1);
3234         result->md5[sizeof(result->md5) - 1] = '\0';
3235         strncpy(result->sha1, sha1String, sizeof(result->sha1) - 1);
3236         result->sha1[sizeof(result->sha1) - 1] = '\0';
3237         strncpy(result->sha256, fileSha256String, sizeof(result->sha256) - 1);
3238         result->sha256[sizeof(result->sha256) - 1] = '\0';
3239         result->hashes_complete = result->crc32[0] && result->md5[0] && result->sha1[0] && result->sha256[0];
3240     }
3241
3242     if (xisoFormat == '1')
3243     {
3244         printf("Final RAW ISO Sector Count: %u\n", rawTargetSectors);
3245         printf("Final RAW ISO Byte Count: %llu\n", (unsigned long long)rawTargetSectors * 2048ULL);
3246         printf("CRC32: %s\n", crc32String);
3247         printf("MD5:   %s\n", md5String);
3248         printf("SHA-1: %s\n", sha1String);
3249         printf("SHA-256: %s\n", fileSha256String);
3250         PrintGamePartitionHash(filename);
3251         if (rawSidecarAvailable)
3252         {
3253             if (!WriteXboxDvdSidecarFiles(filename,
3254                                           &sidecarCapture,
3255                                           rawTargetSectors,
3256                                           isDualLayer,
3257                                           rawVideoSectors,
3258                                           rawGameSourceLba,
3259                                           rawGameSectors,
3260                                           sha1String,
3261                                           md5String,
3262                                           crc32String,
3263                                           fileSha256String))
3264             {
3265                 printf("[WARN] Failed to write one or more XDVD sidecar metadata files.\n");
3266             }
3267         }
3268         else
3269         {
3270             printf("[WARN] XDVD sidecar metadata was not captured for this raw dump.\n");
3271         }
3272     }
3273     else
3274     {
3275         printf("Final XISO Byte Count: see progress target above.\n");
3276         printf("CRC32: %s\n", crc32String);
3277         printf("MD5:   %s\n", md5String);
3278         printf("SHA-1: %s\n", sha1String);
3279         printf("SHA-256: %s\n", fileSha256String);
3280     }
3281     FormatElapsedTime(GetTickCount() - operationStartTick, operationTimeStr);
3282     printf("\nOperation Complete! Total elapsed: %s\n", operationTimeStr);
3283
3284     /* The caller owns final drive cleanup.  Embedded FriiDump runs issue one
3285      * STOP UNIT after Redump verification; standalone bridge runs stop the
3286      * drive in xbox_ref_gdr8050l_dump_core() before closing the handle. */
3287     if (EjectOnSuccess)
3288         ControlTray(hDevice, TRUE);
3289
3290 cleanup:
3291     if (outFile)
3292     {
3293         if (result && result->cancelled)
3294         {
3295             if (!FlushAndCommitOutput(outFile, outputLabel))
3296                 printf("\n[WARN] Could not fully flush the controlled partial %s output before hashing.\n",
3297                        outputLabel ? outputLabel : "Xbox");
3298         }
3299         if (fclose(outFile) != 0)
3300         {
3301             printf("\n[WARN] fclose failed for partial %s output.\n",
3302                    outputLabel ? outputLabel : "Xbox");
3303             if (errno)
3304                 printf("       errno: %d (%s)\n", errno, strerror(errno));
3305         }
3306         outFile = NULL;
3307     }
3308     if (hHash)
3309         CryptDestroyHash(hHash);
3310     if (hProv)
3311         CryptReleaseContext(hProv, 0);
3312     if (result) {
3313         result->dump_success = dumpOk ? 1 : 0;
3314         result->elapsed_seconds = (double)(GetTickCount() - operationStartTick) / 1000.0;
3315         if (result->output_size == 0 && filename && filename[0])
3316             result->output_size = GetFileSizeBytes64(filename);
3317         result->completed_sectors = (uint32_t)(result->output_size / 2048ULL);
3318
3319         if (result->cancelled &&
3320             result->output_size > 0 &&
3321             !result->hashes_complete)
3322         {
3323             printf("\n[HASH] Finalizing controlled partial Xbox output hashes...\n");
3324             if (CalculateFileHashes(filename,
3325                                     crc32String, sizeof(crc32String),
3326                                     md5String, sizeof(md5String),
3327                                     fileSha1String, sizeof(fileSha1String),
3328                                     fileSha256String, sizeof(fileSha256String)))
3329             {
3330                 strncpy(result->crc32, crc32String, sizeof(result->crc32) - 1);
3331                 result->crc32[sizeof(result->crc32) - 1] = '\0';
3332                 strncpy(result->md5, md5String, sizeof(result->md5) - 1);
3333                 result->md5[sizeof(result->md5) - 1] = '\0';
3334                 strncpy(result->sha1, fileSha1String, sizeof(result->sha1) - 1);
3335                 result->sha1[sizeof(result->sha1) - 1] = '\0';
3336                 strncpy(result->sha256, fileSha256String, sizeof(result->sha256) - 1);
3337                 result->sha256[sizeof(result->sha256) - 1] = '\0';
3338                 result->hashes_complete =
3339                     result->crc32[0] &&
3340                     result->md5[0] &&
3341                     result->sha1[0] &&
3342                     result->sha256[0];
3343                 if (result->hashes_complete)
3344                     printf("[OK] Controlled partial Xbox output hashes captured.\n");
3345             }
3346             else
3347             {
3348                 printf("[WARN] Controlled partial Xbox output hashing failed.\n");
3349             }
3350         }
3351     }
3352
3353     g_xbox_dump_cancel = NULL;
3354     g_xbox_dump_cancel_data = NULL;
3355     g_xbox_dump_cancel_result = NULL;
3356
3357     return dumpOk;
3358 }