diff --git a/Makefile b/Makefile index cf7f426..cf5faed 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ include $(DEVKITPRO)/libnx/switch_rules VERSION_MAJOR := 1 VERSION_MINOR := 1 -VERSION_MICRO := 1 +VERSION_MICRO := 2 APP_TITLE := nxdumptool APP_AUTHOR := MCMrARM, DarkMatterCore diff --git a/README.md b/README.md index 7d53765..e8d925c 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,26 @@ If you like my work and you'd like to support me in any way, it's not necessary, Changelog -------------- +**v1.1.2:** +* Delta fragment NCAs are now included in update NSPs dumped from SD/eMMC if the "Generate ticket-less dump" option is disabled. +* It is now possible to generate ticket-less NSP dumps from bundled updates in gamecards. Please bear in mind that this option requires the external "sdmc:/switch/prod.keys" file. +* UI tweaks: + * The application now keeps track of the selected title in SD/eMMC and "orphan" content modes when entering a menu and then going back to the list. + * After selecting a title in the SD/eMMC menu, information about content already dumped related to the selected title will now be displayed (BASE / UPD / DLC). + * Likewise, after selecting a title in the "orphan" title list (Y button), an additional line will now display if the selected title has been dumped or not. + * This also informs the user if the dumps contain console-specific data. + * Three additional entries will now be displayed in the "orphan" title list. + * Upwards and downwards arrows will now be displayed for lists that exceed the max element count. + * Because of this change, max element count for the SD/eMMC title list had to be reduced from 4 to 3. + * Leftwards and rightwards arrowheads are now displayed in menus with options. + * A "hint" message is now displayed in the "orphan" content mode to let the user know they'll be able to find gamecard updates in that section. +* If a file has been already dumped, the application will display a prompt asking the user if they want to proceed anyway or not. This doesn't apply to full HFS0/ExeFS/RomFS data dumps. +* It is now possible to jump from the first list element to the last one and viceversa using the D-Pad Up/Down and Left Stick Up/Down. The Right Stick is still used exclusively for fast scrolling and won't be affected by this change. +* Fixed a bug where NSP/ExeFS/RomFS dumping would fail if the written entry count returned by `ncmContentMetaDatabaseListApplication()` didn't match the total entry count for the selected NSP dump type. +* Fixed a bug where NSP/ExeFS/RomFS dumping would fail if an invalid title index was used with `ncmContentMetaDatabaseGet()`. + +Thanks to [Maschell](https://github.com/Maschell), [DuIslingr](https://github.com/DuIslingr) and MUXI from PSXTools forums for reporting these bugs and providing with testing! + **v1.1.1:** * Project name changed to `nxdumptool`. This is no longer a gamecard-only tool. * Added ExeFS dumping/browsing support. This feature, along with the already available RomFS options, makes the application an excellent tool for modders! diff --git a/source/dumper.c b/source/dumper.c index e24e1ac..c072d3e 100644 --- a/source/dumper.c +++ b/source/dumper.c @@ -19,6 +19,8 @@ extern FsDeviceOperator fsOperatorInstance; +extern nca_keyset_t nca_keyset; + extern u64 freeSpace; extern int breaks; @@ -44,6 +46,14 @@ extern FsStorageId *titlePatchStorageId; extern u32 titleAddOnCount; extern FsStorageId *titleAddOnStorageId; +extern u32 sdCardTitleAppCount; +extern u32 sdCardTitlePatchCount; +extern u32 sdCardTitleAddOnCount; + +extern u32 nandUserTitleAppCount; +extern u32 nandUserTitlePatchCount; +extern u32 nandUserTitleAddOnCount; + extern AppletType programAppletType; extern exefs_ctx_t exeFsContext; @@ -176,8 +186,37 @@ bool dumpCartridgeImage(bool isFat32, bool setXciArchiveBit, bool dumpCert, bool { if (setXciArchiveBit) { + // Temporary, we'll use this to check if the dump already exists (it should have the archive bit set if so) snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s.xci", XCI_DUMP_PATH, dumpName); - + } else { + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s.xc%u", XCI_DUMP_PATH, dumpName, splitIndex); + } + } else { + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s.xci", XCI_DUMP_PATH, dumpName); + } + + // Check if the dump already exists + if (checkIfFileExists(filename)) + { + // Ask the user if they want to proceed anyway + int cur_breaks = breaks; + breaks++; + + proceed = yesNoPrompt("You have already dumped this content. Do you wish to proceed anyway?"); + if (!proceed) + { + uiDrawString("Process canceled.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } else { + // Remove the prompt from the screen + breaks = cur_breaks; + uiFill(0, 8 + (breaks * (font_height + (font_height / 4))) + (font_height / 8), FB_WIDTH, FB_HEIGHT - (8 + (breaks * (font_height + (font_height / 4))) + (font_height / 8)), BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + } + } + + if (proceed) + { + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32 && setXciArchiveBit) + { // Since we may actually be dealing with an existing directory with the archive bit set or unset, let's try both // Better safe than sorry unlink(filename); @@ -187,312 +226,308 @@ bool dumpCartridgeImage(bool isFat32, bool setXciArchiveBit, bool dumpCert, bool sprintf(tmp_idx, "/%02u", splitIndex); strcat(filename, tmp_idx); - } else { - snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s.xc%u", XCI_DUMP_PATH, dumpName, splitIndex); } - } else { - snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s.xci", XCI_DUMP_PATH, dumpName); - } - - outFile = fopen(filename, "wb"); - if (outFile) - { - buf = malloc(DUMP_BUFFER_SIZE); - if (buf) + + outFile = fopen(filename, "wb"); + if (outFile) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dump procedure started. Hold %s to cancel.", NINTENDO_FONT_B); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - breaks += 2; - - if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) + buf = malloc(DUMP_BUFFER_SIZE); + if (buf) { - uiDrawString("Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dump procedure started. Hold %s to cancel.", NINTENDO_FONT_B); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks += 2; - } - - progressCtx.line_offset = (breaks + 4); - timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start)); - - for(partition = 0; partition < ISTORAGE_PARTITION_CNT; partition++) - { - n = DUMP_BUFFER_SIZE; - workaroundPartitionZeroAccess(); - - if (R_SUCCEEDED(result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle))) + if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) { - if (R_SUCCEEDED(result = fsOpenGameCardStorage(&gameCardStorage, &handle, partition))) + uiDrawString("Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; + } + + progressCtx.line_offset = (breaks + 4); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start)); + + for(partition = 0; partition < ISTORAGE_PARTITION_CNT; partition++) + { + n = DUMP_BUFFER_SIZE; + + workaroundPartitionZeroAccess(); + + if (R_SUCCEEDED(result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle))) { - for(partitionOffset = 0; partitionOffset < partitionSizes[partition]; partitionOffset += n, progressCtx.curOffset += n) + if (R_SUCCEEDED(result = fsOpenGameCardStorage(&gameCardStorage, &handle, partition))) { - uiFill(0, ((progressCtx.line_offset - 4) * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", strrchr(filename, '/' ) + 1); - uiDrawString(strbuf, 8, ((progressCtx.line_offset - 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping IStorage partition #%u...", partition); - uiDrawString(strbuf, 8, ((progressCtx.line_offset - 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - - if (DUMP_BUFFER_SIZE > (partitionSizes[partition] - partitionOffset)) n = (partitionSizes[partition] - partitionOffset); - - if (R_FAILED(result = fsStorageRead(&gameCardStorage, partitionOffset, buf, n))) + for(partitionOffset = 0; partitionOffset < partitionSizes[partition]; partitionOffset += n, progressCtx.curOffset += n) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "StorageRead failed (0x%08X) at offset 0x%016lX for partition #%u", result, partitionOffset, partition); - uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - proceed = false; - break; - } - - // Remove gamecard certificate - if (progressCtx.curOffset == 0 && !dumpCert) memset(buf + CERT_OFFSET, 0xFF, CERT_SIZE); - - if (calcCrc) - { - if (!trimDump) + uiFill(0, ((progressCtx.line_offset - 4) * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", strrchr(filename, '/' ) + 1); + uiDrawString(strbuf, 8, ((progressCtx.line_offset - 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping IStorage partition #%u...", partition); + uiDrawString(strbuf, 8, ((progressCtx.line_offset - 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + if (DUMP_BUFFER_SIZE > (partitionSizes[partition] - partitionOffset)) n = (partitionSizes[partition] - partitionOffset); + + if (R_FAILED(result = fsStorageRead(&gameCardStorage, partitionOffset, buf, n))) { - if (dumpCert) + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "StorageRead failed (0x%08X) at offset 0x%016lX for partition #%u", result, partitionOffset, partition); + uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + + // Remove gamecard certificate + if (progressCtx.curOffset == 0 && !dumpCert) memset(buf + CERT_OFFSET, 0xFF, CERT_SIZE); + + if (calcCrc) + { + if (!trimDump) { - if (progressCtx.curOffset == 0) + if (dumpCert) { - // Update CRC32 (with gamecard certificate) - crc32(buf, n, &crc1); - - // Backup gamecard certificate to an array - char tmpCert[CERT_SIZE] = {'\0'}; - memcpy(tmpCert, buf + CERT_OFFSET, CERT_SIZE); - - // Remove gamecard certificate from buffer - memset(buf + CERT_OFFSET, 0xFF, CERT_SIZE); - - // Update CRC32 (without gamecard certificate) - crc32(buf, n, &crc2); - - // Restore gamecard certificate to buffer - memcpy(buf + CERT_OFFSET, tmpCert, CERT_SIZE); + if (progressCtx.curOffset == 0) + { + // Update CRC32 (with gamecard certificate) + crc32(buf, n, &crc1); + + // Backup gamecard certificate to an array + char tmpCert[CERT_SIZE] = {'\0'}; + memcpy(tmpCert, buf + CERT_OFFSET, CERT_SIZE); + + // Remove gamecard certificate from buffer + memset(buf + CERT_OFFSET, 0xFF, CERT_SIZE); + + // Update CRC32 (without gamecard certificate) + crc32(buf, n, &crc2); + + // Restore gamecard certificate to buffer + memcpy(buf + CERT_OFFSET, tmpCert, CERT_SIZE); + } else { + // Update CRC32 (with gamecard certificate) + crc32(buf, n, &crc1); + + // Update CRC32 (without gamecard certificate) + crc32(buf, n, &crc2); + } } else { - // Update CRC32 (with gamecard certificate) - crc32(buf, n, &crc1); - - // Update CRC32 (without gamecard certificate) + // Update CRC32 crc32(buf, n, &crc2); } } else { // Update CRC32 - crc32(buf, n, &crc2); - } - } else { - // Update CRC32 - crc32(buf, n, &crc1); - } - } - - if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32 && (progressCtx.curOffset + n) >= ((splitIndex + 1) * SPLIT_FILE_XCI_PART_SIZE)) - { - u64 new_file_chunk_size = ((progressCtx.curOffset + n) - ((splitIndex + 1) * SPLIT_FILE_XCI_PART_SIZE)); - u64 old_file_chunk_size = (n - new_file_chunk_size); - - if (old_file_chunk_size > 0) - { - write_res = fwrite(buf, 1, old_file_chunk_size, outFile); - if (write_res != old_file_chunk_size) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", old_file_chunk_size, progressCtx.curOffset, splitIndex, write_res); - uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - proceed = false; - break; + crc32(buf, n, &crc1); } } - fclose(outFile); - outFile = NULL; - - if (new_file_chunk_size > 0 || (progressCtx.curOffset + n) < progressCtx.totalSize) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32 && (progressCtx.curOffset + n) >= ((splitIndex + 1) * SPLIT_FILE_XCI_PART_SIZE)) { - splitIndex++; + u64 new_file_chunk_size = ((progressCtx.curOffset + n) - ((splitIndex + 1) * SPLIT_FILE_XCI_PART_SIZE)); + u64 old_file_chunk_size = (n - new_file_chunk_size); - if (setXciArchiveBit) + if (old_file_chunk_size > 0) { - snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s.xci/%02u", XCI_DUMP_PATH, dumpName, splitIndex); - } else { - snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s.xc%u", XCI_DUMP_PATH, dumpName, splitIndex); - } - - outFile = fopen(filename, "wb"); - if (!outFile) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file for part #%u!", splitIndex); - uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - proceed = false; - break; - } - - if (new_file_chunk_size > 0) - { - write_res = fwrite(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); - if (write_res != new_file_chunk_size) + write_res = fwrite(buf, 1, old_file_chunk_size, outFile); + if (write_res != old_file_chunk_size) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, write_res); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", old_file_chunk_size, progressCtx.curOffset, splitIndex, write_res); uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); proceed = false; break; } } - } - } else { - write_res = fwrite(buf, 1, n, outFile); - if (write_res != n) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", n, progressCtx.curOffset, write_res); - uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - if ((progressCtx.curOffset + n) > FAT32_FILESIZE_LIMIT) + fclose(outFile); + outFile = NULL; + + if (new_file_chunk_size > 0 || (progressCtx.curOffset + n) < progressCtx.totalSize) { - uiDrawString("You're probably using a FAT32 partition. Make sure to enable the \"Split output dump\" option.", 8, ((progressCtx.line_offset + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - fat32_error = true; + splitIndex++; + + if (setXciArchiveBit) + { + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s.xci/%02u", XCI_DUMP_PATH, dumpName, splitIndex); + } else { + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s.xc%u", XCI_DUMP_PATH, dumpName, splitIndex); + } + + outFile = fopen(filename, "wb"); + if (!outFile) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file for part #%u!", splitIndex); + uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + + if (new_file_chunk_size > 0) + { + write_res = fwrite(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); + if (write_res != new_file_chunk_size) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, write_res); + uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + } + } + } else { + write_res = fwrite(buf, 1, n, outFile); + if (write_res != n) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", n, progressCtx.curOffset, write_res); + uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + + if ((progressCtx.curOffset + n) > FAT32_FILESIZE_LIMIT) + { + uiDrawString("You're probably using a FAT32 partition. Make sure to enable the \"Split output dump\" option.", 8, ((progressCtx.line_offset + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + fat32_error = true; + } + + proceed = false; + break; } - - proceed = false; - break; } - } - - printProgressBar(&progressCtx, true, n); - - if ((progressCtx.curOffset + n) < progressCtx.totalSize && ((progressCtx.curOffset / DUMP_BUFFER_SIZE) % 10) == 0) - { - hidScanInput(); - u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO); - if (keysDown & KEY_B) + printProgressBar(&progressCtx, true, n); + + if ((progressCtx.curOffset + n) < progressCtx.totalSize && ((progressCtx.curOffset / DUMP_BUFFER_SIZE) % 10) == 0) { - uiDrawString("Process canceled.", 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - proceed = false; - break; + hidScanInput(); + + u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO); + if (keysDown & KEY_B) + { + uiDrawString("Process canceled.", 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } } } + + if (progressCtx.curOffset >= progressCtx.totalSize) success = true; + + // Support empty files + if (!partitionSizes[partition]) + { + uiFill(0, ((progressCtx.line_offset - 4) * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", strrchr(filename, '/' ) + 1); + uiDrawString(strbuf, 8, ((progressCtx.line_offset - 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping IStorage partition #%u...", partition); + uiDrawString(strbuf, 8, ((progressCtx.line_offset - 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + printProgressBar(&progressCtx, false, 0); + } + + fsStorageClose(&gameCardStorage); + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "OpenGameCardStorage failed for partition #%u! (0x%08X)", partition, result); + uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; } - - if (progressCtx.curOffset >= progressCtx.totalSize) success = true; - - // Support empty files - if (!partitionSizes[partition]) - { - uiFill(0, ((progressCtx.line_offset - 4) * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", strrchr(filename, '/' ) + 1); - uiDrawString(strbuf, 8, ((progressCtx.line_offset - 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping IStorage partition #%u...", partition); - uiDrawString(strbuf, 8, ((progressCtx.line_offset - 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - - printProgressBar(&progressCtx, false, 0); - } - - fsStorageClose(&gameCardStorage); } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "OpenGameCardStorage failed for partition #%u! (0x%08X)", partition, result); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "GetGameCardHandle failed for partition #%u! (0x%08X)", partition, result); uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); proceed = false; } - } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "GetGameCardHandle failed for partition #%u! (0x%08X)", partition, result); - uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - proceed = false; + + if (!proceed) + { + setProgressBarError(&progressCtx); + if (fat32_error) breaks += 2; + break; + } } - if (!proceed) - { - setProgressBarError(&progressCtx); - if (fat32_error) breaks += 2; - break; - } + free(buf); + + breaks = (progressCtx.line_offset + 2); + } else { + uiDrawString("Failed to allocate memory for the dump process!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } - free(buf); + if (outFile) fclose(outFile); - breaks = (progressCtx.line_offset + 2); - } else { - uiDrawString("Failed to allocate memory for the dump process!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - } - - if (outFile) fclose(outFile); - - if (success) - { - timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now)); - progressCtx.now -= progressCtx.start; - - formatETAString(progressCtx.now, progressCtx.etaInfo, sizeof(progressCtx.etaInfo) / sizeof(progressCtx.etaInfo[0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", progressCtx.etaInfo); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); - - if (calcCrc) + if (success) { - breaks++; + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now)); + progressCtx.now -= progressCtx.start; - if (!trimDump) + formatETAString(progressCtx.now, progressCtx.etaInfo, sizeof(progressCtx.etaInfo) / sizeof(progressCtx.etaInfo[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", progressCtx.etaInfo); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + + if (calcCrc) { - if (dumpCert) + breaks++; + + if (!trimDump) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XCI dump CRC32 checksum (with certificate): %08X", crc1); + if (dumpCert) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XCI dump CRC32 checksum (with certificate): %08X", crc1); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + breaks++; + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XCI dump CRC32 checksum (without certificate): %08X", crc2); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XCI dump CRC32 checksum: %08X", crc2); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + } + + breaks += 2; + uiDrawString("Starting verification process using XML database from NSWDB.COM...", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks++; + + uiRefreshDisplay(); + + gameCardDumpNSWDBCheck(crc2); + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XCI dump CRC32 checksum: %08X", crc1); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); breaks++; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XCI dump CRC32 checksum (without certificate): %08X", crc2); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); - } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XCI dump CRC32 checksum: %08X", crc2); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + uiDrawString("Dump verification disabled (not compatible with trimmed dumps).", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); } - - breaks += 2; - uiDrawString("Starting verification process using XML database from NSWDB.COM...", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - breaks++; - - uiRefreshDisplay(); - - gameCardDumpNSWDBCheck(crc2); - } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XCI dump CRC32 checksum: %08X", crc1); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); - breaks++; - - uiDrawString("Dump verification disabled (not compatible with trimmed dumps).", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); } - } - - // Set archive bit (only for FAT32 and if the required option is enabled) - if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32 && setXciArchiveBit) - { - snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s.xci", XCI_DUMP_PATH, dumpName); - if (R_FAILED(result = fsdevSetArchiveBit(filename))) - { - breaks += 2; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Warning: failed to set archive bit on output directory! (0x%08X)", result); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - } - } - } else { - if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32) - { - if (setXciArchiveBit) + + // Set archive bit (only for FAT32 and if the required option is enabled) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32 && setXciArchiveBit) { snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s.xci", XCI_DUMP_PATH, dumpName); - removeDirectory(filename); - } else { - for(u8 i = 0; i <= splitIndex; i++) + if (R_FAILED(result = fsdevSetArchiveBit(filename))) { - snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s.xc%u", XCI_DUMP_PATH, dumpName, i); - unlink(filename); + breaks += 2; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Warning: failed to set archive bit on output directory! (0x%08X)", result); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } } else { - unlink(filename); + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32) + { + if (setXciArchiveBit) + { + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s.xci", XCI_DUMP_PATH, dumpName); + removeDirectory(filename); + } else { + for(u8 i = 0; i <= splitIndex; i++) + { + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s.xc%u", XCI_DUMP_PATH, dumpName, i); + unlink(filename); + } + } + } else { + unlink(filename); + } } + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file \"%s\"!", filename); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } - } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file \"%s\"!", filename); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } else { uiDrawString("Error: not enough free space available in the SD card.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); @@ -512,6 +547,8 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd u32 i = 0, j = 0; u32 written = 0; u32 total = 0; + u32 titleCount = 0; + u32 ncmTitleIndex = 0; u32 titleNcaCount = 0; u32 partition = 0; @@ -536,7 +573,6 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd NcmApplicationContentMetaKey *titleList = NULL; NcmContentRecord *titleContentRecords = NULL; size_t titleListSize = sizeof(NcmApplicationContentMetaKey); - titleListSize *= (selectedNspDumpType == DUMP_APP_NSP ? titleAppCount : (selectedNspDumpType == DUMP_PATCH_NSP ? titlePatchCount : titleAddOnCount)); cnmt_xml_program_info xml_program_info; cnmt_xml_content_info *xml_content_info = NULL; @@ -589,7 +625,7 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd u8 *buf = NULL; u8 splitIndex = 0; u32 crc = 0; - bool proceed = true, success = false, dumping = false, fat32_error = false; + bool proceed = true, success = false, dumping = false, fat32_error = false, removeFile = true; progress_ctx_t progressCtx; memset(&progressCtx, 0, sizeof(progress_ctx_t)); @@ -600,20 +636,6 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd int initial_breaks = breaks; - if ((selectedNspDumpType == DUMP_APP_NSP && !titleAppCount) || (selectedNspDumpType == DUMP_PATCH_NSP && !titlePatchCount) || (selectedNspDumpType == DUMP_ADDON_NSP && !titleAddOnCount)) - { - uiDrawString("Error: invalid title type count!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - breaks += 2; - return false; - } - - if ((selectedNspDumpType == DUMP_APP_NSP && titleIndex > (titleAppCount - 1)) || (selectedNspDumpType == DUMP_PATCH_NSP && titleIndex > (titlePatchCount - 1)) || (selectedNspDumpType == DUMP_ADDON_NSP && titleIndex > (titleAddOnCount - 1))) - { - uiDrawString("Error: invalid title index!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - breaks += 2; - return false; - } - if ((selectedNspDumpType == DUMP_APP_NSP && !titleAppStorageId) || (selectedNspDumpType == DUMP_PATCH_NSP && !titlePatchStorageId) || (selectedNspDumpType == DUMP_ADDON_NSP && !titleAddOnStorageId)) { uiDrawString("Error: title storage ID unavailable!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); @@ -630,6 +652,40 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd return false; } + switch(curStorageId) + { + case FsStorageId_GameCard: + titleCount = (selectedNspDumpType == DUMP_APP_NSP ? titleAppCount : (selectedNspDumpType == DUMP_PATCH_NSP ? titlePatchCount : titleAddOnCount)); + ncmTitleIndex = titleIndex; + break; + case FsStorageId_SdCard: + titleCount = (selectedNspDumpType == DUMP_APP_NSP ? sdCardTitleAppCount : (selectedNspDumpType == DUMP_PATCH_NSP ? sdCardTitlePatchCount : sdCardTitleAddOnCount)); + ncmTitleIndex = titleIndex; + break; + case FsStorageId_NandUser: + titleCount = (selectedNspDumpType == DUMP_APP_NSP ? nandUserTitleAppCount : (selectedNspDumpType == DUMP_PATCH_NSP ? nandUserTitlePatchCount : nandUserTitleAddOnCount)); + ncmTitleIndex = (titleIndex - (selectedNspDumpType == DUMP_APP_NSP ? sdCardTitleAppCount : (selectedNspDumpType == DUMP_PATCH_NSP ? sdCardTitlePatchCount : sdCardTitleAddOnCount))); + break; + default: + break; + } + + if (!titleCount) + { + uiDrawString("Error: invalid title type count!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; + return false; + } + + if (ncmTitleIndex > (titleCount - 1)) + { + uiDrawString("Error: invalid title index!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; + return false; + } + + titleListSize *= titleCount; + char *dumpName = generateNSPDumpName(selectedNspDumpType, titleIndex); if (!dumpName) { @@ -639,7 +695,7 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd } // If we're dealing with a gamecard, call workaroundPartitionZeroAccess() and read the secure partition header. Otherwise, ncmContentStorageReadContentIdFile() will fail with error 0x00171002 - // Also open an IStorage instance to the secure partition, since we may need it if we're dealing with a Patch with titlekey crypto + // Also open an IStorage instance for the HFS0 Secure partition, since we may also need it if we're dealing with a Patch with titlekey crypto, in order to retrieve the tik file if (curStorageId == FsStorageId_GameCard) { partition = (hfs0_partition_cnt - 1); // Select the secure partition @@ -702,14 +758,14 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd goto out; } - if (written != total || (selectedNspDumpType == DUMP_APP_NSP && written != titleAppCount) || (selectedNspDumpType == DUMP_PATCH_NSP && written != titlePatchCount) || (selectedNspDumpType == DUMP_ADDON_NSP && written != titleAddOnCount)) + if (written != total) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: title count mismatch in ncmContentMetaDatabaseListApplication (%u != %u)", written, (selectedNspDumpType == DUMP_APP_NSP ? titleAppCount : (selectedNspDumpType == DUMP_PATCH_NSP ? titlePatchCount : titleAddOnCount))); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: title count mismatch in ncmContentMetaDatabaseListApplication (%u != %u)", written, total); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } - if (R_FAILED(result = ncmContentMetaDatabaseGet(&ncmDb, &(titleList[titleIndex].metaRecord), sizeof(NcmContentMetaRecordsHeader), &contentRecordsHeader, &contentRecordsHeaderReadSize))) + if (R_FAILED(result = ncmContentMetaDatabaseGet(&ncmDb, &(titleList[ncmTitleIndex].metaRecord), sizeof(NcmContentMetaRecordsHeader), &contentRecordsHeader, &contentRecordsHeaderReadSize))) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentMetaDatabaseGet failed! (0x%08X)", result); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); @@ -725,7 +781,7 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd goto out; } - if (R_FAILED(result = ncmContentMetaDatabaseListContentInfo(&ncmDb, &(titleList[titleIndex].metaRecord), 0, titleContentRecords, titleNcaCount * sizeof(NcmContentRecord), &written))) + if (R_FAILED(result = ncmContentMetaDatabaseListContentInfo(&ncmDb, &(titleList[ncmTitleIndex].metaRecord), 0, titleContentRecords, titleNcaCount * sizeof(NcmContentRecord), &written))) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentMetaDatabaseListContentInfo failed! (0x%08X)", result); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); @@ -734,9 +790,9 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd // Fill information for our CNMT XML memset(&xml_program_info, 0, sizeof(cnmt_xml_program_info)); - xml_program_info.type = titleList[titleIndex].metaRecord.type; - xml_program_info.title_id = titleList[titleIndex].metaRecord.titleId; - xml_program_info.version = titleList[titleIndex].metaRecord.version; + xml_program_info.type = titleList[ncmTitleIndex].metaRecord.type; + xml_program_info.title_id = titleList[ncmTitleIndex].metaRecord.titleId; + xml_program_info.version = titleList[ncmTitleIndex].metaRecord.version; xml_program_info.nca_cnt = titleNcaCount; xml_content_info = calloc(titleNcaCount, sizeof(cnmt_xml_content_info)); @@ -765,8 +821,8 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd continue; } - // Skip Delta Fragments or any other unknown content types - if (titleContentRecords[titleRecordIndex].type >= NCA_CONTENT_TYPE_DELTA) + // Skip Delta Fragments or any other unknown content types if we're not dealing with Patch titles dumped from installed SD/eMMC (with tiklessDump disabled) + if (titleContentRecords[titleRecordIndex].type >= NCA_CONTENT_TYPE_DELTA && (curStorageId == FsStorageId_GameCard || selectedNspDumpType != DUMP_PATCH_NSP || tiklessDump)) { xml_program_info.nca_cnt--; i--; @@ -784,7 +840,7 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, ncaHeader, NCA_FULL_HEADER_LENGTH))) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentStorageReadContentIdFile failed for NCA \"%s\"! (0x%08X)", xml_content_info[i].nca_id_str, result); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to read header from NCA \"%s\"! (0x%08X)", xml_content_info[i].nca_id_str, result); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); proceed = false; break; @@ -840,8 +896,54 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd { if (!has_rights_id) { - // We're most likely dealing with a custom XCI mounted through SX OS, so let's change back the content distribution method + // We're could be dealing with a custom XCI mounted through SX OS, so let's change back the content distribution method dec_nca_header.distribution = 0; + } else { + if (!rights_info.retrieved_tik) + { + // Retrieve tik file + if (!getPartitionHfs0FileByName(&gameCardStorage, rights_info.tik_filename, (u8*)(&(rights_info.tik_data)), ETICKET_TIK_FILE_SIZE)) + { + proceed = false; + break; + } + + memcpy(rights_info.enc_titlekey, rights_info.tik_data.titlekey_block, 0x10); + + rights_info.retrieved_tik = true; + } + + // Mess with the NCA header if we're dealing with a content with a populated rights ID field and if tiklessDump is true (removeConsoleData is ignored) + if (tiklessDump) + { + // Load external keys + if (!loadExternalKeys()) + { + proceed = false; + break; + } + + // Decrypt titlekey + u8 crypto_type = xml_content_info[i].keyblob; + if (crypto_type) crypto_type--; + + Aes128Context titlekey_aes_ctx; + aes128ContextCreate(&titlekey_aes_ctx, nca_keyset.titlekeks[crypto_type], false); + aes128DecryptBlock(&titlekey_aes_ctx, rights_info.dec_titlekey, rights_info.enc_titlekey); + + memset(xml_content_info[i].decrypted_nca_keys, 0, NCA_KEY_AREA_SIZE); + memcpy(xml_content_info[i].decrypted_nca_keys + (NCA_KEY_AREA_KEY_SIZE * 2), rights_info.dec_titlekey, 0x10); + + // Generate new encrypted NCA key area using titlekey + if (!generateEncryptedNcaKeyAreaWithTitlekey(&dec_nca_header, xml_content_info[i].decrypted_nca_keys)) + { + proceed = false; + break; + } + + // Remove rights ID from NCA + memset(dec_nca_header.rights_id, 0, 0x10); + } } } } else @@ -914,7 +1016,7 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, cnmtNcaBuf, xml_content_info[cnmtNcaIndex].size))) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentStorageReadContentIdFile failed for CNMT NCA \"%s\"! (0x%08X)", xml_content_info[cnmtNcaIndex].nca_id_str, result); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to read CNMT NCA \"%s\"! (0x%08X)", xml_content_info[cnmtNcaIndex].nca_id_str, result); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } @@ -938,15 +1040,12 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd generateCnmtXml(&xml_program_info, xml_content_info, cnmtXml); - bool includeCertAndTik = (rights_info.has_rights_id && (curStorageId == FsStorageId_GameCard || (curStorageId != FsStorageId_GameCard && !tiklessDump))); + bool includeCertAndTik = (rights_info.has_rights_id && !tiklessDump); if (includeCertAndTik) { if (curStorageId == FsStorageId_GameCard) { - // Retrieve tik file - if (!getPartitionHfs0FileByName(&gameCardStorage, rights_info.tik_filename, (u8*)(&(rights_info.tik_data)), ETICKET_TIK_FILE_SIZE)) goto out; - // Ticket files from Patch titles bundled with gamecards have a different layout // Let's convert it to a normal "common" ticket memset(rights_info.tik_data.signature, 0xFF, 0x100); @@ -1057,8 +1156,29 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd goto out; } + // Temporary, we'll use this to check if the dump already exists (it should have the archive bit set if so) snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "%s%s.nsp", NSP_DUMP_PATH, dumpName); + // Check if the dump already exists + if (checkIfFileExists(dumpPath)) + { + // Ask the user if they want to proceed anyway + int cur_breaks = breaks; + breaks++; + + proceed = yesNoPrompt("You have already dumped this content. Do you wish to proceed anyway?"); + if (!proceed) + { + uiDrawString("Process canceled.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + removeFile = false; + goto out; + } else { + // Remove the prompt from the screen + breaks = cur_breaks; + uiFill(0, 8 + (breaks * (font_height + (font_height / 4))) + (font_height / 8), FB_WIDTH, FB_HEIGHT - (8 + (breaks * (font_height + (font_height / 4))) + (font_height / 8)), BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + } + } + // Since we may actually be dealing with an existing directory with the archive bit set or unset, let's try both // Better safe than sorry unlink(dumpPath); @@ -1185,7 +1305,7 @@ bool dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleInd if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, nca_offset, buf, n))) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentStorageReadContentIdFile failed (0x%08X) at offset 0x%016lX for NCA \"%s\".", result, nca_offset, xml_content_info[i].nca_id_str); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to read %lu bytes chunk at offset 0x%016lX from NCA \"%s\"! (0x%08X)", n, nca_offset, xml_content_info[i].nca_id_str, result); uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); proceed = false; break; @@ -1777,13 +1897,16 @@ out: if (fat32_error) breaks += 2; } - snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "%s%s.nsp", NSP_DUMP_PATH, dumpName); - - if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32) + if (removeFile) { - removeDirectory(dumpPath); - } else { - unlink(dumpPath); + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "%s%s.nsp", NSP_DUMP_PATH, dumpName); + + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32) + { + removeDirectory(dumpPath); + } else { + unlink(dumpPath); + } } } @@ -1839,7 +1962,7 @@ bool dumpRawHfs0Partition(u32 partition, bool doSplitting) { Result result; u64 partitionOffset; - bool success = false, fat32_error = false; + bool proceed = true, success = false, fat32_error = false; u8 *buf = NULL; u64 n = DUMP_BUFFER_SIZE; FsGameCardHandle handle; @@ -1904,171 +2027,191 @@ bool dumpRawHfs0Partition(u32 partition, bool doSplitting) snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s - Partition %u (%s).hfs0", HFS0_DUMP_PATH, dumpName, partition, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, partition)); } - outFile = fopen(filename, "wb"); - if (outFile) + // Check if the dump already exists + if (checkIfFileExists(filename)) { - buf = malloc(DUMP_BUFFER_SIZE); - if (buf) + // Ask the user if they want to proceed anyway + int cur_breaks = breaks; + + proceed = yesNoPrompt("You have already dumped this content. Do you wish to proceed anyway?"); + if (!proceed) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping raw HFS0 partition #%u. Hold %s to cancel.", partition, NINTENDO_FONT_B); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - breaks += 2; - - if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) + uiDrawString("Process canceled.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } else { + // Remove the prompt from the screen + breaks = cur_breaks; + uiFill(0, 8 + (breaks * (font_height + (font_height / 4))) + (font_height / 8), FB_WIDTH, FB_HEIGHT - (8 + (breaks * (font_height + (font_height / 4))) + (font_height / 8)), BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + } + } + + if (proceed) + { + outFile = fopen(filename, "wb"); + if (outFile) + { + buf = malloc(DUMP_BUFFER_SIZE); + if (buf) { - uiDrawString("Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping raw HFS0 partition #%u. Hold %s to cancel.", partition, NINTENDO_FONT_B); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks += 2; - } - - uiRefreshDisplay(); - - progressCtx.line_offset = (breaks + 2); - timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start)); - - for (progressCtx.curOffset = 0; progressCtx.curOffset < progressCtx.totalSize; progressCtx.curOffset += n) - { - uiFill(0, ((progressCtx.line_offset - 2) * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 2, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", strrchr(filename, '/' ) + 1); - uiDrawString(strbuf, 8, ((progressCtx.line_offset - 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - - if (DUMP_BUFFER_SIZE > (progressCtx.totalSize - progressCtx.curOffset)) n = (progressCtx.totalSize - progressCtx.curOffset); - - if (R_FAILED(result = fsStorageRead(&gameCardStorage, partitionOffset + progressCtx.curOffset, buf, n))) + if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "StorageRead failed (0x%08X) at offset 0x%016lX", result, partitionOffset + progressCtx.curOffset); - uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - break; + uiDrawString("Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; } - if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting && (progressCtx.curOffset + n) >= ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)) + uiRefreshDisplay(); + + progressCtx.line_offset = (breaks + 2); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start)); + + for (progressCtx.curOffset = 0; progressCtx.curOffset < progressCtx.totalSize; progressCtx.curOffset += n) { - u64 new_file_chunk_size = ((progressCtx.curOffset + n) - ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)); - u64 old_file_chunk_size = (n - new_file_chunk_size); + uiFill(0, ((progressCtx.line_offset - 2) * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 2, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - if (old_file_chunk_size > 0) + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", strrchr(filename, '/' ) + 1); + uiDrawString(strbuf, 8, ((progressCtx.line_offset - 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + if (DUMP_BUFFER_SIZE > (progressCtx.totalSize - progressCtx.curOffset)) n = (progressCtx.totalSize - progressCtx.curOffset); + + if (R_FAILED(result = fsStorageRead(&gameCardStorage, partitionOffset + progressCtx.curOffset, buf, n))) { - write_res = fwrite(buf, 1, old_file_chunk_size, outFile); - if (write_res != old_file_chunk_size) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", old_file_chunk_size, progressCtx.curOffset, splitIndex, write_res); - uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - break; - } + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "StorageRead failed (0x%08X) at offset 0x%016lX", result, partitionOffset + progressCtx.curOffset); + uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + break; } - fclose(outFile); - outFile = NULL; - - if (new_file_chunk_size > 0 || (progressCtx.curOffset + n) < progressCtx.totalSize) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting && (progressCtx.curOffset + n) >= ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)) { - splitIndex++; - snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s - Partition %u (%s).hfs0.%02u", HFS0_DUMP_PATH, dumpName, partition, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, partition), splitIndex); + u64 new_file_chunk_size = ((progressCtx.curOffset + n) - ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)); + u64 old_file_chunk_size = (n - new_file_chunk_size); - outFile = fopen(filename, "wb"); - if (!outFile) + if (old_file_chunk_size > 0) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file for part #%u!", splitIndex); - uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - break; - } - - if (new_file_chunk_size > 0) - { - write_res = fwrite(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); - if (write_res != new_file_chunk_size) + write_res = fwrite(buf, 1, old_file_chunk_size, outFile); + if (write_res != old_file_chunk_size) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, write_res); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", old_file_chunk_size, progressCtx.curOffset, splitIndex, write_res); uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); break; } } + + fclose(outFile); + outFile = NULL; + + if (new_file_chunk_size > 0 || (progressCtx.curOffset + n) < progressCtx.totalSize) + { + splitIndex++; + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s - Partition %u (%s).hfs0.%02u", HFS0_DUMP_PATH, dumpName, partition, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, partition), splitIndex); + + outFile = fopen(filename, "wb"); + if (!outFile) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file for part #%u!", splitIndex); + uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + break; + } + + if (new_file_chunk_size > 0) + { + write_res = fwrite(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); + if (write_res != new_file_chunk_size) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, write_res); + uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + break; + } + } + } + } else { + write_res = fwrite(buf, 1, n, outFile); + if (write_res != n) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", n, progressCtx.curOffset, write_res); + uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + + if ((progressCtx.curOffset + n) > FAT32_FILESIZE_LIMIT) + { + uiDrawString("You're probably using a FAT32 partition. Make sure to enable file splitting.", 8, ((progressCtx.line_offset + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + fat32_error = true; + } + + break; + } + } + + printProgressBar(&progressCtx, true, n); + + if ((progressCtx.curOffset + n) < progressCtx.totalSize && ((progressCtx.curOffset / DUMP_BUFFER_SIZE) % 10) == 0) + { + hidScanInput(); + + u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO); + if (keysDown & KEY_B) + { + uiDrawString("Process canceled.", 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + break; + } + } + } + + if (progressCtx.curOffset >= progressCtx.totalSize) success = true; + + // Support empty files + if (!progressCtx.totalSize) + { + uiFill(0, ((progressCtx.line_offset - 2) * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 2, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", strrchr(filename, '/' ) + 1); + uiDrawString(strbuf, 8, ((progressCtx.line_offset - 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + progressCtx.progress = 100; + + printProgressBar(&progressCtx, false, 0); + } + + breaks = (progressCtx.line_offset + 2); + + if (success) + { + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now)); + progressCtx.now -= progressCtx.start; + + formatETAString(progressCtx.now, progressCtx.etaInfo, sizeof(progressCtx.etaInfo) / sizeof(progressCtx.etaInfo[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", progressCtx.etaInfo); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + } else { + setProgressBarError(&progressCtx); + if (fat32_error) breaks += 2; + } + + free(buf); + } else { + uiDrawString("Failed to allocate memory for the dump process!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } + + if (outFile) fclose(outFile); + + if (!success) + { + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting) + { + for(u8 i = 0; i <= splitIndex; i++) + { + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s - Partition %u (%s).hfs0.%02u", HFS0_DUMP_PATH, dumpName, partition, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, partition), i); + unlink(filename); } } else { - write_res = fwrite(buf, 1, n, outFile); - if (write_res != n) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", n, progressCtx.curOffset, write_res); - uiDrawString(strbuf, 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - - if ((progressCtx.curOffset + n) > FAT32_FILESIZE_LIMIT) - { - uiDrawString("You're probably using a FAT32 partition. Make sure to enable file splitting.", 8, ((progressCtx.line_offset + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - fat32_error = true; - } - - break; - } - } - - printProgressBar(&progressCtx, true, n); - - if ((progressCtx.curOffset + n) < progressCtx.totalSize && ((progressCtx.curOffset / DUMP_BUFFER_SIZE) % 10) == 0) - { - hidScanInput(); - - u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO); - if (keysDown & KEY_B) - { - uiDrawString("Process canceled.", 8, ((progressCtx.line_offset + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - break; - } - } - } - - if (progressCtx.curOffset >= progressCtx.totalSize) success = true; - - // Support empty files - if (!progressCtx.totalSize) - { - uiFill(0, ((progressCtx.line_offset - 2) * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 2, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", strrchr(filename, '/' ) + 1); - uiDrawString(strbuf, 8, ((progressCtx.line_offset - 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - - progressCtx.progress = 100; - - printProgressBar(&progressCtx, false, 0); - } - - breaks = (progressCtx.line_offset + 2); - - if (success) - { - timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now)); - progressCtx.now -= progressCtx.start; - - formatETAString(progressCtx.now, progressCtx.etaInfo, sizeof(progressCtx.etaInfo) / sizeof(progressCtx.etaInfo[0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", progressCtx.etaInfo); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); - } else { - setProgressBarError(&progressCtx); - if (fat32_error) breaks += 2; - } - - free(buf); - } else { - uiDrawString("Failed to allocate memory for the dump process!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - } - - if (outFile) fclose(outFile); - - if (!success) - { - if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting) - { - for(u8 i = 0; i <= splitIndex; i++) - { - snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s - Partition %u (%s).hfs0.%02u", HFS0_DUMP_PATH, dumpName, partition, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, partition), i); unlink(filename); } - } else { - unlink(filename); } + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file \"%s\"!", filename); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } - } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file \"%s\"!", filename); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } else { uiDrawString("Error: not enough free space available in the SD card.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); @@ -2478,7 +2621,7 @@ bool dumpFileFromHfs0Partition(u32 partition, u32 file, char *filename) u64 file_offset = 0; u64 file_size = 0; - bool success = false; + bool proceed = true, success = false; if (getHfs0EntryDetails(partitionHfs0Header, partitionHfs0HeaderOffset, partitionHfs0HeaderSize, partitionHfs0FileCount, file, false, partition, &file_offset, &file_size)) { @@ -2503,36 +2646,58 @@ bool dumpFileFromHfs0Partition(u32 partition, u32 file, char *filename) { mkdir(destCopyPath, 0744); - snprintf(destCopyPath, sizeof(destCopyPath) / sizeof(destCopyPath[0]), "%s%s - Partition %u (%s)/%s", HFS0_DUMP_PATH, dumpName, partition, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, partition), fixedFilename); + strcat(destCopyPath, "/"); + strcat(destCopyPath, fixedFilename); breaks++; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Hold %s to cancel.", NINTENDO_FONT_B); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - breaks += 2; - if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) + // Check if the dump already exists + if (checkIfFileExists(destCopyPath)) { - uiDrawString("Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - breaks += 2; + // Ask the user if they want to proceed anyway + int cur_breaks = breaks; + + proceed = yesNoPrompt("You have already dumped this content. Do you wish to proceed anyway?"); + if (!proceed) + { + uiDrawString("Process canceled.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } else { + // Remove the prompt from the screen + breaks = cur_breaks; + uiFill(0, 8 + (breaks * (font_height + (font_height / 4))) + (font_height / 8), FB_WIDTH, FB_HEIGHT - (8 + (breaks * (font_height + (font_height / 4))) + (font_height / 8)), BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + } } - uiRefreshDisplay(); - - progressCtx.line_offset = (breaks + 4); - timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start)); - - success = copyFileFromHfs0(partition, filename, destCopyPath, file_offset, file_size, &progressCtx, true); - - breaks = (progressCtx.line_offset + 2); - - if (success) + if (proceed) { - timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now)); - progressCtx.now -= progressCtx.start; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Hold %s to cancel.", NINTENDO_FONT_B); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks += 2; - formatETAString(progressCtx.now, progressCtx.etaInfo, sizeof(progressCtx.etaInfo) / sizeof(progressCtx.etaInfo[0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", progressCtx.etaInfo); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) + { + uiDrawString("Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; + } + + uiRefreshDisplay(); + + progressCtx.line_offset = (breaks + 4); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start)); + + success = copyFileFromHfs0(partition, filename, destCopyPath, file_offset, file_size, &progressCtx, true); + + breaks = (progressCtx.line_offset + 2); + + if (success) + { + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now)); + progressCtx.now -= progressCtx.start; + + formatETAString(progressCtx.now, progressCtx.etaInfo, sizeof(progressCtx.etaInfo) / sizeof(progressCtx.etaInfo[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", progressCtx.etaInfo); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + } } } else { breaks++; @@ -2574,14 +2739,14 @@ bool dumpExeFsSectionData(u32 appIndex, bool doSplitting) if (!titleAppCount) { - uiDrawString("Error: invalid gamecard application count!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + uiDrawString("Error: invalid application count!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); breaks += 2; return false; } if (appIndex > (titleAppCount - 1)) { - uiDrawString("Error: invalid gamecard application index!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + uiDrawString("Error: invalid application index!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); breaks += 2; return false; } @@ -2857,7 +3022,7 @@ bool dumpFileFromExeFsSection(u32 appIndex, u32 fileIndex, bool doSplitting) FILE *outFile = NULL; u8 *buf = NULL; u8 splitIndex = 0; - bool proceed = true, success = false, fat32_error = false; + bool proceed = true, success = false, fat32_error = false, removeFile = true; char dumpPath[NAME_BUF_LEN * 2] = {'\0'}; char tmp_idx[5]; @@ -2897,12 +3062,45 @@ bool dumpFileFromExeFsSection(u32 appIndex, u32 fileIndex, bool doSplitting) strcat(dumpPath, exeFsFilename); removeIllegalCharacters(dumpPath + cur_len); + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting) + { + sprintf(tmp_idx, ".%02u", splitIndex); + strcat(dumpPath, tmp_idx); + } + progressCtx.totalSize = exeFsContext.exefs_entries[fileIndex].file_size; convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, sizeof(progressCtx.totalSizeStr) / sizeof(progressCtx.totalSizeStr[0])); snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "File size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - breaks += 2; + breaks++; + + if (progressCtx.totalSize > freeSpace) + { + uiDrawString("Error: not enough free space available in the SD card.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + + breaks++; + + // Check if the dump already exists + if (checkIfFileExists(dumpPath)) + { + // Ask the user if they want to proceed anyway + int cur_breaks = breaks; + + proceed = yesNoPrompt("You have already dumped this content. Do you wish to proceed anyway?"); + if (!proceed) + { + uiDrawString("Process canceled.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + removeFile = false; + goto out; + } else { + // Remove the prompt from the screen + breaks = cur_breaks; + uiFill(0, 8 + (breaks * (font_height + (font_height / 4))) + (font_height / 8), FB_WIDTH, FB_HEIGHT - (8 + (breaks * (font_height + (font_height / 4))) + (font_height / 8)), BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + } + } snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Hold %s to cancel.", NINTENDO_FONT_B); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); @@ -2919,12 +3117,6 @@ bool dumpFileFromExeFsSection(u32 appIndex, u32 fileIndex, bool doSplitting) uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks += 2; - if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting) - { - sprintf(tmp_idx, ".%02u", splitIndex); - strcat(dumpPath, tmp_idx); - } - outFile = fopen(dumpPath, "wb"); if (!outFile) { @@ -3076,19 +3268,22 @@ out: { if (fat32_error) breaks += 2; - if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting) + if (removeFile) { - for(u8 i = 0; i <= splitIndex; i++) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting) { - char *tmp = strrchr(dumpPath, '.'); - if (tmp != NULL) *tmp = '\0'; - - sprintf(tmp_idx, ".%02u", splitIndex); - strcat(dumpPath, tmp_idx); + for(u8 i = 0; i <= splitIndex; i++) + { + char *tmp = strrchr(dumpPath, '.'); + if (tmp != NULL) *tmp = '\0'; + + sprintf(tmp_idx, ".%02u", splitIndex); + strcat(dumpPath, tmp_idx); + unlink(dumpPath); + } + } else { unlink(dumpPath); } - } else { - unlink(dumpPath); } } @@ -3380,14 +3575,14 @@ bool dumpRomFsSectionData(u32 appIndex) if (!titleAppCount) { - uiDrawString("Error: invalid gamecard application count!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + uiDrawString("Error: invalid application count!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); breaks += 2; return false; } if (appIndex > (titleAppCount - 1)) { - uiDrawString("Error: invalid gamecard application index!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + uiDrawString("Error: invalid application index!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); breaks += 2; return false; } @@ -3487,7 +3682,7 @@ bool dumpFileFromRomFsSection(u32 appIndex, u32 file_offset, bool doSplitting) FILE *outFile = NULL; u8 *buf = NULL; u8 splitIndex = 0; - bool proceed = true, success = false, fat32_error = false; + bool proceed = true, success = false, fat32_error = false, removeFile = true; char dumpPath[NAME_BUF_LEN * 2] = {'\0'}; char tmp_idx[5]; @@ -3561,12 +3756,45 @@ bool dumpFileFromRomFsSection(u32 appIndex, u32 file_offset, bool doSplitting) strncat(dumpPath, (char*)entry->name, entry->nameLen); removeIllegalCharacters(dumpPath + cur_len); + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting) + { + sprintf(tmp_idx, ".%02u", splitIndex); + strcat(dumpPath, tmp_idx); + } + progressCtx.totalSize = entry->dataSize; convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, sizeof(progressCtx.totalSizeStr) / sizeof(progressCtx.totalSizeStr[0])); snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "File size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - breaks += 2; + breaks++; + + if (progressCtx.totalSize > freeSpace) + { + uiDrawString("Error: not enough free space available in the SD card.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + + breaks++; + + // Check if the dump already exists + if (checkIfFileExists(dumpPath)) + { + // Ask the user if they want to proceed anyway + int cur_breaks = breaks; + + proceed = yesNoPrompt("You have already dumped this content. Do you wish to proceed anyway?"); + if (!proceed) + { + uiDrawString("Process canceled.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + removeFile = false; + goto out; + } else { + // Remove the prompt from the screen + breaks = cur_breaks; + uiFill(0, 8 + (breaks * (font_height + (font_height / 4))) + (font_height / 8), FB_WIDTH, FB_HEIGHT - (8 + (breaks * (font_height + (font_height / 4))) + (font_height / 8)), BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + } + } snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Hold %s to cancel.", NINTENDO_FONT_B); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); @@ -3583,12 +3811,6 @@ bool dumpFileFromRomFsSection(u32 appIndex, u32 file_offset, bool doSplitting) uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks += 2; - if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting) - { - sprintf(tmp_idx, ".%02u", splitIndex); - strcat(dumpPath, tmp_idx); - } - outFile = fopen(dumpPath, "wb"); if (!outFile) { @@ -3740,19 +3962,22 @@ out: { if (fat32_error) breaks += 2; - if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting) + if (removeFile) { - for(u8 i = 0; i <= splitIndex; i++) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting) { - char *tmp = strrchr(dumpPath, '.'); - if (tmp != NULL) *tmp = '\0'; - - sprintf(tmp_idx, ".%02u", splitIndex); - strcat(dumpPath, tmp_idx); + for(u8 i = 0; i <= splitIndex; i++) + { + char *tmp = strrchr(dumpPath, '.'); + if (tmp != NULL) *tmp = '\0'; + + sprintf(tmp_idx, ".%02u", splitIndex); + strcat(dumpPath, tmp_idx); + unlink(dumpPath); + } + } else { unlink(dumpPath); } - } else { - unlink(dumpPath); } } @@ -3769,7 +3994,7 @@ bool dumpGameCardCertificate() Result result; FsGameCardHandle handle; FsStorage gameCardStorage; - bool success = false; + bool proceed = true, success = false; FILE *outFile = NULL; char filename[NAME_BUF_LEN * 2] = {'\0'}; u8 buf[CERT_SIZE]; @@ -3806,36 +4031,56 @@ bool dumpGameCardCertificate() snprintf(filename, sizeof(filename) / sizeof(filename[0]), "%s%s - Certificate (%08X).bin", CERT_DUMP_PATH, dumpName, crc); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping gamecard certificate to \"%s\"...", strrchr(filename, '/' ) + 1); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - breaks += 2; - - if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) + // Check if the dump already exists + if (checkIfFileExists(filename)) { - uiDrawString("Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - breaks += 2; + // Ask the user if they want to proceed anyway + int cur_breaks = breaks; + + proceed = yesNoPrompt("You have already dumped this content. Do you wish to proceed anyway?"); + if (!proceed) + { + uiDrawString("Process canceled.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } else { + // Remove the prompt from the screen + breaks = cur_breaks; + uiFill(0, 8 + (breaks * (font_height + (font_height / 4))) + (font_height / 8), FB_WIDTH, FB_HEIGHT - (8 + (breaks * (font_height + (font_height / 4))) + (font_height / 8)), BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + } } - uiRefreshDisplay(); - - outFile = fopen(filename, "wb"); - if (outFile) + if (proceed) { - write_res = fwrite(buf, 1, CERT_SIZE, outFile); - if (write_res == CERT_SIZE) + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping gamecard certificate to \"%s\"...", strrchr(filename, '/' ) + 1); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks += 2; + + if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) { - success = true; - uiDrawString("Process successfully completed!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); - } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %u bytes certificate data! (wrote %lu bytes)", CERT_SIZE, write_res); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + uiDrawString("Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; } - fclose(outFile); - if (!success) unlink(filename); - } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file \"%s\"!", filename); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + uiRefreshDisplay(); + + outFile = fopen(filename, "wb"); + if (outFile) + { + write_res = fwrite(buf, 1, CERT_SIZE, outFile); + if (write_res == CERT_SIZE) + { + success = true; + uiDrawString("Process successfully completed!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %u bytes certificate data! (wrote %lu bytes)", CERT_SIZE, write_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } + + fclose(outFile); + if (!success) unlink(filename); + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file \"%s\"!", filename); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } } } else { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "StorageRead failed (0x%08X) at offset 0x%08X", result, CERT_OFFSET); diff --git a/source/keys.c b/source/keys.c index 0bfafc5..0151f59 100644 --- a/source/keys.c +++ b/source/keys.c @@ -331,7 +331,7 @@ bool findFSRodataKeys(keyLocation *location) return true; } -bool getNcaKeys() +bool loadMemoryKeys() { if (nca_keyset.memory_key_cnt > 0) return true; @@ -630,7 +630,7 @@ int parse_hex_key(unsigned char *key, const char *hex, unsigned int len) return 1; } -int loadExternalKeys(FILE *f) +int readKeysFromFile(FILE *f) { u32 i; int ret; @@ -695,6 +695,38 @@ int loadExternalKeys(FILE *f) return 1; } +bool loadExternalKeys() +{ + // Check if the keyset has been already loaded + if (nca_keyset.ext_key_cnt > 0) return true; + + // Open keys file + FILE *keysFile = fopen(keysFilePath, "rb"); + if (!keysFile) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unable to open \"%s\" to retrieve \"eticket_rsa_kek\", titlekeks and KAEKs!", keysFilePath); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + // Load keys + int ret = readKeysFromFile(keysFile); + fclose(keysFile); + + if (ret < 1) + { + if (ret == -1) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unable to parse necessary keys from \"%s\"! (keys file empty?)", keysFilePath); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } + + return false; + } + + return true; +} + bool testKeyPair(const void *E, const void *D, const void *N) { if (!E || !D || !N) @@ -951,33 +983,8 @@ bool retrieveNcaTikTitleKey(nca_header_t *dec_nca_header, u8 *out_tik, u8 *out_e return false; } - // Retrieve external keys - if (!nca_keyset.ext_key_cnt) - { - // Open keys file - FILE *keysFile = fopen(keysFilePath, "rb"); - if (!keysFile) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unable to open \"%s\" to retrieve \"eticket_rsa_kek\" and titlekeks!", keysFilePath); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - return false; - } - - // Load keys - int ret = loadExternalKeys(keysFile); - fclose(keysFile); - - if (ret < 1) - { - if (ret == -1) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unable to parse necessary keys from \"%s\"! (keys file empty?)", keysFilePath); - uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - } - - return false; - } - } + // Load external keys + if (!loadExternalKeys()) return false; // Decrypt eTicket RSA key memcpy(ctr, eticket_data + ETICKET_DEVKEY_CTR_OFFSET, 0x10); diff --git a/source/keys.h b/source/keys.h index 557431f..8f6bb7c 100644 --- a/source/keys.h +++ b/source/keys.h @@ -64,8 +64,9 @@ typedef struct { u8 key_area_keys[0x20][3][0x10]; /* Key area encryption keys. */ } PACKED nca_keyset_t; -bool getNcaKeys(); +bool loadMemoryKeys(); bool decryptNcaKeyArea(nca_header_t *dec_nca_header, u8 *out); +bool loadExternalKeys(); bool retrieveNcaTikTitleKey(nca_header_t *dec_nca_header, u8 *out_tik, u8 *out_enc_key, u8 *out_dec_key); bool generateEncryptedNcaKeyAreaWithTitlekey(nca_header_t *dec_nca_header, u8 *decrypted_nca_keys); bool readCertsFromApplicationRomFs(); diff --git a/source/nca.c b/source/nca.c index 5f643b2..2facf08 100644 --- a/source/nca.c +++ b/source/nca.c @@ -208,7 +208,7 @@ void convertU64ToNcaSize(const u64 size, u8 out[0x6]) bool loadNcaKeyset() { - // Keyset already loaded + // Check if the keyset has been already loaded if (nca_keyset.total_key_cnt > 0) return true; if (!(envIsSyscallHinted(0x60) && // svcDebugActiveProcess @@ -221,7 +221,7 @@ bool loadNcaKeyset() return false; } - return getNcaKeys(); + return loadMemoryKeys(); } size_t aes128XtsNintendoCrypt(Aes128XtsContext *ctx, void *dst, const void *src, size_t size, u32 sector, bool encrypt) @@ -297,7 +297,7 @@ bool processNcaCtrSectionBlock(NcmContentStorage *ncmStorage, const NcmNcaId *nc if (R_FAILED(result = ncmContentStorageReadContentIdFile(ncmStorage, ncaId, block_start_offset, tmp_buf, block_size))) { free(tmp_buf); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentStorageReadContentIdFile failed for %lu bytes block at offset 0x%016lX from NCA \"%s\"! (0x%08X)", block_size, block_start_offset, nca_id, result); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to read encrypted %lu bytes block at offset 0x%016lX from NCA \"%s\"! (0x%08X)", block_size, block_start_offset, nca_id, result); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); return false; } @@ -481,6 +481,8 @@ bool decryptNcaHeader(const u8 *ncaBuf, u64 ncaBufSize, nca_header_t *out, title memset(decrypted_nca_keys, 0, NCA_KEY_AREA_SIZE); memcpy(decrypted_nca_keys + (NCA_KEY_AREA_KEY_SIZE * 2), rights_info->dec_titlekey, 0x10); + + rights_info->retrieved_tik = true; } } else { // Copy what we already have @@ -2209,7 +2211,7 @@ bool generateNacpXmlFromNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId out: // Manually free these pointers - // Calling freeRomFsContext() would also close the ncmStorage handle + // Calling freeRomFsContext() would also close the ncmStorage handle and the gamecard IStorage instance (if available) free(romFsContext.romfs_dir_entries); romFsContext.romfs_dir_entries = NULL; diff --git a/source/nca.h b/source/nca.h index e08a75a..8e30495 100644 --- a/source/nca.h +++ b/source/nca.h @@ -299,6 +299,7 @@ typedef struct { u8 dec_titlekey[0x10]; u8 cert_data[ETICKET_CERT_FILE_SIZE]; rsa2048_sha256_ticket tik_data; + bool retrieved_tik; } PACKED title_rights_ctx; typedef struct { diff --git a/source/ui.c b/source/ui.c index 2cd8b31..5a3c038 100644 --- a/source/ui.c +++ b/source/ui.c @@ -85,9 +85,18 @@ int scroll = 0; int breaks = 0; int font_height = 0; +int titleListCursor = 0; +int titleListScroll = 0; + +int orphanListCursor = 0; +int orphanListScroll = 0; + curMenuType menuType; static bool orphanMode = false; +static char titleSelectorStr[NAME_BUF_LEN] = {'\0'}; +static char dumpedNspInfoStr[NAME_BUF_LEN] = {'\0'}; + static u32 selectedAppInfoIndex = 0; static u32 selectedAppIndex; static u32 selectedPatchIndex; @@ -563,7 +572,7 @@ void uiDrawString(const char *string, int x, int y, u8 r, u8 g, u8 b) } } -u32 uiGetStrWidth(char *string) +u32 uiGetStrWidth(const char *string) { if (!string || !strlen(string)) return 0; @@ -691,6 +700,45 @@ void uiPrintHeadline() uiDrawString(appHeadline, 8, 8, 255, 255, 255); } +void uiPrintOption(int x, int y, int endPosition, bool leftArrow, bool rightArrow, int r, int g, int b) +{ + if (!strlen(strbuf) || endPosition < OPTIONS_X_END_POS || endPosition >= (FB_WIDTH - 8)) return; + + int xpos = x; + char *option = strbuf; + u32 optionStrWidth = uiGetStrWidth(option); + + if (leftArrow) uiDrawString("<", xpos, y, 255, 255, 255); + + xpos += uiGetStrWidth("<"); + + // Check if we're dealing with a long title selector string + if (strlen(option) > 3 && optionStrWidth >= (endPosition - xpos - (font_height * 2))) + { + while(optionStrWidth >= (endPosition - xpos - (font_height * 2))) + { + option++; + optionStrWidth = uiGetStrWidth(option); + } + + option[0] = option[1] = option[2] = '.'; + optionStrWidth = uiGetStrWidth(option); + + snprintf(titleSelectorStr, sizeof(titleSelectorStr) / sizeof(titleSelectorStr[0]), option); + } + + xpos += (((endPosition - xpos) / 2) - (optionStrWidth / 2)); + + uiDrawString(option, xpos, y, r, g, b); + + if (rightArrow) + { + xpos = endPosition; + + uiDrawString(">", xpos, y, 255, 255, 255); + } +} + void error_screen(const char *fmt, ...) { consoleInit(NULL); @@ -960,9 +1008,52 @@ void uiDeinit() void uiSetState(UIState state) { + if (uiState == stateSdCardEmmcMenu) + { + if (state != stateMainMenu) + { + // Store current cursor/scroll values + titleListCursor = cursor; + titleListScroll = scroll; + } else { + // Reset title list cursor/scroll values + titleListCursor = 0; + titleListScroll = 0; + } + } else + if (uiState == stateSdCardEmmcOrphanPatchAddOnMenu) + { + if (state != stateSdCardEmmcMenu) + { + // Store current cursor/scroll values + orphanListCursor = cursor; + orphanListScroll = scroll; + } else { + // Reset orphan list cursor/scroll values + orphanListCursor = 0; + orphanListScroll = 0; + } + } + uiState = state; - cursor = 0; - scroll = 0; + + if (state == stateSdCardEmmcMenu) + { + // Override cursor/scroll values + cursor = titleListCursor; + scroll = titleListScroll; + } else + if (state == stateSdCardEmmcOrphanPatchAddOnMenu) + { + // Override cursor/scroll values + cursor = orphanListCursor; + scroll = orphanListScroll; + } else { + cursor = 0; + scroll = 0; + } + + titleSelectorStr[0] = '\0'; } UIState uiGetState() @@ -983,11 +1074,17 @@ UIResult uiProcess() u32 keysHeld; int scrollAmount = 0; + bool scrollWithKeysDown = false; u32 patch, addon, xpos, ypos, startYPos; char versionStr[128] = {'\0'}; + int maxElements = (uiState == stateSdCardEmmcMenu ? SDCARD_MAX_ELEMENTS : (uiState == stateSdCardEmmcOrphanPatchAddOnMenu ? ORPHAN_MAX_ELEMENTS : (uiState == stateHfs0Browser ? HFS0_MAX_ELEMENTS : ((uiState == stateExeFsSectionBrowser || uiState == stateRomFsSectionBrowser) ? ROMFS_MAX_ELEMENTS : COMMON_MAX_ELEMENTS)))); + + const char *upwardsArrow = UPWARDS_ARROW; + const char *downwardsArrow = DOWNWARDS_ARROW; + uiPrintHeadline(); loadTitleInfo(); @@ -1174,7 +1271,7 @@ UIResult uiProcess() if (titleAuthor != NULL && titleAuthor[selectedAppInfoIndex] != NULL && strlen(titleAuthor[selectedAppInfoIndex])) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Developer: %s", titleAuthor[selectedAppInfoIndex]); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Publisher: %s", titleAuthor[selectedAppInfoIndex]); uiDrawString(strbuf, xpos, ypos, 0, 255, 0); ypos += (font_height + (font_height / 4)); } @@ -1273,9 +1370,157 @@ UIResult uiProcess() snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Total bundled DLC(s): %u", titleAddOnCount); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); } + } else + if (menuType == MENUTYPE_SDCARD_EMMC) + { + if (!strlen(dumpedNspInfoStr)) + { + // Look for dumped content in the SD card + char *dumpName = NULL; + char dumpPath[NAME_BUF_LEN] = {'\0'}, tmpStr[64] = {'\0'}; + bool dumpedBase = false, dumpedBaseConsoleData = false; + + u32 patchCnt = 0, addOnCnt = 0; + u32 patchCntConsoleData = 0, addOnCntConsoleData = 0; + + snprintf(dumpedNspInfoStr, sizeof(dumpedNspInfoStr) / sizeof(dumpedNspInfoStr[0]), "Content already dumped: "); + + // First look for a dumped base application + dumpName = generateNSPDumpName(DUMP_APP_NSP, selectedAppInfoIndex); + if (dumpName) + { + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "%s%s.nsp", NSP_DUMP_PATH, dumpName); + + free(dumpName); + dumpName = NULL; + + if (checkIfFileExists(dumpPath)) + { + dumpedBase = true; + dumpedBaseConsoleData = checkIfDumpedNspContainsConsoleData(dumpPath); + } + } + + // Look for dumped updates + if (titlePatchCount > 0) + { + for(patch = 0; patch < titlePatchCount; patch++) + { + if ((titleAppTitleID[selectedAppInfoIndex] | APPLICATION_PATCH_BITMASK) == titlePatchTitleID[patch]) + { + dumpName = generateNSPDumpName(DUMP_PATCH_NSP, patch); + if (dumpName) + { + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "%s%s.nsp", NSP_DUMP_PATH, dumpName); + + free(dumpName); + dumpName = NULL; + + if (checkIfFileExists(dumpPath)) + { + patchCnt++; + if (checkIfDumpedNspContainsConsoleData(dumpPath)) patchCntConsoleData++; + } + } + } + } + } + + // Look for dumped DLCs + if (titleAddOnCount > 0) + { + for(addon = 0; addon < titleAddOnCount; addon++) + { + if ((titleAppTitleID[selectedAppInfoIndex] & APPLICATION_ADDON_BITMASK) == (titleAddOnTitleID[addon] & APPLICATION_ADDON_BITMASK)) + { + dumpName = generateNSPDumpName(DUMP_ADDON_NSP, addon); + if (dumpName) + { + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "%s%s.nsp", NSP_DUMP_PATH, dumpName); + + free(dumpName); + dumpName = NULL; + + if (checkIfFileExists(dumpPath)) + { + addOnCnt++; + if (checkIfDumpedNspContainsConsoleData(dumpPath)) addOnCntConsoleData++; + } + } + } + } + } + + if (!dumpedBase && !patchCnt && !addOnCnt) + { + strcat(dumpedNspInfoStr, "NONE"); + } else { + if (dumpedBase) + { + strcat(dumpedNspInfoStr, "BASE"); + + if (dumpedBaseConsoleData) + { + strcat(dumpedNspInfoStr, " (with console data)"); + } else { + strcat(dumpedNspInfoStr, " (without console data)"); + } + } + + if (patchCnt) + { + if (patchCntConsoleData) + { + if (patchCntConsoleData == patchCnt) + { + if (patchCnt > 1) + { + snprintf(tmpStr, sizeof(tmpStr) / sizeof(tmpStr[0]), "%u UPD (all with console data)", patchCnt); + } else { + snprintf(tmpStr, sizeof(tmpStr) / sizeof(tmpStr[0]), "UPD (with console data)"); + } + } else { + snprintf(tmpStr, sizeof(tmpStr) / sizeof(tmpStr[0]), "%u UPD (%u with console data)", patchCnt, patchCntConsoleData); + } + } else { + if (patchCnt > 1) + { + snprintf(tmpStr, sizeof(tmpStr) / sizeof(tmpStr[0]), "%u UPD (all without console data)", patchCnt); + } else { + snprintf(tmpStr, sizeof(tmpStr) / sizeof(tmpStr[0]), "UPD (without console data)"); + } + } + + if (dumpedBase) strcat(dumpedNspInfoStr, ", "); + + strcat(dumpedNspInfoStr, tmpStr); + } + + if (addOnCnt) + { + if (addOnCntConsoleData) + { + if (addOnCntConsoleData == addOnCnt) + { + snprintf(tmpStr, sizeof(tmpStr) / sizeof(tmpStr[0]), "%u DLC (%s console data)", addOnCnt, (addOnCnt > 1 ? "all with" : "with")); + } else { + snprintf(tmpStr, sizeof(tmpStr) / sizeof(tmpStr[0]), "%u DLC (%u with console data)", addOnCnt, addOnCntConsoleData); + } + } else { + snprintf(tmpStr, sizeof(tmpStr) / sizeof(tmpStr[0]), "%u DLC (%s console data)", addOnCnt, (addOnCnt > 1 ? "all without" : "without")); + } + + if (dumpedBase || patchCnt) strcat(dumpedNspInfoStr, ", "); + + strcat(dumpedNspInfoStr, tmpStr); + } + } + } - breaks += 2; + uiDrawString(dumpedNspInfoStr, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); } + + breaks += 2; } else if (menuType == MENUTYPE_SDCARD_EMMC && orphanMode && (uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu)) { @@ -1286,7 +1531,54 @@ UIResult uiProcess() convertTitleVersionToDecimal((uiState == stateNspPatchDumpMenu ? titlePatchVersion[selectedPatchIndex] : titleAddOnVersion[selectedAddOnIndex]), versionStr, sizeof(versionStr)); snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Version: %s", versionStr); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + breaks++; + + if (!strlen(dumpedNspInfoStr)) + { + // Look for dumped content in the SD card + char *dumpName = NULL; + char dumpPath[NAME_BUF_LEN] = {'\0'}; + + snprintf(dumpedNspInfoStr, sizeof(dumpedNspInfoStr) / sizeof(dumpedNspInfoStr[0]), "Title already dumped: "); + + if (uiState == stateNspPatchDumpMenu) + { + dumpName = generateNSPDumpName(DUMP_PATCH_NSP, selectedPatchIndex); + } else + if (uiState == stateNspAddOnDumpMenu) + { + dumpName = generateNSPDumpName(DUMP_ADDON_NSP, selectedAddOnIndex); + } + + if (dumpName) + { + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "%s%s.nsp", NSP_DUMP_PATH, dumpName); + + free(dumpName); + dumpName = NULL; + + if (checkIfFileExists(dumpPath)) + { + strcat(dumpedNspInfoStr, "Yes"); + + if (checkIfDumpedNspContainsConsoleData(dumpPath)) + { + strcat(dumpedNspInfoStr, " (with console data)"); + } else { + strcat(dumpedNspInfoStr, " (without console data)"); + } + } else { + strcat(dumpedNspInfoStr, "No"); + } + } else { + strcat(dumpedNspInfoStr, "No"); + } + } + + uiDrawString(dumpedNspInfoStr, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); breaks += 2; + } else { + dumpedNspInfoStr[0] = '\0'; } switch(uiState) @@ -1478,6 +1770,8 @@ UIResult uiProcess() uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); } + breaks++; + break; case stateSdCardEmmcTitleMenu: menu = sdCardEmmcMenuItems; @@ -1493,6 +1787,9 @@ UIResult uiProcess() menuItemsCount = filenamesCount; uiDrawString("Dump installed content with missing base application", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks += 2; + + uiDrawString("Hint: gamecard updates can be found in this section.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); if (menuItemsCount) { @@ -1515,14 +1812,23 @@ UIResult uiProcess() if (menu && menuItemsCount) { - breaks += 2; + breaks++; + + if (scroll > 0 && (uiState != stateRomFsSectionBrowser || (uiState == stateRomFsSectionBrowser && (strlen(curRomFsPath) > 1 || cursor > 1)))) + { + u32 arrowWidth = uiGetStrWidth(upwardsArrow); + + uiDrawString(upwardsArrow, (FB_WIDTH / 2) - (arrowWidth / 2), (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + } + + breaks++; j = 0; highlight = false; for(i = scroll; i < menuItemsCount; i++, j++) { - if (((uiState != stateSdCardEmmcMenu && uiState != stateSdCardEmmcOrphanPatchAddOnMenu && uiState != stateHfs0Browser && uiState != stateExeFsSectionBrowser && uiState != stateRomFsSectionBrowser) && j >= COMMON_MAX_ELEMENTS) || (uiState == stateSdCardEmmcMenu && j >= SDCARD_MAX_ELEMENTS) || (uiState == stateSdCardEmmcOrphanPatchAddOnMenu && j >= ORPHAN_MAX_ELEMENTS) || (uiState == stateHfs0Browser && j >= HFS0_MAX_ELEMENTS) || ((uiState == stateExeFsSectionBrowser || uiState == stateRomFsSectionBrowser) && j >= ROMFS_MAX_ELEMENTS)) break; + if (j >= maxElements) break; // Avoid printing the "Create directory with archive bit set" option if "Split output dump" is disabled if (uiState == stateXciDumpMenu && i == 2 && !isFat32) @@ -1547,8 +1853,9 @@ UIResult uiProcess() } } - // Avoid printing the "Remove console specific data" and "Generate ticket-less dump" options in the NSP dump menus if we're dealing with a gamecard title - if (menuType == MENUTYPE_GAMECARD && (uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu) && (i == 3 || i == 4)) + // Avoid printing the "Remove console specific data" option in the NSP dump menus if we're dealing with a gamecard title + // Also, avoid printing the "Generate ticket-less dump" option in the NSP dump menus if we're dealing with a gamecard Application/AddOn title + if (menuType == MENUTYPE_GAMECARD && (((uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu) && i == 3) || ((uiState == stateNspAppDumpMenu || uiState == stateNspAddOnDumpMenu) && i == 4))) { j--; continue; @@ -1604,22 +1911,12 @@ UIResult uiProcess() xpos += (BROWSER_ICON_DIMENSION + 8); } - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), menu[i]); - u32 strWidth = uiGetStrWidth(strbuf); + uiDrawString(menu[i], xpos, ypos, font_r, font_g, font_b); - if ((xpos + strWidth) >= (FB_WIDTH - (font_height * 5))) - { - while((xpos + strWidth) >= (FB_WIDTH - (font_height * 5))) - { - u32 charWidth = uiGetStrWidth(&(strbuf[strlen(strbuf) - 1])); - strbuf[strlen(strbuf) - 1] = '\0'; - strWidth -= charWidth; - } - - strcat(strbuf, "..."); - } + xpos = OPTIONS_X_START_POS; - uiDrawString(strbuf, xpos, ypos, font_r, font_g, font_b); + bool leftArrowCondition = false; + bool rightArrowCondition = false; // Print XCI dump menu settings values if (uiState == stateXciDumpMenu && i > 0) @@ -1627,19 +1924,24 @@ UIResult uiProcess() switch(i) { case 1: // Split output dump (FAT32 support) - uiDrawString((isFat32 ? "Yes" : "No"), OPTIONS_X_POS, ypos, (isFat32 ? 0 : 255), (isFat32 ? 255 : 0), 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s", (isFat32 ? "Yes" : "No")); + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, isFat32, !isFat32, (isFat32 ? 0 : 255), (isFat32 ? 255 : 0), 0); break; case 2: // Create directory with archive bit set - uiDrawString((setXciArchiveBit ? "Yes" : "No"), OPTIONS_X_POS, ypos, (setXciArchiveBit ? 0 : 255), (setXciArchiveBit ? 255 : 0), 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s", (setXciArchiveBit ? "Yes" : "No")); + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, setXciArchiveBit, !setXciArchiveBit, (setXciArchiveBit ? 0 : 255), (setXciArchiveBit ? 255 : 0), 0); break; case 3: // Dump certificate - uiDrawString((dumpCert ? "Yes" : "No"), OPTIONS_X_POS, ypos, (dumpCert ? 0 : 255), (dumpCert ? 255 : 0), 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s", (dumpCert ? "Yes" : "No")); + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, dumpCert, !dumpCert, (dumpCert ? 0 : 255), (dumpCert ? 255 : 0), 0); break; case 4: // Trim output dump - uiDrawString((trimDump ? "Yes" : "No"), OPTIONS_X_POS, ypos, (trimDump ? 0 : 255), (trimDump ? 255 : 0), 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s", (trimDump ? "Yes" : "No")); + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, trimDump, !trimDump, (trimDump ? 0 : 255), (trimDump ? 255 : 0), 0); break; case 5: // CRC32 checksum calculation + dump verification - uiDrawString((calcCrc ? "Yes" : "No"), OPTIONS_X_POS, ypos, (calcCrc ? 0 : 255), (calcCrc ? 255 : 0), 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s", (calcCrc ? "Yes" : "No")); + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, calcCrc, !calcCrc, (calcCrc ? 0 : 255), (calcCrc ? 255 : 0), 0); break; default: break; @@ -1652,39 +1954,62 @@ UIResult uiProcess() switch(i) { case 1: // Split output dump (FAT32 support) - uiDrawString((isFat32 ? "Yes" : "No"), OPTIONS_X_POS, ypos, (isFat32 ? 0 : 255), (isFat32 ? 255 : 0), 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s", (isFat32 ? "Yes" : "No")); + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, isFat32, !isFat32, (isFat32 ? 0 : 255), (isFat32 ? 255 : 0), 0); break; case 2: // CRC32 checksum calculation - uiDrawString((calcCrc ? "Yes" : "No"), OPTIONS_X_POS, ypos, (calcCrc ? 0 : 255), (calcCrc ? 255 : 0), 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s", (calcCrc ? "Yes" : "No")); + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, calcCrc, !calcCrc, (calcCrc ? 0 : 255), (calcCrc ? 255 : 0), 0); break; case 3: // Remove console specific data - uiDrawString((removeConsoleData ? "Yes" : "No"), OPTIONS_X_POS, ypos, (removeConsoleData ? 0 : 255), (removeConsoleData ? 255 : 0), 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s", (removeConsoleData ? "Yes" : "No")); + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, removeConsoleData, !removeConsoleData, (removeConsoleData ? 0 : 255), (removeConsoleData ? 255 : 0), 0); break; case 4: // Generate ticket-less dump - uiDrawString((tiklessDump ? "Yes" : "No"), OPTIONS_X_POS, ypos, (tiklessDump ? 0 : 255), (tiklessDump ? 255 : 0), 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s", (tiklessDump ? "Yes" : "No")); + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS, tiklessDump, !tiklessDump, (tiklessDump ? 0 : 255), (tiklessDump ? 255 : 0), 0); break; case 5: // Bundled application/update/DLC to dump if (uiState == stateNspAppDumpMenu) { - // Print application name - convertTitleVersionToDecimal(titleAppVersion[selectedAppIndex], versionStr, sizeof(versionStr)); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s v%s", titleName[selectedAppIndex], versionStr); + if (!strlen(titleSelectorStr)) + { + // Print application name + convertTitleVersionToDecimal(titleAppVersion[selectedAppIndex], versionStr, sizeof(versionStr)); + snprintf(titleSelectorStr, sizeof(titleSelectorStr) / sizeof(titleSelectorStr[0]), "%s v%s", titleName[selectedAppIndex], versionStr); + } + + leftArrowCondition = (menuType == MENUTYPE_GAMECARD && titleAppCount > 1 && selectedAppIndex > 0); + rightArrowCondition = (menuType == MENUTYPE_GAMECARD && titleAppCount > 1 && selectedAppIndex < (titleAppCount - 1)); } else if (uiState == stateNspPatchDumpMenu) { - // Find a matching application to print its name - // Otherwise, just print the Title ID - retrieveDescriptionForPatchOrAddOn(titlePatchTitleID[selectedPatchIndex], titlePatchVersion[selectedPatchIndex], false, NULL); + if (!strlen(titleSelectorStr)) + { + // Find a matching application to print its name + // Otherwise, just print the Title ID + retrieveDescriptionForPatchOrAddOn(titlePatchTitleID[selectedPatchIndex], titlePatchVersion[selectedPatchIndex], false, (menuType == MENUTYPE_GAMECARD), NULL, titleSelectorStr, sizeof(titleSelectorStr) / sizeof(titleSelectorStr[0])); + } + + leftArrowCondition = ((menuType == MENUTYPE_GAMECARD && titlePatchCount > 0 && selectedPatchIndex > 0) || (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && retrievePreviousPatchOrAddOnIndexFromBaseApplication(selectedPatchIndex, selectedAppInfoIndex, false) != selectedPatchIndex)); + rightArrowCondition = ((menuType == MENUTYPE_GAMECARD && titlePatchCount > 0 && selectedPatchIndex < (titlePatchCount - 1)) || (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && retrieveNextPatchOrAddOnIndexFromBaseApplication(selectedPatchIndex, selectedAppInfoIndex, false) != selectedPatchIndex)); } else if (uiState == stateNspAddOnDumpMenu) { - // Find a matching application to print its name and Title ID - // Otherwise, just print the Title ID - retrieveDescriptionForPatchOrAddOn(titleAddOnTitleID[selectedAddOnIndex], titleAddOnVersion[selectedAddOnIndex], true, NULL); + if (!strlen(titleSelectorStr)) + { + // Find a matching application to print its name and Title ID + // Otherwise, just print the Title ID + retrieveDescriptionForPatchOrAddOn(titleAddOnTitleID[selectedAddOnIndex], titleAddOnVersion[selectedAddOnIndex], true, (menuType == MENUTYPE_GAMECARD), NULL, titleSelectorStr, sizeof(titleSelectorStr) / sizeof(titleSelectorStr[0])); + } + + leftArrowCondition = ((menuType == MENUTYPE_GAMECARD && titleAddOnCount > 0 && selectedAddOnIndex > 0) || (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && retrievePreviousPatchOrAddOnIndexFromBaseApplication(selectedAddOnIndex, selectedAppInfoIndex, true) != selectedAddOnIndex)); + rightArrowCondition = ((menuType == MENUTYPE_GAMECARD && titleAddOnCount > 0 && selectedAddOnIndex < (titleAddOnCount - 1)) || (menuType == MENUTYPE_SDCARD_EMMC && !orphanMode && retrieveNextPatchOrAddOnIndexFromBaseApplication(selectedAddOnIndex, selectedAppInfoIndex, true) != selectedAddOnIndex)); } - uiDrawString(strbuf, OPTIONS_X_POS, ypos, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), titleSelectorStr); + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS_NSP, leftArrowCondition, rightArrowCondition, 255, 255, 255); break; default: break; @@ -1707,10 +2032,19 @@ UIResult uiProcess() switch(i) { case 1: // Bundled application to dump/browse - // Print application name - convertTitleVersionToDecimal(titleAppVersion[selectedAppIndex], versionStr, sizeof(versionStr)); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s v%s", titleName[selectedAppIndex], versionStr); - uiDrawString(strbuf, OPTIONS_X_POS, ypos, 255, 255, 255); + if (!strlen(titleSelectorStr)) + { + // Print application name + convertTitleVersionToDecimal(titleAppVersion[selectedAppIndex], versionStr, sizeof(versionStr)); + snprintf(titleSelectorStr, sizeof(titleSelectorStr) / sizeof(titleSelectorStr[0]), "%s v%s", titleName[selectedAppIndex], versionStr); + } + + leftArrowCondition = (menuType == MENUTYPE_GAMECARD && titleAppCount > 1 && selectedAppIndex > 0); + rightArrowCondition = (menuType == MENUTYPE_GAMECARD && titleAppCount > 1 && selectedAppIndex < (titleAppCount - 1)); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), titleSelectorStr); + + uiPrintOption(xpos, ypos, OPTIONS_X_END_POS_NSP, leftArrowCondition, rightArrowCondition, 255, 255, 255); break; default: break; @@ -1719,6 +2053,15 @@ UIResult uiProcess() if (i == cursor) highlight = false; } + + if ((scroll + maxElements) < menuItemsCount) + { + ypos = ((breaks * (font_height + (font_height / 4))) + (uiState == stateSdCardEmmcMenu ? (j * (NACP_ICON_DOWNSCALED + 12)) : (j * (font_height + 12)))); + + u32 arrowWidth = uiGetStrWidth(downwardsArrow); + + uiDrawString(downwardsArrow, (FB_WIDTH / 2) - (arrowWidth / 2), ypos, 255, 255, 255); + } } else { if (uiState == stateSdCardEmmcOrphanPatchAddOnMenu) { @@ -1731,6 +2074,7 @@ UIResult uiProcess() uiRefreshDisplay(); hidScanInput(); + keysDown = hidKeysDown(CONTROLLER_P1_AUTO); keysHeld = hidKeysHeld(CONTROLLER_P1_AUTO); @@ -1819,10 +2163,18 @@ UIResult uiProcess() } // Go up - if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) scrollAmount = -1; + if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) + { + scrollAmount = -1; + scrollWithKeysDown = ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP)); + } // Go down - if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) scrollAmount = 1; + if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) + { + scrollAmount = 1; + scrollWithKeysDown = ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN)); + } } else if (uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu) { @@ -1886,8 +2238,7 @@ UIResult uiProcess() if (selectedAppIndex > 0) { selectedAppIndex--; - } else { - selectedAppIndex = 0; + titleSelectorStr[0] = '\0'; } } } else @@ -1898,31 +2249,19 @@ UIResult uiProcess() if (selectedPatchIndex > 0) { selectedPatchIndex--; - } else { - selectedPatchIndex = 0; + titleSelectorStr[0] = '\0'; } } else if (menuType == MENUTYPE_SDCARD_EMMC) { if (!orphanMode) { - bool foundMatch = false; - u32 startIndex = selectedPatchIndex; - int curIndex = ((int)selectedPatchIndex - 1); - - while(curIndex >= 0) + u32 newIndex = retrievePreviousPatchOrAddOnIndexFromBaseApplication(selectedPatchIndex, selectedAppInfoIndex, false); + if (newIndex != selectedPatchIndex) { - if (checkIfPatchOrAddOnBelongToBaseApplication((u32)curIndex, selectedAppInfoIndex, false)) - { - selectedPatchIndex = (u32)curIndex; - foundMatch = true; - break; - } - - curIndex--; + selectedPatchIndex = newIndex; + titleSelectorStr[0] = '\0'; } - - if (!foundMatch) selectedPatchIndex = startIndex; } } } else @@ -1933,31 +2272,19 @@ UIResult uiProcess() if (selectedAddOnIndex > 0) { selectedAddOnIndex--; - } else { - selectedAddOnIndex = 0; + titleSelectorStr[0] = '\0'; } } else if (menuType == MENUTYPE_SDCARD_EMMC) { if (!orphanMode) { - bool foundMatch = false; - u32 startIndex = selectedAddOnIndex; - int curIndex = ((int)selectedAddOnIndex - 1); - - while(curIndex >= 0) + u32 newIndex = retrievePreviousPatchOrAddOnIndexFromBaseApplication(selectedAddOnIndex, selectedAppInfoIndex, true); + if (newIndex != selectedAddOnIndex) { - if (checkIfPatchOrAddOnBelongToBaseApplication((u32)curIndex, selectedAppInfoIndex, true)) - { - selectedAddOnIndex = (u32)curIndex; - foundMatch = true; - break; - } - - curIndex--; + selectedAddOnIndex = newIndex; + titleSelectorStr[0] = '\0'; } - - if (!foundMatch) selectedAddOnIndex = startIndex; } } } @@ -1989,10 +2316,10 @@ UIResult uiProcess() { if (menuType == MENUTYPE_GAMECARD) { - if (titleAppCount > 1) + if (titleAppCount > 1 && (selectedAppIndex + 1) < titleAppCount) { selectedAppIndex++; - if (selectedAppIndex >= titleAppCount) selectedAppIndex = (titleAppCount - 1); + titleSelectorStr[0] = '\0'; } } } else @@ -2000,33 +2327,22 @@ UIResult uiProcess() { if (menuType == MENUTYPE_GAMECARD) { - if (titlePatchCount > 1) + if (titlePatchCount > 1 && (selectedPatchIndex + 1) < titlePatchCount) { selectedPatchIndex++; - if (selectedPatchIndex >= titlePatchCount) selectedPatchIndex = (titlePatchCount - 1); + titleSelectorStr[0] = '\0'; } } else if (menuType == MENUTYPE_SDCARD_EMMC) { if (!orphanMode) { - bool foundMatch = false; - u32 startIndex = selectedPatchIndex; - - selectedPatchIndex++; - - while(selectedPatchIndex < titlePatchCount) + u32 newIndex = retrieveNextPatchOrAddOnIndexFromBaseApplication(selectedPatchIndex, selectedAppInfoIndex, false); + if (newIndex != selectedPatchIndex) { - if (checkIfPatchOrAddOnBelongToBaseApplication(selectedPatchIndex, selectedAppInfoIndex, false)) - { - foundMatch = true; - break; - } - - selectedPatchIndex++; + selectedPatchIndex = newIndex; + titleSelectorStr[0] = '\0'; } - - if (!foundMatch) selectedPatchIndex = startIndex; } } } else @@ -2034,33 +2350,22 @@ UIResult uiProcess() { if (menuType == MENUTYPE_GAMECARD) { - if (titleAddOnCount > 1) + if (titleAddOnCount > 1 && (selectedAddOnIndex + 1) < titleAddOnCount) { selectedAddOnIndex++; - if (selectedAddOnIndex >= titleAddOnCount) selectedAddOnIndex = (titleAddOnCount - 1); + titleSelectorStr[0] = '\0'; } } else if (menuType == MENUTYPE_SDCARD_EMMC) { if (!orphanMode) { - bool foundMatch = false; - u32 startIndex = selectedAddOnIndex; - - selectedAddOnIndex++; - - while(selectedAddOnIndex < titleAddOnCount) + u32 newIndex = retrieveNextPatchOrAddOnIndexFromBaseApplication(selectedAddOnIndex, selectedAppInfoIndex, false); + if (newIndex != selectedAddOnIndex) { - if (checkIfPatchOrAddOnBelongToBaseApplication(selectedAddOnIndex, selectedAppInfoIndex, true)) - { - foundMatch = true; - break; - } - - selectedAddOnIndex++; + selectedAddOnIndex = newIndex; + titleSelectorStr[0] = '\0'; } - - if (!foundMatch) selectedAddOnIndex = startIndex; } } } @@ -2071,10 +2376,18 @@ UIResult uiProcess() } // Go up - if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) scrollAmount = -1; + if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) + { + scrollAmount = -1; + scrollWithKeysDown = ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP)); + } // Go down - if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) scrollAmount = 1; + if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) + { + scrollAmount = 1; + scrollWithKeysDown = ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN)); + } } else if (uiState == stateExeFsSectionDataDumpMenu || uiState == stateExeFsSectionBrowserMenu || uiState == stateRomFsSectionDataDumpMenu || uiState == stateRomFsSectionBrowserMenu) { @@ -2105,8 +2418,7 @@ UIResult uiProcess() if (selectedAppIndex > 0) { selectedAppIndex--; - } else { - selectedAppIndex = 0; + titleSelectorStr[0] = '\0'; } } break; @@ -2123,10 +2435,10 @@ UIResult uiProcess() case 1: // Bundled application to dump/browse if (menuType == MENUTYPE_GAMECARD) { - if (titleAppCount > 1) + if (titleAppCount > 1 && (selectedAppIndex + 1) < titleAppCount) { selectedAppIndex++; - if (selectedAppIndex >= titleAppCount) selectedAppIndex = (titleAppCount - 1); + titleSelectorStr[0] = '\0'; } } break; @@ -2136,10 +2448,18 @@ UIResult uiProcess() } // Go up - if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) scrollAmount = -1; + if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) + { + scrollAmount = -1; + scrollWithKeysDown = ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP)); + } // Go down - if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) scrollAmount = 1; + if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) + { + scrollAmount = 1; + scrollWithKeysDown = ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN)); + } } else { // Select if (keysDown & KEY_A) @@ -2504,7 +2824,7 @@ UIResult uiProcess() } } - // Dump installed content with missing base application + // SD/eMMC menu: Dump installed content with missing base application if (uiState == stateSdCardEmmcMenu && ((titleAppCount && ((titlePatchCount && checkOrphanPatchOrAddOn(false)) || (titleAddOnCount && checkOrphanPatchOrAddOn(true)))) || (!titleAppCount && (titlePatchCount || titleAddOnCount))) && (keysDown & KEY_Y)) { res = resultShowSdCardEmmcOrphanPatchAddOnMenu; @@ -2514,11 +2834,21 @@ UIResult uiProcess() if (menu && menuItemsCount) { // Go up - if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) scrollAmount = -1; + if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) + { + scrollAmount = -1; + scrollWithKeysDown = ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP)); + } + if ((keysDown & KEY_DLEFT) || (keysDown & KEY_LSTICK_LEFT) || (keysHeld & KEY_RSTICK_LEFT)) scrollAmount = -5; // Go down - if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) scrollAmount = 1; + if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) + { + scrollAmount = 1; + scrollWithKeysDown = ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN)); + } + if ((keysDown & KEY_DRIGHT) || (keysDown & KEY_LSTICK_RIGHT) || (keysHeld & KEY_RSTICK_RIGHT)) scrollAmount = 5; } } @@ -2528,23 +2858,36 @@ UIResult uiProcess() { if (scrollAmount > 0) { - for(i = 0; i < scrollAmount; i++) + if (scrollWithKeysDown && (cursor + scrollAmount) > (menuItemsCount - 1)) { - if (cursor < menuItemsCount - 1) + cursor = 0; + scroll = 0; + } else { + for(i = 0; i < scrollAmount; i++) { - cursor++; - if (((uiState != stateSdCardEmmcMenu && uiState != stateSdCardEmmcOrphanPatchAddOnMenu && uiState != stateHfs0Browser && uiState != stateExeFsSectionBrowser && uiState != stateRomFsSectionBrowser) && (cursor - scroll) >= COMMON_MAX_ELEMENTS) || (uiState == stateSdCardEmmcMenu && (cursor - scroll) >= SDCARD_MAX_ELEMENTS) || (uiState == stateSdCardEmmcOrphanPatchAddOnMenu && (cursor - scroll) >= ORPHAN_MAX_ELEMENTS) || (uiState == stateHfs0Browser && (cursor - scroll) >= HFS0_MAX_ELEMENTS) || ((uiState == stateExeFsSectionBrowser || uiState == stateRomFsSectionBrowser) && (cursor - scroll) >= ROMFS_MAX_ELEMENTS)) scroll++; + if (cursor < (menuItemsCount - 1)) + { + cursor++; + if ((cursor - scroll) >= maxElements) scroll++; + } } } } else if (scrollAmount < 0) { - for(i = 0; i < -scrollAmount; i++) + if (scrollWithKeysDown && (cursor + scrollAmount) < 0) { - if (cursor > 0) + cursor = (menuItemsCount - 1); + scroll = (menuItemsCount - maxElements); + if (scroll < 0) scroll = 0; + } else { + for(i = 0; i < -scrollAmount; i++) { - cursor--; - if ((cursor - scroll) < 0) scroll--; + if (cursor > 0) + { + cursor--; + if ((cursor - scroll) < 0) scroll--; + } } } } @@ -2570,7 +2913,7 @@ UIResult uiProcess() { if ((titlePatchCount && !titleAddOnCount) || (!titlePatchCount && titleAddOnCount)) { - if (cursor >= 2) cursor = 1; + if (cursor >= 2) cursor = ((scrollWithKeysDown && scrollAmount > 0) ? 0 : 1); } else if (!titlePatchCount && !titleAddOnCount) { @@ -2582,7 +2925,7 @@ UIResult uiProcess() { if ((titlePatchCount && checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false) && (!titleAddOnCount || !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true))) || ((!titlePatchCount || !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false)) && titleAddOnCount && checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true))) { - if (cursor >= 2) cursor = 1; + if (cursor >= 2) cursor = ((scrollWithKeysDown && scrollAmount > 0) ? 0 : 1); } else if ((!titlePatchCount || !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, false)) && (!titleAddOnCount || !checkIfBaseApplicationHasPatchOrAddOn(selectedAppInfoIndex, true))) { @@ -2592,12 +2935,13 @@ UIResult uiProcess() } } - // Avoid placing the cursor on the "Remove console specific data" and "Generate ticket-less dump" options in the NSP dump menus if we're dealing with a gamecard title - if (menuType == MENUTYPE_GAMECARD && (uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu) && (cursor == 3 || cursor == 4)) + // Avoid placing the cursor on the "Remove console specific data" option in the NSP dump menus if we're dealing with a gamecard title + // Also, avoid placing the cursor on the "Generate ticket-less dump" option in the NSP dump menus if we're dealing with a gamecard Application/AddOn title + if (menuType == MENUTYPE_GAMECARD && (((uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu) && cursor == 3) || ((uiState == stateNspAppDumpMenu || uiState == stateNspAddOnDumpMenu) && cursor == 4))) { if (scrollAmount > 0) { - cursor = 5; + cursor = ((uiState == stateNspPatchDumpMenu && cursor == 3) ? 4 : 5); } else if (scrollAmount < 0) { @@ -2619,7 +2963,16 @@ UIResult uiProcess() } // Avoid placing the cursor on the parent directory entry ("..") in the RomFS browser if we're currently at the root directory - if (uiState == stateRomFsSectionBrowser && cursor == 0 && strlen(curRomFsPath) <= 1) cursor = 1; + if (uiState == stateRomFsSectionBrowser && cursor == 0 && strlen(curRomFsPath) <= 1) + { + cursor = ((scrollWithKeysDown && scrollAmount < 0) ? (menuItemsCount - 1) : 1); + + if (cursor == (menuItemsCount - 1)) + { + scroll = (menuItemsCount - maxElements); + if (scroll < 0) scroll = 0; + } + } } } } else @@ -2675,6 +3028,12 @@ UIResult uiProcess() uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks++; + if (menuType == MENUTYPE_GAMECARD && selectedNspDumpType == DUMP_PATCH_NSP) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", menu[4], (tiklessDump ? "Yes" : "No")); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks++; + } else if (menuType == MENUTYPE_SDCARD_EMMC) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", menu[3], (removeConsoleData ? "Yes" : "No")); @@ -2696,11 +3055,11 @@ UIResult uiProcess() } else if (selectedNspDumpType == DUMP_PATCH_NSP) { - retrieveDescriptionForPatchOrAddOn(titlePatchTitleID[selectedPatchIndex], titlePatchVersion[selectedPatchIndex], false, menu[5]); + retrieveDescriptionForPatchOrAddOn(titlePatchTitleID[selectedPatchIndex], titlePatchVersion[selectedPatchIndex], false, true, menu[5], strbuf, sizeof(strbuf) / sizeof(strbuf[0])); } else if (selectedNspDumpType == DUMP_ADDON_NSP) { - retrieveDescriptionForPatchOrAddOn(titleAddOnTitleID[selectedAddOnIndex], titleAddOnVersion[selectedAddOnIndex], true, menu[5]); + retrieveDescriptionForPatchOrAddOn(titleAddOnTitleID[selectedAddOnIndex], titleAddOnVersion[selectedAddOnIndex], true, true, menu[5], strbuf, sizeof(strbuf) / sizeof(strbuf[0])); } uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); @@ -2756,8 +3115,6 @@ UIResult uiProcess() if (getHfs0FileList(selectedPartitionIndex)) { - cursor = 0; - scroll = 0; res = resultShowHfs0Browser; } else { waitForButtonPress(); @@ -2818,8 +3175,6 @@ UIResult uiProcess() { if (getExeFsFileList()) { - cursor = 0; - scroll = 0; res = resultShowExeFsSectionBrowser; } else { freeExeFsContext(); @@ -2895,8 +3250,6 @@ UIResult uiProcess() { if (getRomFsFileList(0)) { - cursor = 0; - scroll = 0; res = resultShowRomFsSectionBrowser; } else { freeRomFsContext(); @@ -2929,8 +3282,6 @@ UIResult uiProcess() { if (getRomFsFileList(romFsBrowserEntries[selectedFileIndex].offset)) { - cursor = 0; - scroll = 0; res = resultShowRomFsSectionBrowser; } else { romfs_fail = true; diff --git a/source/ui.h b/source/ui.h index a00346b..ab521a9 100644 --- a/source/ui.h +++ b/source/ui.h @@ -19,18 +19,25 @@ #define HIGHLIGHT_FONT_COLOR_G 255 #define HIGHLIGHT_FONT_COLOR_B 197 +// UTF-8 +#define UPWARDS_ARROW "\xE2\x86\x91" +#define DOWNWARDS_ARROW "\xE2\x86\x93" + #define COMMON_MAX_ELEMENTS 8 #define HFS0_MAX_ELEMENTS 14 #define ROMFS_MAX_ELEMENTS 12 -#define SDCARD_MAX_ELEMENTS 4 +#define SDCARD_MAX_ELEMENTS 3 #define ORPHAN_MAX_ELEMENTS 12 -#define OPTIONS_X_POS (35 * CHAR_PT_SIZE) +#define OPTIONS_X_START_POS (35 * CHAR_PT_SIZE) +#define OPTIONS_X_END_POS (OPTIONS_X_START_POS + (6 * CHAR_PT_SIZE)) +#define OPTIONS_X_END_POS_NSP (FB_WIDTH - (4 * CHAR_PT_SIZE)) #define TAB_WIDTH 4 #define BROWSER_ICON_DIMENSION 16 +// UTF-16 #define NINTENDO_FONT_A "\xE0\xA0" #define NINTENDO_FONT_B "\xE0\xA1" #define NINTENDO_FONT_Y "\xE0\xA3" @@ -148,7 +155,7 @@ bool uiLoadJpgFromFile(const char *filename, int expectedWidth, int expectedHeig void uiDrawString(const char *string, int x, int y, u8 r, u8 g, u8 b); -u32 uiGetStrWidth(char *string); +u32 uiGetStrWidth(const char *string); void uiRefreshDisplay(); diff --git a/source/util.c b/source/util.c index 9986f8c..f763a86 100644 --- a/source/util.c +++ b/source/util.c @@ -97,6 +97,14 @@ u64 *titleAddOnTitleID = NULL; u32 *titleAddOnVersion = NULL; FsStorageId *titleAddOnStorageId = NULL; +u32 sdCardTitleAppCount = 0; +u32 sdCardTitlePatchCount = 0; +u32 sdCardTitleAddOnCount = 0; + +u32 nandUserTitleAppCount = 0; +u32 nandUserTitlePatchCount = 0; +u32 nandUserTitleAddOnCount = 0; + char **titleName = NULL; char **fixedTitleName = NULL; char **titleAuthor = NULL; @@ -361,6 +369,14 @@ void freeTitleInfo() titleAddOnStorageId = NULL; } + sdCardTitleAppCount = 0; + sdCardTitlePatchCount = 0; + sdCardTitleAddOnCount = 0; + + nandUserTitleAppCount = 0; + nandUserTitlePatchCount = 0; + nandUserTitleAddOnCount = 0; + if (titleName != NULL) { freeStringsPtr(titleName); @@ -1065,7 +1081,21 @@ void loadTitleInfo() freeTitleInfo(); - proceed = (getTitleIDAndVersionList(FsStorageId_SdCard) && getTitleIDAndVersionList(FsStorageId_NandUser)); + if (getTitleIDAndVersionList(FsStorageId_SdCard)) + { + sdCardTitleAppCount = titleAppCount; + sdCardTitlePatchCount = titlePatchCount; + sdCardTitleAddOnCount = titleAddOnCount; + + if (getTitleIDAndVersionList(FsStorageId_NandUser)) + { + nandUserTitleAppCount = (titleAppCount - sdCardTitleAppCount); + nandUserTitlePatchCount = (titlePatchCount - sdCardTitlePatchCount); + nandUserTitleAddOnCount = (titleAddOnCount - sdCardTitleAddOnCount); + + proceed = true; + } + } } if (proceed && titleAppCount > 0) @@ -1479,6 +1509,8 @@ bool readProgramNcaExeFsOrRomFs(u32 appIndex, bool readRomFs) u32 i = 0; u32 written = 0; u32 total = 0; + u32 appCount = 0; + u32 ncmAppIndex = 0; u32 appNcaCount = 0; u32 partition = 0; @@ -1497,7 +1529,7 @@ bool readProgramNcaExeFsOrRomFs(u32 appIndex, bool readRomFs) NcmApplicationContentMetaKey *appList = NULL; NcmContentRecord *appContentRecords = NULL; - size_t appListSize = (sizeof(NcmApplicationContentMetaKey) * titleAppCount); + size_t appListSize = sizeof(NcmApplicationContentMetaKey); NcmNcaId ncaId; char ncaIdStr[33] = {'\0'}; @@ -1508,9 +1540,9 @@ bool readProgramNcaExeFsOrRomFs(u32 appIndex, bool readRomFs) bool success = false, foundProgram = false; - if (!titleAppCount || appIndex > (titleAppCount - 1)) + if (!titleAppStorageId) { - uiDrawString("Error: invalid application index!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + uiDrawString("Error: title storage ID unavailable!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); breaks += 2; return false; } @@ -1524,6 +1556,40 @@ bool readProgramNcaExeFsOrRomFs(u32 appIndex, bool readRomFs) return false; } + switch(curStorageId) + { + case FsStorageId_GameCard: + appCount = titleAppCount; + ncmAppIndex = appIndex; + break; + case FsStorageId_SdCard: + appCount = sdCardTitleAppCount; + ncmAppIndex = appIndex; + break; + case FsStorageId_NandUser: + appCount = nandUserTitleAppCount; + ncmAppIndex = (appIndex - sdCardTitleAppCount); + break; + default: + break; + } + + if (!appCount) + { + uiDrawString("Error: invalid base application count!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; + return false; + } + + if (ncmAppIndex > (appCount - 1)) + { + uiDrawString("Error: invalid base application index!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; + return false; + } + + appListSize *= appCount; + // If we're dealing with a gamecard, call workaroundPartitionZeroAccess() and read the secure partition header. Otherwise, ncmContentStorageReadContentIdFile() will fail with error 0x00171002 if (curStorageId == FsStorageId_GameCard) { @@ -1575,14 +1641,14 @@ bool readProgramNcaExeFsOrRomFs(u32 appIndex, bool readRomFs) goto out; } - if (written != total || written != titleAppCount) + if (written != total) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: title count mismatch in ncmContentMetaDatabaseListApplication (%u != %u)", written, titleAppCount); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: title count mismatch in ncmContentMetaDatabaseListApplication (%u != %u)", written, total); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } - if (R_FAILED(result = ncmContentMetaDatabaseGet(&ncmDb, &(appList[appIndex].metaRecord), sizeof(NcmContentMetaRecordsHeader), &contentRecordsHeader, &contentRecordsHeaderReadSize))) + if (R_FAILED(result = ncmContentMetaDatabaseGet(&ncmDb, &(appList[ncmAppIndex].metaRecord), sizeof(NcmContentMetaRecordsHeader), &contentRecordsHeader, &contentRecordsHeaderReadSize))) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentMetaDatabaseGet failed! (0x%08X)", result); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); @@ -1598,7 +1664,7 @@ bool readProgramNcaExeFsOrRomFs(u32 appIndex, bool readRomFs) goto out; } - if (R_FAILED(result = ncmContentMetaDatabaseListContentInfo(&ncmDb, &(appList[appIndex].metaRecord), 0, appContentRecords, appNcaCount * sizeof(NcmContentRecord), &written))) + if (R_FAILED(result = ncmContentMetaDatabaseListContentInfo(&ncmDb, &(appList[ncmAppIndex].metaRecord), 0, appContentRecords, appNcaCount * sizeof(NcmContentRecord), &written))) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentMetaDatabaseListContentInfo failed! (0x%08X)", result); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); @@ -1641,7 +1707,7 @@ bool readProgramNcaExeFsOrRomFs(u32 appIndex, bool readRomFs) if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, ncaHeader, NCA_FULL_HEADER_LENGTH))) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentStorageReadContentIdFile failed! (0x%08X)", result); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to read header from Program NCA! (0x%08X)", result); uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } @@ -1896,6 +1962,21 @@ bool getRomFsFileList(u32 dir_offset) romFsBrowserEntries[i].offset = entryOffset; snprintf(curName, entry->nameLen + 1, (char*)entry->name); + + // Fix entry name length + u32 strWidth = uiGetStrWidth(curName); + + if ((BROWSER_ICON_DIMENSION + 16 + strWidth) >= (FB_WIDTH - (font_height * 5))) + { + while((BROWSER_ICON_DIMENSION + 16 + strWidth) >= (FB_WIDTH - (font_height * 5))) + { + curName[strlen(curName) - 1] = '\0'; + strWidth = uiGetStrWidth(curName); + } + + strcat(curName, "..."); + } + addStringToFilenameBuffer(curName, &nextFilename); i++; @@ -1921,6 +2002,21 @@ bool getRomFsFileList(u32 dir_offset) romFsBrowserEntries[i].offset = entryOffset; snprintf(curName, entry->nameLen + 1, (char*)entry->name); + + // Fix entry name length + u32 strWidth = uiGetStrWidth(curName); + + if ((BROWSER_ICON_DIMENSION + 16 + strWidth) >= (FB_WIDTH - (font_height * 5))) + { + while((BROWSER_ICON_DIMENSION + 16 + strWidth) >= (FB_WIDTH - (font_height * 5))) + { + curName[strlen(curName) - 1] = '\0'; + strWidth = uiGetStrWidth(curName); + } + + strcat(curName, "..."); + } + addStringToFilenameBuffer(curName, &nextFilename); i++; @@ -2125,18 +2221,20 @@ char *generateNSPDumpName(nspDumpType selectedNspDumpType, u32 titleIndex) return fullname; } -void retrieveDescriptionForPatchOrAddOn(u64 titleID, u32 version, bool addOn, const char *prefix) +void retrieveDescriptionForPatchOrAddOn(u64 titleID, u32 version, bool addOn, bool addAppName, const char *prefix, char *outBuf, size_t outBufSize) { + if (!outBuf || !outBufSize) return; + char versionStr[128] = {'\0'}; convertTitleVersionToDecimal(version, versionStr, sizeof(versionStr)); - if (!titleAppCount || !titleAppTitleID || !titleName || !*titleName) + if (!titleAppCount || !titleAppTitleID || !titleName || !*titleName || !addAppName) { if (prefix) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%016lX v%s", prefix, titleID, versionStr); + snprintf(outBuf, outBufSize, "%s%016lX v%s", prefix, titleID, versionStr); } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%016lX v%s", titleID, versionStr); + snprintf(outBuf, outBufSize, "%016lX v%s", titleID, versionStr); } return; @@ -2158,16 +2256,16 @@ void retrieveDescriptionForPatchOrAddOn(u64 titleID, u32 version, bool addOn, co { if (prefix) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s | %016lX v%s", prefix, titleName[app], titleID, versionStr); + snprintf(outBuf, outBufSize, "%s%s | %016lX v%s", prefix, titleName[app], titleID, versionStr); } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s | %016lX v%s", titleName[app], titleID, versionStr); + snprintf(outBuf, outBufSize, "%s | %016lX v%s", titleName[app], titleID, versionStr); } } else { if (prefix) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%016lX v%s", prefix, titleID, versionStr); + snprintf(outBuf, outBufSize, "%s%016lX v%s", prefix, titleID, versionStr); } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%016lX v%s", titleID, versionStr); + snprintf(outBuf, outBufSize, "%016lX v%s", titleID, versionStr); } } } @@ -2336,7 +2434,7 @@ bool checkIfBaseApplicationHasPatchOrAddOn(u32 appIndex, bool addOn) return false; } -bool checkIfPatchOrAddOnBelongToBaseApplication(u32 titleIndex, u32 appIndex, bool addOn) +bool checkIfPatchOrAddOnBelongsToBaseApplication(u32 titleIndex, u32 appIndex, bool addOn) { if (!titleAppCount || appIndex > (titleAppCount - 1) || !titleAppTitleID || (!addOn && (!titlePatchCount || titleIndex > (titlePatchCount - 1) || !titlePatchTitleID)) || (addOn && (!titleAddOnCount || titleIndex > (titleAddOnCount - 1) || !titleAddOnTitleID))) return false; @@ -2354,12 +2452,52 @@ u32 retrieveFirstPatchOrAddOnIndexFromBaseApplication(u32 appIndex, bool addOn) for(titleIndex = 0; titleIndex < count; titleIndex++) { - if ((!addOn && (titleAppTitleID[appIndex] | APPLICATION_PATCH_BITMASK) == titlePatchTitleID[titleIndex]) || (addOn && (titleAppTitleID[appIndex] & APPLICATION_ADDON_BITMASK) == (titleAddOnTitleID[titleIndex] & APPLICATION_ADDON_BITMASK))) return titleIndex; + if (checkIfPatchOrAddOnBelongsToBaseApplication(titleIndex, appIndex, addOn)) return titleIndex; } return 0; } +u32 retrievePreviousPatchOrAddOnIndexFromBaseApplication(u32 startTitleIndex, u32 appIndex, bool addOn) +{ + u32 count = (!addOn ? titlePatchCount : titleAddOnCount); + u32 retTitleIndex = startTitleIndex; + u32 curTitleIndex = 0; + + if (!titleAppCount || appIndex > (titleAppCount - 1) || !titleAppTitleID || !startTitleIndex || startTitleIndex >= count || (!addOn && (!titlePatchCount || !titlePatchTitleID)) || (addOn && (!titleAddOnCount || !titleAddOnTitleID))) return retTitleIndex; + + for(curTitleIndex = startTitleIndex; curTitleIndex > 0; curTitleIndex--) + { + if (checkIfPatchOrAddOnBelongsToBaseApplication((curTitleIndex - 1), appIndex, addOn)) + { + retTitleIndex = (curTitleIndex - 1); + break; + } + } + + return retTitleIndex; +} + +u32 retrieveNextPatchOrAddOnIndexFromBaseApplication(u32 startTitleIndex, u32 appIndex, bool addOn) +{ + u32 count = (!addOn ? titlePatchCount : titleAddOnCount); + u32 retTitleIndex = startTitleIndex; + u32 curTitleIndex = 0; + + if (!titleAppCount || appIndex > (titleAppCount - 1) || !titleAppTitleID || startTitleIndex >= count || (!addOn && (!titlePatchCount || !titlePatchTitleID)) || (addOn && (!titleAddOnCount || !titleAddOnTitleID))) return retTitleIndex; + + for(curTitleIndex = (startTitleIndex + 1); curTitleIndex < count; curTitleIndex++) + { + if (checkIfPatchOrAddOnBelongsToBaseApplication(curTitleIndex, appIndex, addOn)) + { + retTitleIndex = curTitleIndex; + break; + } + } + + return retTitleIndex; +} + void waitForButtonPress() { uiDrawString("Press any button to continue", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); @@ -2435,6 +2573,173 @@ void convertDataToHexString(const u8 *data, const u32 dataSize, char *outBuf, co } } +bool checkIfFileExists(const char *path) +{ + if (!path || !strlen(path)) return false; + + FILE *chkfile = fopen(path, "rb"); + if (chkfile) + { + fclose(chkfile); + return true; + } + + return false; +} + +bool yesNoPrompt(const char *message) +{ + if (message && strlen(message)) + { + uiDrawString(message, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks++; + } + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "[ %s ] Yes | [ %s ] No", NINTENDO_FONT_A, NINTENDO_FONT_B); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks += 2; + + uiRefreshDisplay(); + + bool ret = false; + + while(true) + { + hidScanInput(); + + u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO); + + if (keysDown & KEY_A) + { + ret = true; + break; + } else + if (keysDown & KEY_B) + { + ret = false; + break; + } + } + + return ret; +} + +bool checkIfDumpedNspContainsConsoleData(const char *nspPath) +{ + if (!nspPath || !strlen(nspPath)) return false; + + FILE *nspFile = NULL; + u64 nspSize = 0; + + size_t read_bytes; + pfs0_header nspHeader; + pfs0_entry_table *nspEntries = NULL; + char *nspStrTable = NULL; + + u32 i; + bool foundTik = false; + u64 tikOffset = 0, tikSize = 0; + rsa2048_sha256_ticket tikData; + + const u8 titlekey_block_0x190_empty_hash[0x20] = { + 0x2D, 0xFB, 0xA6, 0x33, 0x81, 0x70, 0x46, 0xC7, 0xF5, 0x59, 0xED, 0x4B, 0x93, 0x07, 0x60, 0x48, + 0x43, 0x5F, 0x7E, 0x1A, 0x90, 0xF1, 0x4E, 0xB8, 0x03, 0x5C, 0x04, 0xB9, 0xEB, 0xAE, 0x25, 0x37 + }; + + u8 titlekey_block_0x190_hash[0x20]; + + nspFile = fopen(nspPath, "rb"); + if (!nspFile) return false; + + fseek(nspFile, 0, SEEK_END); + nspSize = ftell(nspFile); + rewind(nspFile); + + if (nspSize < sizeof(pfs0_header)) + { + fclose(nspFile); + return false; + } + + read_bytes = fread(&nspHeader, 1, sizeof(pfs0_header), nspFile); + + if (read_bytes != sizeof(pfs0_header) || bswap_32(nspHeader.magic) != PFS0_MAGIC || nspSize < (sizeof(pfs0_header) + (sizeof(pfs0_entry_table) * (u64)nspHeader.file_cnt) + (u64)nspHeader.str_table_size)) + { + fclose(nspFile); + return false; + } + + nspEntries = calloc((u64)nspHeader.file_cnt, sizeof(pfs0_entry_table)); + if (!nspEntries) + { + fclose(nspFile); + return false; + } + + read_bytes = fread(nspEntries, 1, sizeof(pfs0_entry_table) * (u64)nspHeader.file_cnt, nspFile); + + if (read_bytes != (sizeof(pfs0_entry_table) * (u64)nspHeader.file_cnt)) + { + free(nspEntries); + fclose(nspFile); + return false; + } + + nspStrTable = calloc((u64)nspHeader.str_table_size, sizeof(char)); + if (!nspStrTable) + { + free(nspEntries); + fclose(nspFile); + return false; + } + + read_bytes = fread(nspStrTable, 1, (u64)nspHeader.str_table_size, nspFile); + + if (read_bytes != (u64)nspHeader.str_table_size) + { + free(nspStrTable); + free(nspEntries); + fclose(nspFile); + return false; + } + + for(i = 0; i < nspHeader.file_cnt; i++) + { + char *curFilename = (nspStrTable + nspEntries[i].filename_offset); + + if (!strncasecmp(curFilename + strlen(curFilename) - 4, ".tik", 4)) + { + tikOffset = (sizeof(pfs0_header) + (sizeof(pfs0_entry_table) * (u64)nspHeader.file_cnt) + (u64)nspHeader.str_table_size + nspEntries[i].file_offset); + tikSize = nspEntries[i].file_size; + foundTik = true; + break; + } + } + + free(nspStrTable); + free(nspEntries); + + if (!foundTik || tikSize != ETICKET_TIK_FILE_SIZE || nspSize < (tikOffset + tikSize)) + { + fclose(nspFile); + return false; + } + + fseek(nspFile, tikOffset, SEEK_SET); + + read_bytes = fread(&tikData, 1, ETICKET_TIK_FILE_SIZE, nspFile); + + fclose(nspFile); + + if (read_bytes != ETICKET_TIK_FILE_SIZE) return false; + + sha256CalculateHash(titlekey_block_0x190_hash, tikData.titlekey_block + 0x10, 0xF0); + + if (!strncmp(tikData.sig_issuer, "Root-CA00000003-XS00000021", 26) || memcmp(titlekey_block_0x190_hash, titlekey_block_0x190_empty_hash, 0x20) != 0 || tikData.titlekey_type != ETICKET_TITLEKEY_COMMON || tikData.ticket_id != 0 || tikData.device_id != 0 || tikData.account_id != 0) return true; + + return false; +} + void removeDirectory(const char *path) { if (!path || !strlen(path)) return; diff --git a/source/util.h b/source/util.h index d7ea47f..8f7fcd1 100644 --- a/source/util.h +++ b/source/util.h @@ -188,7 +188,7 @@ char *generateDumpFullName(); char *generateNSPDumpName(nspDumpType selectedNspDumpType, u32 titleIndex); -void retrieveDescriptionForPatchOrAddOn(u64 titleID, u32 version, bool addOn, const char *prefix); +void retrieveDescriptionForPatchOrAddOn(u64 titleID, u32 version, bool addOn, bool addAppName, const char *prefix, char *outBuf, size_t outBufSize); bool checkOrphanPatchOrAddOn(bool addOn); @@ -196,10 +196,14 @@ void generateOrphanPatchOrAddOnList(); bool checkIfBaseApplicationHasPatchOrAddOn(u32 appIndex, bool addOn); -bool checkIfPatchOrAddOnBelongToBaseApplication(u32 titleIndex, u32 appIndex, bool addOn); +bool checkIfPatchOrAddOnBelongsToBaseApplication(u32 titleIndex, u32 appIndex, bool addOn); u32 retrieveFirstPatchOrAddOnIndexFromBaseApplication(u32 appIndex, bool addOn); +u32 retrievePreviousPatchOrAddOnIndexFromBaseApplication(u32 startTitleIndex, u32 appIndex, bool addOn); + +u32 retrieveNextPatchOrAddOnIndexFromBaseApplication(u32 startTitleIndex, u32 appIndex, bool addOn); + void waitForButtonPress(); void printProgressBar(progress_ctx_t *progressCtx, bool calcData, u64 chunkSize); @@ -208,6 +212,12 @@ void setProgressBarError(progress_ctx_t *progressCtx); void convertDataToHexString(const u8 *data, const u32 dataSize, char *outBuf, const u32 outBufSize); +bool checkIfFileExists(const char *path); + +bool yesNoPrompt(const char *message); + +bool checkIfDumpedNspContainsConsoleData(const char *nspPath); + void removeDirectory(const char *path); void gameCardDumpNSWDBCheck(u32 crc);