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