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