mirror of
https://github.com/DarkMatterCore/nxdumptool.git
synced 2024-11-26 04:02:11 +00:00
40ca7e2d6c
Worked on the legacy codebase earlier and now I need a bath...
5577 lines
237 KiB
C
5577 lines
237 KiB
C
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <dirent.h>
|
|
#include <memory.h>
|
|
#include <limits.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <math.h>
|
|
#include <ctype.h>
|
|
|
|
#include "crc32_fast.h"
|
|
#include "dumper.h"
|
|
#include "fs_ext.h"
|
|
#include "ui.h"
|
|
#include "nca.h"
|
|
#include "keys.h"
|
|
#include "save.h"
|
|
|
|
/* Extern variables */
|
|
|
|
extern nca_keyset_t nca_keyset;
|
|
|
|
extern u64 freeSpace;
|
|
|
|
extern bool highlight;
|
|
extern int breaks;
|
|
extern int font_height;
|
|
|
|
extern gamecard_ctx_t gameCardInfo;
|
|
|
|
extern u32 titleAppCount, titlePatchCount, titleAddOnCount;
|
|
extern u32 sdCardTitleAppCount, sdCardTitlePatchCount, sdCardTitleAddOnCount;
|
|
extern u32 emmcTitleAppCount, emmcTitlePatchCount, emmcTitleAddOnCount;
|
|
|
|
extern base_app_ctx_t *baseAppEntries;
|
|
extern patch_addon_ctx_t *patchEntries, *addOnEntries;
|
|
|
|
extern exefs_ctx_t exeFsContext;
|
|
extern romfs_ctx_t romFsContext;
|
|
extern bktr_ctx_t bktrContext;
|
|
|
|
extern char curRomFsPath[NAME_BUF_LEN];
|
|
extern u32 curRomFsDirOffset;
|
|
|
|
extern u8 *enabledNormalIconBuf;
|
|
extern u8 *enabledHighlightIconBuf;
|
|
extern u8 *disabledNormalIconBuf;
|
|
extern u8 *disabledHighlightIconBuf;
|
|
|
|
extern u8 *dumpBuf;
|
|
|
|
extern char strbuf[NAME_BUF_LEN];
|
|
|
|
extern u64 freeSpace;
|
|
extern char freeSpaceStr[32];
|
|
|
|
extern char cfwDirStr[32];
|
|
|
|
static void dumpStartMsg()
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Dump procedure started. Hold " NINTENDO_FONT_B " to cancel.");
|
|
breaks++;
|
|
}
|
|
|
|
bool dumpNXCardImage(xciOptions *xciDumpCfg)
|
|
{
|
|
if (!xciDumpCfg)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid XCI configuration struct!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
bool isFat32 = xciDumpCfg->isFat32;
|
|
bool setXciArchiveBit = xciDumpCfg->setXciArchiveBit;
|
|
bool keepCert = xciDumpCfg->keepCert;
|
|
bool trimDump = xciDumpCfg->trimDump;
|
|
bool calcCrc = xciDumpCfg->calcCrc;
|
|
bool useNoIntroLookup = xciDumpCfg->useNoIntroLookup;
|
|
bool useBrackets = xciDumpCfg->useBrackets;
|
|
|
|
u64 partitionOffset = 0, xciDataSize = 0, n;
|
|
u64 partitionSizes[ISTORAGE_PARTITION_CNT];
|
|
char dumpPath[NAME_BUF_LEN] = {'\0'};
|
|
u32 partition;
|
|
Result result;
|
|
bool proceed = true, success = false, fat32_error = false;
|
|
FILE *outFile = NULL;
|
|
u8 splitIndex = 0;
|
|
u32 certCrc = 0, certlessCrc = 0;
|
|
|
|
memset(dumpBuf, 0, DUMP_BUFFER_SIZE);
|
|
|
|
progress_ctx_t progressCtx;
|
|
memset(&progressCtx, 0, sizeof(progress_ctx_t));
|
|
|
|
bool seqDumpMode = false, seqDumpFileRemove = false, seqDumpFinish = false;
|
|
char seqDumpFilename[NAME_BUF_LEN] = {'\0'};
|
|
FILE *seqDumpFile = NULL;
|
|
u64 seqDumpFileSize = 0, seqDumpSessionOffset = 0;
|
|
|
|
sequentialXciCtx seqXciCtx;
|
|
memset(&seqXciCtx, 0, sizeof(sequentialXciCtx));
|
|
|
|
char tmp_idx[5];
|
|
|
|
size_t read_res, write_res;
|
|
|
|
char *dumpName = generateGameCardDumpName(useBrackets);
|
|
if (!dumpName)
|
|
{
|
|
// We're probably dealing with a forced XCI dump
|
|
dumpName = calloc(16, sizeof(char));
|
|
if (!dumpName)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to generate output dump name!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
sprintf(dumpName, "gamecard");
|
|
}
|
|
|
|
// Check if we're dealing with a sequential dump
|
|
snprintf(seqDumpFilename, MAX_CHARACTERS(seqDumpFilename), "%s%s.xci.seq", XCI_DUMP_PATH, dumpName);
|
|
seqDumpMode = checkIfFileExists(seqDumpFilename);
|
|
if (seqDumpMode)
|
|
{
|
|
// Open sequence file
|
|
seqDumpFile = fopen(seqDumpFilename, "rb+");
|
|
if (!seqDumpFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to open existing sequential dump reference file for reading! (\"%s\")", __func__, seqDumpFilename);
|
|
goto out;
|
|
}
|
|
|
|
// Retrieve sequence file size
|
|
fseek(seqDumpFile, 0, SEEK_END);
|
|
seqDumpFileSize = ftell(seqDumpFile);
|
|
rewind(seqDumpFile);
|
|
|
|
// Check file size
|
|
if (seqDumpFileSize != sizeof(sequentialXciCtx))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: sequential dump reference file size mismatch! (%lu != %lu)", __func__, seqDumpFileSize, sizeof(sequentialXciCtx));
|
|
seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
|
|
// Read file contents
|
|
read_res = fread(&seqXciCtx, 1, seqDumpFileSize, seqDumpFile);
|
|
rewind(seqDumpFile);
|
|
|
|
if (read_res != seqDumpFileSize)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to read %lu bytes long sequential dump reference file! (read %lu bytes)", __func__, seqDumpFileSize, read_res);
|
|
goto out;
|
|
}
|
|
|
|
// Check if the IStorage partition index is valid
|
|
if (seqXciCtx.partitionIndex > (ISTORAGE_PARTITION_CNT - 1))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid IStorage partition index in sequential dump reference file!", __func__);
|
|
seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
|
|
// Restore parameters from the sequence file
|
|
isFat32 = true;
|
|
setXciArchiveBit = false;
|
|
keepCert = seqXciCtx.keepCert;
|
|
trimDump = seqXciCtx.trimDump;
|
|
calcCrc = seqXciCtx.calcCrc;
|
|
splitIndex = seqXciCtx.partNumber;
|
|
certCrc = seqXciCtx.certCrc;
|
|
certlessCrc = seqXciCtx.certlessCrc;
|
|
progressCtx.curOffset = ((u64)seqXciCtx.partNumber * SPLIT_FILE_SEQUENTIAL_SIZE);
|
|
}
|
|
|
|
u64 partSize = (seqDumpMode ? SPLIT_FILE_SEQUENTIAL_SIZE : (!setXciArchiveBit ? SPLIT_FILE_XCI_PART_SIZE : SPLIT_FILE_NSP_PART_SIZE));
|
|
|
|
// Retrieve dump sizes for each IStorage partition
|
|
for(partition = 0; partition < ISTORAGE_PARTITION_CNT; partition++)
|
|
{
|
|
partitionSizes[partition] = gameCardInfo.IStoragePartitionSizes[partition];
|
|
xciDataSize += partitionSizes[partition];
|
|
}
|
|
|
|
if (trimDump)
|
|
{
|
|
// Change dump size for the secure IStorage partition
|
|
u64 partitionSizesSum = 0;
|
|
for(partition = 0; partition < (ISTORAGE_PARTITION_CNT - 1); partition++) partitionSizesSum += partitionSizes[partition];
|
|
|
|
partitionSizes[ISTORAGE_PARTITION_CNT - 1] = (gameCardInfo.trimmedSize - partitionSizesSum);
|
|
|
|
progressCtx.totalSize = gameCardInfo.trimmedSize;
|
|
} else {
|
|
progressCtx.totalSize = xciDataSize;
|
|
}
|
|
|
|
convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, MAX_CHARACTERS(progressCtx.totalSizeStr));
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Output dump size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize);
|
|
breaks += 2;
|
|
|
|
if (seqDumpMode)
|
|
{
|
|
// Check if the current offset doesn't exceed the total XCI size
|
|
if (progressCtx.curOffset >= progressCtx.totalSize)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid XCI offset in the sequential dump reference file!", __func__);
|
|
seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
|
|
// Check if the current partition offset doesn't exceed the partition size
|
|
if (seqXciCtx.partitionOffset >= partitionSizes[seqXciCtx.partitionIndex])
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid IStorage partition offset in the sequential dump reference file!", __func__);
|
|
seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
|
|
u64 curXciOffset = 0, restSize = 0;
|
|
|
|
for(u32 i = 0; i < seqXciCtx.partitionIndex; i++) curXciOffset += partitionSizes[i];
|
|
curXciOffset += seqXciCtx.partitionOffset;
|
|
|
|
restSize = (progressCtx.totalSize - curXciOffset);
|
|
|
|
// Check if our previously calculated XCI offset is aligned to SPLIT_FILE_SEQUENTIAL_SIZE
|
|
if (curXciOffset != progressCtx.curOffset)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: overall XCI dump offset isn't aligned to 0x%08X in the sequential dump reference file!", __func__, (u32)SPLIT_FILE_SEQUENTIAL_SIZE);
|
|
seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
|
|
// Check if there's enough free space to continue the sequential dump process
|
|
if (progressCtx.totalSize > freeSpace && ((restSize > SPLIT_FILE_SEQUENTIAL_SIZE && freeSpace < SPLIT_FILE_SEQUENTIAL_SIZE) || (restSize <= SPLIT_FILE_SEQUENTIAL_SIZE && freeSpace < restSize)))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Inform that we are resuming an already started sequential dump operation
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Resuming previous sequential dump operation. Configuration parameters overrided.");
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Keep certificate: %s | Trim output dump: %s | CRC32 checksum calculation + dump verification: %s.", (keepCert ? "Yes" : "No"), (trimDump ? "Yes" : "No"), (calcCrc ? "Yes" : "No"));
|
|
breaks += 2;
|
|
|
|
uiRefreshDisplay();
|
|
} else {
|
|
if (progressCtx.totalSize > freeSpace)
|
|
{
|
|
// Check if we have at least (SPLIT_FILE_SEQUENTIAL_SIZE + sizeof(sequentialXciCtx)) of free space
|
|
if (freeSpace < (SPLIT_FILE_SEQUENTIAL_SIZE + sizeof(sequentialXciCtx)))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Ask the user if they want to use the sequential dump mode
|
|
int cur_breaks = breaks;
|
|
|
|
if (!yesNoPrompt("There's not enough space available to generate a whole dump in this session. Do you want to use sequential dumping?\nIn this mode, the selected content will be dumped in more than one session.\nYou'll have to transfer the generated part files to a PC before continuing the process in the next session."))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
goto out;
|
|
}
|
|
|
|
// Remove the prompt from the screen
|
|
breaks = cur_breaks;
|
|
uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), BG_COLOR_RGB);
|
|
uiRefreshDisplay();
|
|
|
|
// Modify config parameters
|
|
isFat32 = true;
|
|
setXciArchiveBit = false;
|
|
|
|
partSize = SPLIT_FILE_SEQUENTIAL_SIZE;
|
|
|
|
seqDumpMode = true;
|
|
seqDumpFileSize = sizeof(sequentialXciCtx);
|
|
|
|
// Fill information in our sequential context
|
|
seqXciCtx.keepCert = keepCert;
|
|
seqXciCtx.trimDump = trimDump;
|
|
seqXciCtx.calcCrc = calcCrc;
|
|
|
|
// Create sequential reference file and keep the handle to it opened
|
|
seqDumpFile = fopen(seqDumpFilename, "wb+");
|
|
if (!seqDumpFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to create sequential dump reference file! (\"%s\")", __func__, seqDumpFilename);
|
|
goto out;
|
|
}
|
|
|
|
write_res = fwrite(&seqXciCtx, 1, seqDumpFileSize, seqDumpFile);
|
|
rewind(seqDumpFile);
|
|
|
|
if (write_res != seqDumpFileSize)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk to the sequential dump reference file! (wrote %lu bytes)", __func__, seqDumpFileSize, write_res);
|
|
seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
|
|
// Update free space
|
|
freeSpace -= seqDumpFileSize;
|
|
}
|
|
}
|
|
|
|
if (seqDumpMode)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.xci.%02u", XCI_DUMP_PATH, dumpName, splitIndex);
|
|
} else {
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32)
|
|
{
|
|
if (setXciArchiveBit)
|
|
{
|
|
// Temporary, we'll use this to check if the dump already exists (it should have the archive bit set if so)
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.xci", XCI_DUMP_PATH, dumpName);
|
|
} else {
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.xc%u", XCI_DUMP_PATH, dumpName, splitIndex);
|
|
}
|
|
} else {
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.xci", XCI_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(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
goto out;
|
|
} else {
|
|
// Remove the prompt from the screen
|
|
breaks = cur_breaks;
|
|
uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), BG_COLOR_RGB);
|
|
}
|
|
}
|
|
|
|
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
|
|
remove(dumpPath);
|
|
fsdevDeleteDirectoryRecursively(dumpPath);
|
|
|
|
mkdir(dumpPath, 0744);
|
|
|
|
sprintf(tmp_idx, "/%02u", splitIndex);
|
|
strcat(dumpPath, tmp_idx);
|
|
}
|
|
}
|
|
|
|
outFile = fopen(dumpPath, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open output file \"%s\"!", __func__, dumpPath);
|
|
goto out;
|
|
}
|
|
|
|
// Start dump process
|
|
dumpStartMsg();
|
|
appletModeOperationWarning();
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
changeHomeButtonBlockStatus(true);
|
|
|
|
progressCtx.line_offset = (breaks + 4);
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start));
|
|
|
|
u32 startPartitionIndex = (seqDumpMode ? seqXciCtx.partitionIndex : 0);
|
|
u64 startPartitionOffset;
|
|
|
|
for(partition = startPartitionIndex; partition < ISTORAGE_PARTITION_CNT; partition++)
|
|
{
|
|
n = DUMP_BUFFER_SIZE;
|
|
|
|
startPartitionOffset = ((seqDumpMode && partition == startPartitionIndex) ? seqXciCtx.partitionOffset : 0);
|
|
|
|
openIStoragePartition idx = (openIStoragePartition)(partition + 1);
|
|
|
|
result = openGameCardStoragePartition(idx);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open IStorage partition #%u! (0x%08X)", __func__, partition, result);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
for(partitionOffset = startPartitionOffset; partitionOffset < partitionSizes[partition]; partitionOffset += n, progressCtx.curOffset += n, seqDumpSessionOffset += n)
|
|
{
|
|
if (seqDumpMode && seqDumpFinish) break;
|
|
|
|
uiFill(0, ((progressCtx.line_offset - 4) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 4, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 4), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(dumpPath, '/' ) + 1);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Dumping IStorage partition #%u...", partition);
|
|
|
|
if (n > (partitionSizes[partition] - partitionOffset)) n = (partitionSizes[partition] - partitionOffset);
|
|
|
|
// Check if the next read chunk will exceed the size of the current part file
|
|
if (seqDumpMode && (seqDumpSessionOffset + n) >= (((splitIndex - seqXciCtx.partNumber) + 1) * partSize))
|
|
{
|
|
u64 new_file_chunk_size = ((seqDumpSessionOffset + n) - (((splitIndex - seqXciCtx.partNumber) + 1) * partSize));
|
|
u64 old_file_chunk_size = (n - new_file_chunk_size);
|
|
|
|
u64 remainderDumpSize = (progressCtx.totalSize - (progressCtx.curOffset + old_file_chunk_size));
|
|
u64 remainderFreeSize = (freeSpace - (seqDumpSessionOffset + old_file_chunk_size));
|
|
|
|
// Check if we have enough space for the next part
|
|
// If so, set the chunk size to old_file_chunk_size
|
|
if ((remainderDumpSize <= partSize && remainderDumpSize > remainderFreeSize) || (remainderDumpSize > partSize && partSize > remainderFreeSize))
|
|
{
|
|
n = old_file_chunk_size;
|
|
seqDumpFinish = true;
|
|
}
|
|
}
|
|
|
|
result = readGameCardStoragePartition(partitionOffset, dumpBuf, n);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to read %lu bytes chunk at offset 0x%016lX from IStorage partition #%u! (0x%08X)", __func__, n, partitionOffset, partition, result);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
// Remove gamecard certificate
|
|
if (progressCtx.curOffset == 0 && !keepCert) memset(dumpBuf + CERT_OFFSET, 0xFF, CERT_SIZE);
|
|
|
|
if (calcCrc)
|
|
{
|
|
if (!trimDump)
|
|
{
|
|
if (keepCert)
|
|
{
|
|
if (progressCtx.curOffset == 0)
|
|
{
|
|
// Update CRC32 (with gamecard certificate)
|
|
crc32(dumpBuf, n, &certCrc);
|
|
|
|
// Backup gamecard certificate to an array
|
|
char tmpCert[CERT_SIZE] = {'\0'};
|
|
memcpy(tmpCert, dumpBuf + CERT_OFFSET, CERT_SIZE);
|
|
|
|
// Remove gamecard certificate from buffer
|
|
memset(dumpBuf + CERT_OFFSET, 0xFF, CERT_SIZE);
|
|
|
|
// Update CRC32 (without gamecard certificate)
|
|
crc32(dumpBuf, n, &certlessCrc);
|
|
|
|
// Restore gamecard certificate to buffer
|
|
memcpy(dumpBuf + CERT_OFFSET, tmpCert, CERT_SIZE);
|
|
} else {
|
|
// Update CRC32 (with gamecard certificate)
|
|
crc32(dumpBuf, n, &certCrc);
|
|
|
|
// Update CRC32 (without gamecard certificate)
|
|
crc32(dumpBuf, n, &certlessCrc);
|
|
}
|
|
} else {
|
|
// Update CRC32
|
|
crc32(dumpBuf, n, &certlessCrc);
|
|
}
|
|
} else {
|
|
// Update CRC32
|
|
crc32(dumpBuf, n, &certCrc);
|
|
}
|
|
}
|
|
|
|
if ((seqDumpMode || (!seqDumpMode && progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32)) && (progressCtx.curOffset + n) >= ((splitIndex + 1) * partSize))
|
|
{
|
|
u64 new_file_chunk_size = ((progressCtx.curOffset + n) - ((splitIndex + 1) * partSize));
|
|
u64 old_file_chunk_size = (n - new_file_chunk_size);
|
|
|
|
if (old_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf, 1, old_file_chunk_size, outFile);
|
|
if (write_res != old_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, old_file_chunk_size, progressCtx.curOffset, splitIndex, write_res);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
fclose(outFile);
|
|
outFile = NULL;
|
|
|
|
if (((seqDumpMode && !seqDumpFinish) || !seqDumpMode) && (new_file_chunk_size > 0 || (progressCtx.curOffset + n) < progressCtx.totalSize))
|
|
{
|
|
splitIndex++;
|
|
|
|
if (seqDumpMode)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.xci.%02u", XCI_DUMP_PATH, dumpName, splitIndex);
|
|
} else {
|
|
if (setXciArchiveBit)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.xci/%02u", XCI_DUMP_PATH, dumpName, splitIndex);
|
|
} else {
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.xc%u", XCI_DUMP_PATH, dumpName, splitIndex);
|
|
}
|
|
}
|
|
|
|
outFile = fopen(dumpPath, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to open output file for part #%u!", __func__, splitIndex);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
if (new_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf + old_file_chunk_size, 1, new_file_chunk_size, outFile);
|
|
if (write_res != new_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, write_res);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
write_res = fwrite(dumpBuf, 1, n, outFile);
|
|
if (write_res != n)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", __func__, n, progressCtx.curOffset, write_res);
|
|
|
|
if (!seqDumpMode && (progressCtx.curOffset + n) > FAT32_FILESIZE_LIMIT)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 4), FONT_COLOR_RGB, "You're probably using a FAT32 partition. Make sure to enable the \"Split output dump\" option.");
|
|
fat32_error = true;
|
|
}
|
|
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (seqDumpMode) progressCtx.seqDumpCurOffset = seqDumpSessionOffset;
|
|
printProgressBar(&progressCtx, true, n);
|
|
|
|
if ((progressCtx.curOffset + n) < progressCtx.totalSize && cancelProcessCheck(&progressCtx))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
closeGameCardStoragePartition();
|
|
|
|
if (!proceed)
|
|
{
|
|
if (seqDumpMode) seqDumpFileRemove = true;
|
|
break;
|
|
}
|
|
|
|
// Support empty files
|
|
if (!partitionSizes[partition])
|
|
{
|
|
uiFill(0, ((progressCtx.line_offset - 4) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 4, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 4), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(dumpPath, '/' ) + 1);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Dumping IStorage partition #%u...", partition);
|
|
|
|
printProgressBar(&progressCtx, false, 0);
|
|
}
|
|
|
|
if (progressCtx.curOffset >= progressCtx.totalSize || (seqDumpMode && seqDumpFinish)) success = true;
|
|
|
|
if (seqDumpMode && seqDumpFinish) break;
|
|
}
|
|
|
|
if (!proceed) setProgressBarError(&progressCtx);
|
|
|
|
breaks = (progressCtx.line_offset + 2);
|
|
if (fat32_error) breaks += 2;
|
|
|
|
if (outFile) fclose(outFile);
|
|
|
|
if (success)
|
|
{
|
|
if (seqDumpMode)
|
|
{
|
|
if (seqDumpFinish)
|
|
{
|
|
// Update the sequence reference file in the SD card
|
|
seqXciCtx.partNumber = (splitIndex + 1);
|
|
seqXciCtx.partitionIndex = partition;
|
|
seqXciCtx.partitionOffset = partitionOffset;
|
|
|
|
if (calcCrc)
|
|
{
|
|
seqXciCtx.certCrc = certCrc;
|
|
seqXciCtx.certlessCrc = certlessCrc;
|
|
}
|
|
|
|
write_res = fwrite(&seqXciCtx, 1, seqDumpFileSize, seqDumpFile);
|
|
if (write_res != seqDumpFileSize)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk to the sequential dump reference file! (wrote %lu bytes)", __func__, seqDumpFileSize, write_res);
|
|
success = false;
|
|
seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
} else {
|
|
// Mark the file for deletion
|
|
seqDumpFileRemove = true;
|
|
|
|
// Finally disable sequential dump mode flag
|
|
seqDumpMode = false;
|
|
}
|
|
}
|
|
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now));
|
|
progressCtx.now -= progressCtx.start;
|
|
|
|
formatETAString(progressCtx.now, progressCtx.etaInfo, MAX_CHARACTERS(progressCtx.etaInfo));
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Process successfully completed after %s!", progressCtx.etaInfo);
|
|
|
|
if (seqDumpMode && seqDumpFinish)
|
|
{
|
|
breaks += 2;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Please remember to exit the application and transfer the generated part file(s) to a PC before continuing in the next session!");
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Do NOT move the \"%s\" file!", strrchr(seqDumpFilename, '/' ) + 1);
|
|
}
|
|
|
|
if (!seqDumpMode && calcCrc)
|
|
{
|
|
breaks++;
|
|
|
|
if (!trimDump)
|
|
{
|
|
if (keepCert)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "XCI dump CRC32 checksum (with certificate): %08X | XCI dump CRC32 checksum (without certificate): %08X", certCrc, certlessCrc);
|
|
} else {
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "XCI dump CRC32 checksum: %08X", certlessCrc);
|
|
}
|
|
|
|
breaks++;
|
|
|
|
uiRefreshDisplay();
|
|
|
|
if (useNoIntroLookup)
|
|
{
|
|
noIntroDumpCheck(false, certlessCrc);
|
|
} else {
|
|
gameCardDumpNSWDBCheck(certlessCrc);
|
|
}
|
|
} else {
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "XCI dump CRC32 checksum: %08X", certCrc);
|
|
breaks++;
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Dump verification disabled (not compatible with trimmed dumps).");
|
|
}
|
|
}
|
|
|
|
// Set archive bit (only for FAT32 and if the required option is enabled)
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32 && setXciArchiveBit)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.xci", XCI_DUMP_PATH, dumpName);
|
|
result = fsdevSetConcatenationFileAttribute(dumpPath);
|
|
if (R_FAILED(result))
|
|
{
|
|
breaks += 2;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Warning: failed to set archive bit on output directory! (0x%08X)", result);
|
|
}
|
|
}
|
|
} else {
|
|
if (seqDumpMode)
|
|
{
|
|
for(u8 i = 0; i <= splitIndex; i++)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.xci.%02u", XCI_DUMP_PATH, dumpName, i);
|
|
remove(dumpPath);
|
|
}
|
|
} else {
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32)
|
|
{
|
|
if (setXciArchiveBit)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.xci", XCI_DUMP_PATH, dumpName);
|
|
fsdevDeleteDirectoryRecursively(dumpPath);
|
|
} else {
|
|
for(u8 i = 0; i <= splitIndex; i++)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.xc%u", XCI_DUMP_PATH, dumpName, i);
|
|
remove(dumpPath);
|
|
}
|
|
}
|
|
} else {
|
|
remove(dumpPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
out:
|
|
if (dumpName) free(dumpName);
|
|
|
|
if (seqDumpFile) fclose(seqDumpFile);
|
|
|
|
if (seqDumpFileRemove) remove(seqDumpFilename);
|
|
|
|
breaks += 2;
|
|
|
|
changeHomeButtonBlockStatus(false);
|
|
|
|
return success;
|
|
}
|
|
|
|
int dumpNintendoSubmissionPackage(nspDumpType selectedNspDumpType, u32 titleIndex, nspOptions *nspDumpCfg, bool batch)
|
|
{
|
|
int ret = -1;
|
|
|
|
if (!nspDumpCfg)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid NSP configuration struct!", __func__);
|
|
breaks += 2;
|
|
return ret;
|
|
}
|
|
|
|
bool isFat32 = nspDumpCfg->isFat32;
|
|
bool useNoIntroLookup = nspDumpCfg->useNoIntroLookup;
|
|
bool removeConsoleData = nspDumpCfg->removeConsoleData;
|
|
bool tiklessDump = nspDumpCfg->tiklessDump;
|
|
bool npdmAcidRsaPatch = nspDumpCfg->npdmAcidRsaPatch;
|
|
bool dumpDeltaFragments = nspDumpCfg->dumpDeltaFragments;
|
|
bool useBrackets = nspDumpCfg->useBrackets;
|
|
bool preInstall = false;
|
|
|
|
Result result;
|
|
u32 i = 0, j = 0;
|
|
|
|
NcmStorageId curStorageId;
|
|
NcmContentMetaType metaType;
|
|
u32 titleCount = 0, ncmTitleIndex = 0;
|
|
|
|
char dumpPath[NAME_BUF_LEN] = {'\0'};
|
|
|
|
NcmContentInfo *titleContentInfos = NULL;
|
|
u32 titleContentInfoCnt = 0;
|
|
|
|
NcmContentStorage ncmStorage;
|
|
memset(&ncmStorage, 0, sizeof(NcmContentStorage));
|
|
|
|
cnmt_xml_program_info xml_program_info;
|
|
cnmt_xml_content_info *xml_content_info = NULL;
|
|
|
|
NcmContentId ncaId;
|
|
u8 ncaHeader[NCA_FULL_HEADER_LENGTH] = {0};
|
|
nca_header_t dec_nca_header;
|
|
|
|
nca_cnmt_mod_data ncaCnmtMod;
|
|
memset(&ncaCnmtMod, 0, sizeof(nca_cnmt_mod_data));
|
|
|
|
u32 ncaProgramModCnt = 0;
|
|
nca_program_mod_data *ncaProgramMod = NULL;
|
|
|
|
title_rights_ctx rights_info;
|
|
memset(&rights_info, 0, sizeof(title_rights_ctx));
|
|
|
|
u32 cnmtNcaIndex = 0;
|
|
u8 *cnmtNcaBuf = NULL;
|
|
bool cnmtFound = false;
|
|
char *cnmtXml = NULL;
|
|
|
|
u32 xml_rec_cnt = 0;
|
|
xml_record_info *xml_records = NULL, *tmp_xml_rec = NULL;
|
|
|
|
pfs0_header nspPfs0Header;
|
|
memset(&nspPfs0Header, 0, sizeof(pfs0_header));
|
|
nspPfs0Header.magic = __builtin_bswap32(PFS0_MAGIC);
|
|
|
|
pfs0_file_entry *nspPfs0EntryTable = NULL;
|
|
|
|
char *nspPfs0StrTable = NULL;
|
|
u64 nspPfs0StrTableSize = 0;
|
|
|
|
u64 fullPfs0HeaderSize = 0;
|
|
|
|
u8 **nspPfs0FilePtrs = NULL;
|
|
|
|
Sha256Context nca_hash_ctx;
|
|
sha256ContextCreate(&nca_hash_ctx);
|
|
|
|
u64 n, fileOffset;
|
|
FILE *outFile = NULL;
|
|
u8 splitIndex = 0;
|
|
u32 crc = 0;
|
|
bool proceed = true, dumping = false, fat32_error = false, removeFile = true;
|
|
|
|
progress_ctx_t progressCtx;
|
|
memset(&progressCtx, 0, sizeof(progress_ctx_t));
|
|
|
|
bool seqDumpMode = false, seqDumpFileRemove = false, seqDumpFinish = false;
|
|
char seqDumpFilename[NAME_BUF_LEN] = {'\0'};
|
|
FILE *seqDumpFile = NULL;
|
|
u64 seqDumpFileSize = 0, seqDumpSessionOffset = 0;
|
|
u8 *seqDumpNcaHashes = NULL;
|
|
|
|
sequentialNspCtx seqNspCtx;
|
|
memset(&seqNspCtx, 0, sizeof(sequentialNspCtx));
|
|
|
|
char pfs0HeaderFilename[NAME_BUF_LEN] = {'\0'};
|
|
FILE *pfs0HeaderFile = NULL;
|
|
|
|
char tmp_idx[5];
|
|
|
|
size_t read_res, write_res;
|
|
|
|
if ((selectedNspDumpType == DUMP_APP_NSP && !baseAppEntries) || (selectedNspDumpType == DUMP_PATCH_NSP && !patchEntries) || (selectedNspDumpType == DUMP_ADDON_NSP && !addOnEntries))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: title storage ID unavailable!", __func__);
|
|
breaks += 2;
|
|
return ret;
|
|
}
|
|
|
|
if ((selectedNspDumpType == DUMP_APP_NSP && titleIndex >= titleAppCount) || (selectedNspDumpType == DUMP_PATCH_NSP && titleIndex >= titlePatchCount) || (selectedNspDumpType == DUMP_ADDON_NSP && titleIndex >= titleAddOnCount))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid title index!", __func__);
|
|
breaks += 2;
|
|
return ret;
|
|
}
|
|
|
|
curStorageId = (selectedNspDumpType == DUMP_APP_NSP ? baseAppEntries[titleIndex].storageId : (selectedNspDumpType == DUMP_PATCH_NSP ? patchEntries[titleIndex].storageId : addOnEntries[titleIndex].storageId));
|
|
|
|
ncmTitleIndex = (selectedNspDumpType == DUMP_APP_NSP ? baseAppEntries[titleIndex].ncmIndex : (selectedNspDumpType == DUMP_PATCH_NSP ? patchEntries[titleIndex].ncmIndex : addOnEntries[titleIndex].ncmIndex));
|
|
|
|
metaType = (selectedNspDumpType == DUMP_APP_NSP ? NcmContentMetaType_Application : (selectedNspDumpType == DUMP_PATCH_NSP ? NcmContentMetaType_Patch : NcmContentMetaType_AddOnContent));
|
|
|
|
switch(curStorageId)
|
|
{
|
|
case NcmStorageId_GameCard:
|
|
titleCount = (selectedNspDumpType == DUMP_APP_NSP ? titleAppCount : (selectedNspDumpType == DUMP_PATCH_NSP ? titlePatchCount : titleAddOnCount));
|
|
break;
|
|
case NcmStorageId_SdCard:
|
|
titleCount = (selectedNspDumpType == DUMP_APP_NSP ? sdCardTitleAppCount : (selectedNspDumpType == DUMP_PATCH_NSP ? sdCardTitlePatchCount : sdCardTitleAddOnCount));
|
|
break;
|
|
case NcmStorageId_BuiltInUser:
|
|
titleCount = (selectedNspDumpType == DUMP_APP_NSP ? emmcTitleAppCount : (selectedNspDumpType == DUMP_PATCH_NSP ? emmcTitlePatchCount : emmcTitleAddOnCount));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
char *dumpName = generateNSPDumpName(selectedNspDumpType, titleIndex, useBrackets);
|
|
if (!dumpName)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to generate output dump name!", __func__);
|
|
breaks += 2;
|
|
return ret;
|
|
}
|
|
|
|
if (!batch)
|
|
{
|
|
snprintf(seqDumpFilename, MAX_CHARACTERS(seqDumpFilename), "%s%s.nsp.seq", NSP_DUMP_PATH, dumpName);
|
|
snprintf(pfs0HeaderFilename, MAX_CHARACTERS(pfs0HeaderFilename), "%s%s.nsp.hdr", NSP_DUMP_PATH, dumpName);
|
|
|
|
// Check if we're dealing with a sequential dump
|
|
seqDumpMode = checkIfFileExists(seqDumpFilename);
|
|
if (seqDumpMode)
|
|
{
|
|
// Open sequence file
|
|
seqDumpFile = fopen(seqDumpFilename, "rb+");
|
|
if (!seqDumpFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to open existing sequential dump reference file for reading! (\"%s\")", __func__, seqDumpFilename);
|
|
goto out;
|
|
}
|
|
|
|
// Retrieve sequence file size
|
|
fseek(seqDumpFile, 0, SEEK_END);
|
|
seqDumpFileSize = ftell(seqDumpFile);
|
|
rewind(seqDumpFile);
|
|
|
|
// Read sequentialNspCtx struct info
|
|
read_res = fread(&seqNspCtx, 1, sizeof(sequentialNspCtx), seqDumpFile);
|
|
if (read_res != sizeof(sequentialNspCtx))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to read %lu bytes chunk from the sequential dump reference file! (read %lu bytes)", __func__, sizeof(sequentialNspCtx), read_res);
|
|
goto out;
|
|
}
|
|
|
|
// Check if the storage ID is right
|
|
if (seqNspCtx.storageId != curStorageId)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid source storage ID in sequential dump reference file!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Check if the Program NCA mod count field is valid
|
|
if (seqNspCtx.programNcaModCount > 0 && !seqNspCtx.npdmAcidRsaPatch)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid Program NCA mod count sequential dump reference file!", __func__);
|
|
seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
|
|
// Check file size
|
|
if (seqDumpFileSize != (sizeof(sequentialNspCtx) + (seqNspCtx.ncaCount * SHA256_HASH_SIZE) + (seqNspCtx.programNcaModCount * NCA_FULL_HEADER_LENGTH)))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid sequential dump reference file size!", __func__);
|
|
seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
|
|
// Allocate memory for the NCA hashes
|
|
seqDumpNcaHashes = calloc(1, seqNspCtx.ncaCount * SHA256_HASH_SIZE);
|
|
if (!seqDumpNcaHashes)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to allocate memory for NCA hashes from the sequential dump reference file!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Read NCA hashes
|
|
read_res = fread(seqDumpNcaHashes, 1, seqNspCtx.ncaCount * SHA256_HASH_SIZE, seqDumpFile);
|
|
if (read_res != (seqNspCtx.ncaCount * SHA256_HASH_SIZE))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to read %lu bytes chunk from the sequential dump reference file! (read %lu bytes)", __func__, seqNspCtx.ncaCount * SHA256_HASH_SIZE, read_res);
|
|
goto out;
|
|
}
|
|
|
|
// Restore parameters from the sequence file
|
|
isFat32 = true;
|
|
removeConsoleData = seqNspCtx.removeConsoleData;
|
|
tiklessDump = seqNspCtx.tiklessDump;
|
|
npdmAcidRsaPatch = seqNspCtx.npdmAcidRsaPatch;
|
|
preInstall = seqNspCtx.preInstall;
|
|
splitIndex = seqNspCtx.partNumber;
|
|
progressCtx.curOffset = ((u64)seqNspCtx.partNumber * SPLIT_FILE_SEQUENTIAL_SIZE);
|
|
}
|
|
}
|
|
|
|
u64 partSize = (seqDumpMode ? SPLIT_FILE_SEQUENTIAL_SIZE : SPLIT_FILE_NSP_PART_SIZE);
|
|
|
|
if (!batch)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Retrieving information from encrypted NCA content files...");
|
|
uiRefreshDisplay();
|
|
breaks += 2;
|
|
}
|
|
|
|
if (!retrieveContentInfosFromTitle(curStorageId, metaType, titleCount, ncmTitleIndex, &titleContentInfos, &titleContentInfoCnt))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, strbuf);
|
|
goto out;
|
|
}
|
|
|
|
// If we're dealing with a gamecard, open the Secure HFS0 partition (IStorage partition #1) to read NCA data
|
|
// We may also need to retrieve a ticket if we're dealing with a Patch with titlekey crypto
|
|
if (curStorageId == NcmStorageId_GameCard)
|
|
{
|
|
result = openGameCardStoragePartition(ISTORAGE_PARTITION_SECURE);
|
|
if (R_FAILED(result))
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: failed to open IStorage partition #1! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
result = ncmOpenContentStorage(&ncmStorage, curStorageId);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: ncmOpenContentStorage failed! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
|
|
// Fill information for our CNMT XML
|
|
memset(&xml_program_info, 0, sizeof(cnmt_xml_program_info));
|
|
xml_program_info.type = (u8)metaType;
|
|
xml_program_info.title_id = (selectedNspDumpType == DUMP_APP_NSP ? baseAppEntries[titleIndex].titleId : (selectedNspDumpType == DUMP_PATCH_NSP ? patchEntries[titleIndex].titleId : addOnEntries[titleIndex].titleId));
|
|
xml_program_info.version = (selectedNspDumpType == DUMP_APP_NSP ? baseAppEntries[titleIndex].version : (selectedNspDumpType == DUMP_PATCH_NSP ? patchEntries[titleIndex].version : addOnEntries[titleIndex].version));
|
|
xml_program_info.nca_cnt = titleContentInfoCnt;
|
|
|
|
xml_content_info = calloc(titleContentInfoCnt, sizeof(cnmt_xml_content_info));
|
|
if (!xml_content_info)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to allocate memory for the CNMT XML content info struct!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Fill our CNMT XML content records, leaving the CNMT NCA at the end
|
|
u32 titleContentInfoIndex;
|
|
for(i = 0, titleContentInfoIndex = 0; titleContentInfoIndex < titleContentInfoCnt; i++, titleContentInfoIndex++)
|
|
{
|
|
if (!cnmtFound && titleContentInfos[titleContentInfoIndex].content_type == NcmContentType_Meta)
|
|
{
|
|
cnmtFound = true;
|
|
cnmtNcaIndex = titleContentInfoIndex;
|
|
i--;
|
|
continue;
|
|
}
|
|
|
|
// Skip Delta Fragments and/or any other unknown content types (only if the related option is disabled)
|
|
// Delta Fragments are used to update from a certain version to another version without needing to install the whole update
|
|
// For any dumping purposes, they're useless, because they just increase the size of the output dump. The more updates come out for a title, the more Delta Fragments there will be available for that title
|
|
// Also, since they're basically an eShop thing, they're not available in gamecards (so in this particular case, we need to skip them anyway)
|
|
// However, their content records must be kept intact in the CNMT NCA
|
|
if (titleContentInfos[titleContentInfoIndex].content_type >= NcmContentType_DeltaFragment && !dumpDeltaFragments)
|
|
{
|
|
xml_program_info.nca_cnt--;
|
|
i--;
|
|
continue;
|
|
}
|
|
|
|
// Fill information for our CNMT XML
|
|
xml_content_info[i].type = titleContentInfos[titleContentInfoIndex].content_type;
|
|
memcpy(xml_content_info[i].nca_id, titleContentInfos[titleContentInfoIndex].content_id.c, SHA256_HASH_SIZE / 2); // Temporary
|
|
convertDataToHexString(titleContentInfos[titleContentInfoIndex].content_id.c, SHA256_HASH_SIZE / 2, xml_content_info[i].nca_id_str, SHA256_HASH_SIZE + 1); // Temporary
|
|
convertNcaSizeToU64(titleContentInfos[titleContentInfoIndex].size, &(xml_content_info[i].size));
|
|
xml_content_info[i].id_offset = titleContentInfos[titleContentInfoIndex].id_offset;
|
|
convertDataToHexString(xml_content_info[i].hash, SHA256_HASH_SIZE, xml_content_info[i].hash_str, (SHA256_HASH_SIZE * 2) + 1); // Temporary
|
|
|
|
memcpy(&ncaId, &(titleContentInfos[titleContentInfoIndex].content_id), sizeof(NcmContentId));
|
|
|
|
if (!readNcaDataByContentId(&ncmStorage, &ncaId, 0, ncaHeader, NCA_FULL_HEADER_LENGTH))
|
|
{
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to read header from NCA \"%s\"!", __func__, xml_content_info[i].nca_id_str);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
// Decrypt the NCA header
|
|
// Don't retrieve the ticket and/or titlekey if we're dealing with a Patch with titlekey crypto bundled with the inserted gamecard
|
|
if (!decryptNcaHeader(ncaHeader, NCA_FULL_HEADER_LENGTH, &dec_nca_header, &rights_info, xml_content_info[i].decrypted_nca_keys, (curStorageId != NcmStorageId_GameCard)))
|
|
{
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
// Check if this particular content has a populated Rights ID field
|
|
bool has_rights_id = false;
|
|
|
|
for(j = 0; j < 0x10; j++)
|
|
{
|
|
if (dec_nca_header.rights_id[j] != 0)
|
|
{
|
|
has_rights_id = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check if the missing ticket flag is enabled
|
|
// If so, we may be dealing with a preinstalled title
|
|
if (curStorageId != NcmStorageId_GameCard && has_rights_id && rights_info.missing_tik && !preInstall)
|
|
{
|
|
// Only display the pre-install prompt if we're not running a batch / sequential dump operation (excluding the first run of the latter)
|
|
if (!batch && !seqDumpMode)
|
|
{
|
|
int cur_breaks = breaks;
|
|
breaks += 2;
|
|
|
|
proceed = yesNoPrompt("This is probably a pre-installed title, which explains why a ticket for it couldn't be found (even though its Rights ID field isn't empty).\nDo you want to proceed with the dump procedure anyway?\nBear in mind that no content decryption will be possible for this title in its current status.");
|
|
if (!proceed)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
break;
|
|
} else {
|
|
breaks = cur_breaks;
|
|
preInstall = true;
|
|
}
|
|
}
|
|
|
|
// Remove the prompt / error from the screen
|
|
uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), BG_COLOR_RGB);
|
|
}
|
|
|
|
// Fill information for our CNMT XML
|
|
xml_content_info[i].keyblob = (dec_nca_header.crypto_type2 > dec_nca_header.crypto_type ? dec_nca_header.crypto_type2 : dec_nca_header.crypto_type);
|
|
|
|
if (curStorageId == NcmStorageId_GameCard)
|
|
{
|
|
// Modify content distribution type
|
|
// It's always set to 1 (gamecard) in Applications and Add-Ons bundled in gamecards
|
|
// It's always set to 0 (download) in Patches bundled in gamecards. But if we're dealing with a custom XCI mounted through SX OS, we may need to change that
|
|
dec_nca_header.distribution = 0;
|
|
|
|
if (selectedNspDumpType == DUMP_APP_NSP || selectedNspDumpType == DUMP_ADDON_NSP)
|
|
{
|
|
// Application and AddOn titles don't have a populated Rights ID field when bundled in gamecards
|
|
if (has_rights_id)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: Rights ID field in NCA header not empty!", __func__);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
// Patch ACID public RSA key and recreate the NCA NPDM signature if we're dealing with the Program NCA
|
|
if (xml_content_info[i].type == NcmContentType_Program && npdmAcidRsaPatch)
|
|
{
|
|
if (!processProgramNca(&ncmStorage, &ncaId, &dec_nca_header, &(xml_content_info[i]), &ncaProgramMod, &ncaProgramModCnt, i))
|
|
{
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
} else
|
|
if (selectedNspDumpType == DUMP_PATCH_NSP)
|
|
{
|
|
// Patch titles *do* have a populated Rights ID field and a ticket + certificate chain combination when bundled in gamecards
|
|
// Depending on the dump settings, we may need to change or remove that
|
|
// If no Rights ID is available, we may be dealing with a custom XCI mounted through SX OS. In this particular case, no further modifications should be needed
|
|
if (has_rights_id)
|
|
{
|
|
// Retrieve the ticket from the HFS0 partition in the gamecard
|
|
if (!retrieveTitleKeyFromGameCardTicket(&rights_info, xml_content_info[i].decrypted_nca_keys))
|
|
{
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
// Mess with the NCA header if we're dealing with a NCA with a populated Rights ID field and if tiklessDump is true (removeConsoleData is ignored)
|
|
if (tiklessDump)
|
|
{
|
|
// 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);
|
|
|
|
// Patch ACID pubkey and recreate NCA NPDM signature if we're dealing with the Program NCA
|
|
if (xml_content_info[i].type == NcmContentType_Program && npdmAcidRsaPatch)
|
|
{
|
|
if (!processProgramNca(&ncmStorage, &ncaId, &dec_nca_header, &(xml_content_info[i]), &ncaProgramMod, &ncaProgramModCnt, i))
|
|
{
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else
|
|
if (curStorageId == NcmStorageId_SdCard || curStorageId == NcmStorageId_BuiltInUser)
|
|
{
|
|
// Only mess with the NCA header if we're dealing with a content with a populated Rights ID field, and if both removeConsoleData and tiklessDump are true
|
|
// This will only be done if we were able to retrieve the ticket for this title
|
|
if (has_rights_id && rights_info.retrieved_tik && removeConsoleData && tiklessDump)
|
|
{
|
|
// 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);
|
|
|
|
// Patch ACID pubkey and recreate NCA NPDM signature if we're dealing with the Program NCA
|
|
if (xml_content_info[i].type == NcmContentType_Program && npdmAcidRsaPatch)
|
|
{
|
|
if (!processProgramNca(&ncmStorage, &ncaId, &dec_nca_header, &(xml_content_info[i]), &ncaProgramMod, &ncaProgramModCnt, i))
|
|
{
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((!has_rights_id || (has_rights_id && rights_info.retrieved_tik)) && (xml_content_info[i].type == NcmContentType_Program || xml_content_info[i].type == NcmContentType_Control || xml_content_info[i].type == NcmContentType_LegalInformation))
|
|
{
|
|
// Reallocate XML records
|
|
tmp_xml_rec = realloc(xml_records, (xml_rec_cnt + 1) * sizeof(xml_record_info));
|
|
if (!tmp_xml_rec)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: error reallocating XML records buffer!", __func__);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
xml_records = tmp_xml_rec;
|
|
tmp_xml_rec = NULL;
|
|
|
|
memset(&(xml_records[xml_rec_cnt]), 0, sizeof(xml_record_info));
|
|
xml_records[xml_rec_cnt].nca_index = i;
|
|
|
|
xml_rec_cnt++;
|
|
|
|
// Generate programinfo.xml
|
|
if (xml_content_info[i].type == NcmContentType_Program)
|
|
{
|
|
bool use_acid_pubkey = false;
|
|
|
|
for(j = 0; j < ncaProgramModCnt; j++)
|
|
{
|
|
if (ncaProgramMod[j].nca_index == i)
|
|
{
|
|
use_acid_pubkey = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!generateProgramInfoXml(&ncmStorage, &ncaId, &dec_nca_header, xml_content_info[i].decrypted_nca_keys, use_acid_pubkey, &(xml_records[xml_rec_cnt - 1].xml_data), &(xml_records[xml_rec_cnt - 1].xml_size)))
|
|
{
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Retrieve NACP data (XML and icons)
|
|
if (xml_content_info[i].type == NcmContentType_Control)
|
|
{
|
|
if (!retrieveNacpDataFromNca(&ncmStorage, &ncaId, &dec_nca_header, xml_content_info[i].decrypted_nca_keys, &(xml_records[xml_rec_cnt - 1].xml_data), &(xml_records[xml_rec_cnt - 1].xml_size), &(xml_records[xml_rec_cnt - 1].nacp_icons), &(xml_records[xml_rec_cnt - 1].nacp_icon_cnt)))
|
|
{
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Retrieve legalinfo.xml
|
|
if (xml_content_info[i].type == NcmContentType_LegalInformation)
|
|
{
|
|
if (!retrieveLegalInfoXmlFromNca(&ncmStorage, &ncaId, &dec_nca_header, xml_content_info[i].decrypted_nca_keys, &(xml_records[xml_rec_cnt - 1].xml_data), &(xml_records[xml_rec_cnt - 1].xml_size)))
|
|
{
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reencrypt header
|
|
if (!encryptNcaHeader(&dec_nca_header, xml_content_info[i].encrypted_header_mod, NCA_FULL_HEADER_LENGTH))
|
|
{
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!proceed) goto out;
|
|
|
|
if (proceed && !cnmtFound)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to find CNMT NCA!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Update NCA counter just in case we found any delta fragments and excluded them
|
|
titleContentInfoCnt = xml_program_info.nca_cnt;
|
|
|
|
// Fill information for our CNMT XML
|
|
xml_content_info[titleContentInfoCnt - 1].type = titleContentInfos[cnmtNcaIndex].content_type;
|
|
memcpy(xml_content_info[titleContentInfoCnt - 1].nca_id, titleContentInfos[cnmtNcaIndex].content_id.c, SHA256_HASH_SIZE / 2); // Temporary
|
|
convertDataToHexString(titleContentInfos[cnmtNcaIndex].content_id.c, SHA256_HASH_SIZE / 2, xml_content_info[titleContentInfoCnt - 1].nca_id_str, SHA256_HASH_SIZE + 1); // Temporary
|
|
convertNcaSizeToU64(titleContentInfos[cnmtNcaIndex].size, &(xml_content_info[titleContentInfoCnt - 1].size));
|
|
xml_content_info[titleContentInfoCnt - 1].id_offset = titleContentInfos[cnmtNcaIndex].id_offset;
|
|
convertDataToHexString(xml_content_info[titleContentInfoCnt - 1].hash, SHA256_HASH_SIZE, xml_content_info[titleContentInfoCnt - 1].hash_str, (SHA256_HASH_SIZE * 2) + 1); // Temporary
|
|
|
|
memcpy(&ncaId, &(titleContentInfos[cnmtNcaIndex].content_id), sizeof(NcmContentId));
|
|
|
|
// Update CNMT index
|
|
cnmtNcaIndex = (titleContentInfoCnt - 1);
|
|
|
|
cnmtNcaBuf = malloc(xml_content_info[cnmtNcaIndex].size);
|
|
if (!cnmtNcaBuf)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to allocate memory for CNMT NCA data!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if (!readNcaDataByContentId(&ncmStorage, &ncaId, 0, cnmtNcaBuf, xml_content_info[cnmtNcaIndex].size))
|
|
{
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to read CNMT NCA \"%s\"!", __func__, xml_content_info[cnmtNcaIndex].nca_id_str);
|
|
goto out;
|
|
}
|
|
|
|
// Retrieve CNMT NCA data
|
|
if (!retrieveCnmtNcaData(curStorageId, cnmtNcaBuf, &xml_program_info, xml_content_info, cnmtNcaIndex, &ncaCnmtMod, &rights_info)) goto out;
|
|
|
|
// Generate a placeholder CNMT XML. It's length will be used to calculate the final output dump size
|
|
|
|
// Make sure that the output buffer for our CNMT XML is big enough
|
|
cnmtXml = calloc(NSP_XML_BUFFER_SIZE, sizeof(char));
|
|
if (!cnmtXml)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to allocate memory for the CNMT XML!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
generateCnmtXml(&xml_program_info, xml_content_info, cnmtXml);
|
|
|
|
bool includeTikAndCert = (rights_info.retrieved_tik && !tiklessDump);
|
|
|
|
if (includeTikAndCert)
|
|
{
|
|
// Only mess with the ticket data if removeConsoleData is true, if tiklessDump is false and if we're dealing with a personalized ticket (checked in removeConsoleDataFromTicket())
|
|
// Ticket files from Patch titles bundled with gamecards always use common titlekey crypto
|
|
if ((curStorageId == NcmStorageId_SdCard || curStorageId == NcmStorageId_BuiltInUser) && removeConsoleData) removeConsoleDataFromTicket(&rights_info);
|
|
|
|
// Retrieve cert file
|
|
if (!retrieveCertData(rights_info.cert_data, (rights_info.tik_data.titlekey_type == ETICKET_TITLEKEY_PERSONALIZED)))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, strbuf);
|
|
goto out;
|
|
}
|
|
|
|
// File count = NCA count + CNMT XML + tik + cert
|
|
nspPfs0Header.file_cnt = (titleContentInfoCnt + 3);
|
|
|
|
// Calculate PFS0 String Table size
|
|
nspPfs0StrTableSize = (((nspPfs0Header.file_cnt - 4) * NSP_NCA_FILENAME_LENGTH) + (NSP_CNMT_FILENAME_LENGTH * 2) + NSP_TIK_FILENAME_LENGTH + NSP_CERT_FILENAME_LENGTH);
|
|
} else {
|
|
// File count = NCA count + CNMT XML
|
|
nspPfs0Header.file_cnt = (titleContentInfoCnt + 1);
|
|
|
|
// Calculate PFS0 String Table size
|
|
nspPfs0StrTableSize = (((nspPfs0Header.file_cnt - 2) * NSP_NCA_FILENAME_LENGTH) + (NSP_CNMT_FILENAME_LENGTH * 2));
|
|
}
|
|
|
|
// Add our XML records
|
|
if (xml_rec_cnt)
|
|
{
|
|
for(i = 0; i < xml_rec_cnt; i++)
|
|
{
|
|
if (!xml_records[i].xml_data || !xml_records[i].xml_size) continue;
|
|
|
|
nspPfs0Header.file_cnt++;
|
|
u8 type = xml_content_info[xml_records[i].nca_index].type;
|
|
nspPfs0StrTableSize += (type == NcmContentType_Program ? NSP_PROGRAM_XML_FILENAME_LENGTH : (type == NcmContentType_Control ? NSP_NACP_XML_FILENAME_LENGTH : NSP_LEGAL_XML_FILENAME_LENGTH));
|
|
progressCtx.totalSize += xml_records[i].xml_size;
|
|
|
|
// Add icons if we retrieved them
|
|
if (type == NcmContentType_Control && xml_records[i].nacp_icons && xml_records[i].nacp_icon_cnt)
|
|
{
|
|
for(j = 0; j < xml_records[i].nacp_icon_cnt; j++)
|
|
{
|
|
nspPfs0Header.file_cnt++;
|
|
nspPfs0StrTableSize += (u32)(strlen(xml_records[i].nacp_icons[j].filename) + 1);
|
|
progressCtx.totalSize += xml_records[i].nacp_icons[j].icon_size;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Start NSP creation
|
|
nspPfs0EntryTable = calloc(nspPfs0Header.file_cnt, sizeof(pfs0_file_entry));
|
|
if (!nspPfs0EntryTable)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to allocate memory for the PFS0 file entries!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Make sure we have enough space
|
|
nspPfs0StrTable = calloc(nspPfs0StrTableSize * 2, sizeof(char));
|
|
if (!nspPfs0StrTable)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to allocate memory for the PFS0 string table!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Determine our full NSP header size
|
|
fullPfs0HeaderSize = (sizeof(pfs0_header) + ((u64)nspPfs0Header.file_cnt * sizeof(pfs0_file_entry)) + nspPfs0StrTableSize);
|
|
|
|
// Round up our full NSP header size to a 0x10-byte boundary
|
|
if (!(fullPfs0HeaderSize % 0x10)) fullPfs0HeaderSize++; // If it's already rounded, add more padding
|
|
fullPfs0HeaderSize = round_up(fullPfs0HeaderSize, 0x10);
|
|
|
|
// Determine our String Table size
|
|
nspPfs0Header.str_table_size = (fullPfs0HeaderSize - (sizeof(pfs0_header) + ((u64)nspPfs0Header.file_cnt * sizeof(pfs0_file_entry))));
|
|
|
|
// Allocate memory for PFS0 file data pointer array. Exclude all NCAs but the CNMT NCA
|
|
nspPfs0FilePtrs = calloc(nspPfs0Header.file_cnt - (titleContentInfoCnt - 1), sizeof(u8*));
|
|
if (!nspPfs0FilePtrs)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to allocate memory for the PFS0 file data pointer array!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Fill PFS0 entry table
|
|
// PFS0 string table will be filled at a later time
|
|
u64 curFileOffset = 0;
|
|
u32 curFilenameOffset = 0;
|
|
|
|
u64 entrySize = 0;
|
|
u32 entryFilenameSize = 0;
|
|
|
|
u32 entryIdx = 0, ptrIdx = 0;
|
|
|
|
for(i = 0; i <= titleContentInfoCnt; i++, entryIdx++)
|
|
{
|
|
if (i < titleContentInfoCnt)
|
|
{
|
|
// Always reserve the first titleContentInfoCnt entries for our NCAs
|
|
// Only save the CNMT NCA buffer pointer to the PFS0 file data pointer array. We don't have any other pointers to raw NCA data, so we leave the rest untouched
|
|
entrySize = xml_content_info[i].size;
|
|
entryFilenameSize = (i == cnmtNcaIndex ? NSP_CNMT_FILENAME_LENGTH : NSP_NCA_FILENAME_LENGTH);
|
|
if (i == cnmtNcaIndex) nspPfs0FilePtrs[ptrIdx++] = cnmtNcaBuf;
|
|
} else {
|
|
// Reserve the entry right after our NCAs for the CNMT XML
|
|
entrySize = strlen(cnmtXml);
|
|
entryFilenameSize = NSP_CNMT_FILENAME_LENGTH;
|
|
nspPfs0FilePtrs[ptrIdx++] = (u8*)cnmtXml;
|
|
}
|
|
|
|
nspPfs0EntryTable[i].file_size = entrySize;
|
|
nspPfs0EntryTable[i].file_offset = curFileOffset;
|
|
nspPfs0EntryTable[i].filename_offset = curFilenameOffset;
|
|
|
|
curFileOffset += entrySize;
|
|
curFilenameOffset += entryFilenameSize;
|
|
}
|
|
|
|
for(i = 0; i < xml_rec_cnt; i++, entryIdx++)
|
|
{
|
|
u8 type = xml_content_info[xml_records[i].nca_index].type;
|
|
|
|
if (type == NcmContentType_Control && xml_records[i].nacp_icons && xml_records[i].nacp_icon_cnt)
|
|
{
|
|
// Process all icons at once
|
|
for(j = 0; j < xml_records[i].nacp_icon_cnt; j++, entryIdx++)
|
|
{
|
|
entrySize = xml_records[i].nacp_icons[j].icon_size;
|
|
entryFilenameSize = (u32)(strlen(xml_records[i].nacp_icons[j].filename) + 1); // This is the only entry type with variable filename length
|
|
nspPfs0FilePtrs[ptrIdx++] = xml_records[i].nacp_icons[j].icon_data;
|
|
|
|
nspPfs0EntryTable[entryIdx].file_size = entrySize;
|
|
nspPfs0EntryTable[entryIdx].file_offset = curFileOffset;
|
|
nspPfs0EntryTable[entryIdx].filename_offset = curFilenameOffset;
|
|
|
|
curFileOffset += entrySize;
|
|
curFilenameOffset += entryFilenameSize;
|
|
}
|
|
}
|
|
|
|
// XML entry
|
|
entrySize = xml_records[i].xml_size;
|
|
entryFilenameSize = (type == NcmContentType_Program ? NSP_PROGRAM_XML_FILENAME_LENGTH : (type == NcmContentType_Control ? NSP_NACP_XML_FILENAME_LENGTH : NSP_LEGAL_XML_FILENAME_LENGTH));
|
|
nspPfs0FilePtrs[ptrIdx++] = (u8*)xml_records[i].xml_data;
|
|
|
|
nspPfs0EntryTable[entryIdx].file_size = entrySize;
|
|
nspPfs0EntryTable[entryIdx].file_offset = curFileOffset;
|
|
nspPfs0EntryTable[entryIdx].filename_offset = curFilenameOffset;
|
|
|
|
curFileOffset += entrySize;
|
|
curFilenameOffset += entryFilenameSize;
|
|
}
|
|
|
|
if (includeTikAndCert)
|
|
{
|
|
for(i = 0; i < 2; i++, entryIdx++)
|
|
{
|
|
entrySize = (i == 0 ? ETICKET_TIK_FILE_SIZE : ETICKET_CERT_FILE_SIZE);
|
|
entryFilenameSize = (i == 0 ? NSP_TIK_FILENAME_LENGTH : NSP_CERT_FILENAME_LENGTH);
|
|
nspPfs0FilePtrs[ptrIdx++] = (i == 0 ? (u8*)(&(rights_info.tik_data)) : rights_info.cert_data);
|
|
|
|
nspPfs0EntryTable[entryIdx].file_size = entrySize;
|
|
nspPfs0EntryTable[entryIdx].file_offset = curFileOffset;
|
|
nspPfs0EntryTable[entryIdx].filename_offset = curFilenameOffset;
|
|
|
|
curFileOffset += entrySize;
|
|
curFilenameOffset += entryFilenameSize;
|
|
}
|
|
}
|
|
|
|
// Calculate total dump size
|
|
progressCtx.totalSize += fullPfs0HeaderSize;
|
|
for(i = 0; i < titleContentInfoCnt; i++) progressCtx.totalSize += xml_content_info[i].size;
|
|
progressCtx.totalSize += strlen(cnmtXml);
|
|
if (includeTikAndCert) progressCtx.totalSize += (ETICKET_TIK_FILE_SIZE + ETICKET_CERT_FILE_SIZE);
|
|
|
|
convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, MAX_CHARACTERS(progressCtx.totalSizeStr));
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Total NSP dump size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize);
|
|
uiRefreshDisplay();
|
|
breaks += 2;
|
|
|
|
if (!batch)
|
|
{
|
|
if (seqDumpMode)
|
|
{
|
|
// Check if the current offset doesn't exceed the total NSP size
|
|
if (progressCtx.curOffset >= progressCtx.totalSize)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid NSP offset in the sequential dump reference file!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Check if the NCA count is valid
|
|
// The CNMT NCA is excluded from the hash list
|
|
if (seqNspCtx.ncaCount != (titleContentInfoCnt - 1))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: NCA count mismatch in the sequential dump reference file! (%u != %u)", __func__, seqNspCtx.ncaCount, titleContentInfoCnt - 1);
|
|
goto out;
|
|
}
|
|
|
|
// Check if the Program NCA mod count is valid
|
|
if (seqNspCtx.programNcaModCount != ncaProgramModCnt)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: Program NCA mod count mismatch in the sequential dump reference file! (%u != %u)", __func__, seqNspCtx.programNcaModCount, ncaProgramModCnt);
|
|
goto out;
|
|
}
|
|
|
|
// Check if the PFS0 file count is valid
|
|
if (seqNspCtx.pfs0FileCount != nspPfs0Header.file_cnt)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: PFS0 file count mismatch in the sequential dump reference file! (%u != %u)", __func__, seqNspCtx.pfs0FileCount, nspPfs0Header.file_cnt);
|
|
goto out;
|
|
}
|
|
|
|
// Check if the current PFS0 file index is valid
|
|
if (seqNspCtx.fileIndex >= nspPfs0Header.file_cnt)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid PFS0 file index in the sequential dump reference file!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Check if we're really dealing with a title with a missing ticket if preInstall == true
|
|
if (seqNspCtx.preInstall && !rights_info.missing_tik)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid title preinstall status in the sequential dump reference file!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Check if the current overall offset is aligned to SPLIT_FILE_SEQUENTIAL_SIZE
|
|
u64 curNspOffset = fullPfs0HeaderSize;
|
|
for(i = 0; i < seqNspCtx.fileIndex; i++) curNspOffset += nspPfs0EntryTable[i].file_size;
|
|
curNspOffset += seqNspCtx.fileOffset;
|
|
|
|
if (curNspOffset != progressCtx.curOffset)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: overall NSP dump offset isn't aligned to 0x%08X in the sequential dump reference file!", __func__, (u32)SPLIT_FILE_SEQUENTIAL_SIZE);
|
|
goto out;
|
|
}
|
|
|
|
// Check if there's enough free space to continue the sequential dump process
|
|
u64 restSize = (progressCtx.totalSize - curNspOffset);
|
|
if (progressCtx.totalSize > freeSpace && ((restSize > SPLIT_FILE_SEQUENTIAL_SIZE && freeSpace < SPLIT_FILE_SEQUENTIAL_SIZE) || (restSize <= SPLIT_FILE_SEQUENTIAL_SIZE && freeSpace < restSize)))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Now check if the current PFS0 file entry offset is correct
|
|
if (seqNspCtx.fileOffset >= nspPfs0EntryTable[seqNspCtx.fileIndex].file_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid offset for current PFS0 file entry in the sequential dump reference file!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Copy previously calculated NCA IDs and hashes
|
|
for(i = 0; i < seqNspCtx.fileIndex; i++)
|
|
{
|
|
// Exit loop if we reach the CNMT NCA
|
|
// Its ID/hash calculation is always handled by patchCnmtNca()
|
|
if (i >= (titleContentInfoCnt - 1)) break;
|
|
|
|
// Fill information for our CNMT XML
|
|
memcpy(xml_content_info[i].nca_id, seqDumpNcaHashes + (i * SHA256_HASH_SIZE), SHA256_HASH_SIZE / 2);
|
|
convertDataToHexString(xml_content_info[i].nca_id, SHA256_HASH_SIZE / 2, xml_content_info[i].nca_id_str, SHA256_HASH_SIZE + 1);
|
|
memcpy(xml_content_info[i].hash, seqDumpNcaHashes + (i * SHA256_HASH_SIZE), SHA256_HASH_SIZE);
|
|
convertDataToHexString(xml_content_info[i].hash, SHA256_HASH_SIZE, xml_content_info[i].hash_str, (SHA256_HASH_SIZE * 2) + 1);
|
|
}
|
|
|
|
// Copy the NCA SHA-256 context data, but only if we're not dealing with the CNMT NCA
|
|
if (seqNspCtx.fileIndex < (titleContentInfoCnt - 1)) memcpy(&nca_hash_ctx, &(seqNspCtx.hashCtx), sizeof(Sha256Context));
|
|
|
|
// Restore the modified Program NCA headers
|
|
// The NPDM signature from the NCA headers is generated using cryptographically secure random numbers, so the modified header is stored during the first sequential dump session
|
|
// If needed, it must be restored in later sessions
|
|
for(i = 0; i < ncaProgramModCnt; i++)
|
|
{
|
|
read_res = fread(xml_content_info[ncaProgramMod[i].nca_index].encrypted_header_mod, 1, NCA_FULL_HEADER_LENGTH, seqDumpFile);
|
|
if (read_res != NCA_FULL_HEADER_LENGTH)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to read %lu bytes chunk from the sequential dump reference file! (read %lu bytes)", __func__, NCA_FULL_HEADER_LENGTH, read_res);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
rewind(seqDumpFile);
|
|
|
|
// Inform that we are resuming an already started sequential dump operation
|
|
if (curStorageId == NcmStorageId_GameCard)
|
|
{
|
|
if (selectedNspDumpType == DUMP_APP_NSP)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Resuming previous sequential dump operation. Configuration parameters overrided.");
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Change NPDM RSA key/sig in Program NCA: %s.", (npdmAcidRsaPatch ? "Yes" : "No"));
|
|
} else
|
|
if (selectedNspDumpType == DUMP_PATCH_NSP)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Resuming previous sequential dump operation. Configuration parameters overrided.");
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Generate ticket-less dump: %s | Change NPDM RSA key/sig in Program NCA: %s.", (tiklessDump ? "Yes" : "No"), (npdmAcidRsaPatch ? "Yes" : "No"));
|
|
} else
|
|
if (selectedNspDumpType == DUMP_ADDON_NSP)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Resuming previous sequential dump operation.");
|
|
}
|
|
} else {
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Resuming previous sequential dump operation. Configuration parameters overrided.");
|
|
breaks++;
|
|
|
|
if (selectedNspDumpType == DUMP_APP_NSP || selectedNspDumpType == DUMP_PATCH_NSP)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Remove console specific data: %s | Generate ticket-less dump: %s | Change NPDM RSA key/sig in Program NCA: %s.", (removeConsoleData ? "Yes" : "No"), (tiklessDump ? "Yes" : "No"), (npdmAcidRsaPatch ? "Yes" : "No"));
|
|
} else
|
|
if (selectedNspDumpType == DUMP_ADDON_NSP)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Remove console specific data: %s | Generate ticket-less dump: %s.", (removeConsoleData ? "Yes" : "No"), (tiklessDump ? "Yes" : "No"));
|
|
}
|
|
}
|
|
|
|
breaks++;
|
|
} else {
|
|
if (progressCtx.totalSize > freeSpace)
|
|
{
|
|
// Check if we have enough free space
|
|
// The CNMT NCA is excluded from the hash list
|
|
seqDumpFileSize = (sizeof(sequentialNspCtx) + ((titleContentInfoCnt - 1) * SHA256_HASH_SIZE) + (ncaProgramModCnt * NCA_FULL_HEADER_LENGTH));
|
|
if (freeSpace < (SPLIT_FILE_SEQUENTIAL_SIZE + seqDumpFileSize))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Ask the user if they want to use the sequential dump mode
|
|
int cur_breaks = breaks;
|
|
|
|
if (!yesNoPrompt("There's not enough space available to generate a whole dump in this session. Do you want to use sequential dumping?\nIn this mode, the selected content will be dumped in more than one session.\nYou'll have to transfer the generated part files to a PC before continuing the process in the next session."))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
goto out;
|
|
}
|
|
|
|
// Remove the prompt from the screen
|
|
breaks = cur_breaks;
|
|
uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), BG_COLOR_RGB);
|
|
uiRefreshDisplay();
|
|
|
|
// Modify config parameters
|
|
isFat32 = true;
|
|
partSize = SPLIT_FILE_SEQUENTIAL_SIZE;
|
|
seqDumpMode = true;
|
|
|
|
// Fill information in our sequential context
|
|
seqNspCtx.storageId = curStorageId;
|
|
seqNspCtx.removeConsoleData = removeConsoleData;
|
|
seqNspCtx.tiklessDump = tiklessDump;
|
|
seqNspCtx.npdmAcidRsaPatch = npdmAcidRsaPatch;
|
|
seqNspCtx.preInstall = preInstall;
|
|
seqNspCtx.pfs0FileCount = nspPfs0Header.file_cnt;
|
|
seqNspCtx.ncaCount = (titleContentInfoCnt - 1); // Exclude the CNMT NCA from the hash list
|
|
seqNspCtx.programNcaModCount = ncaProgramModCnt;
|
|
|
|
// Allocate memory for the NCA hashes
|
|
seqDumpNcaHashes = calloc(1, (titleContentInfoCnt - 1) * SHA256_HASH_SIZE);
|
|
if (!seqDumpNcaHashes)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to allocate memory for NCA hashes from the sequential dump reference file!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Create sequential reference file and keep the handle to it opened
|
|
seqDumpFile = fopen(seqDumpFilename, "wb+");
|
|
if (!seqDumpFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to create sequential dump reference file! (\"%s\")", __func__, seqDumpFilename);
|
|
goto out;
|
|
}
|
|
|
|
// Write the sequential dump struct
|
|
write_res = fwrite(&seqNspCtx, 1, sizeof(sequentialNspCtx), seqDumpFile);
|
|
if (write_res != sizeof(sequentialNspCtx))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk to the sequential dump reference file! (wrote %lu bytes)", __func__, sizeof(sequentialNspCtx), write_res);
|
|
seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
|
|
// Write the NCA hashes block
|
|
write_res = fwrite(seqDumpNcaHashes, 1, (titleContentInfoCnt - 1) * SHA256_HASH_SIZE, seqDumpFile);
|
|
if (write_res != ((titleContentInfoCnt - 1) * SHA256_HASH_SIZE))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk to the sequential dump reference file! (wrote %lu bytes)", __func__, (titleContentInfoCnt - 1) * SHA256_HASH_SIZE, write_res);
|
|
seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
|
|
// Write the modified Program NCA headers
|
|
// The NPDM signature from the NCA headers is generated using cryptographically secure random numbers, so we must store the modified header during the first sequential dump session
|
|
for(i = 0; i < ncaProgramModCnt; i++)
|
|
{
|
|
write_res = fwrite(xml_content_info[ncaProgramMod[i].nca_index].encrypted_header_mod, 1, NCA_FULL_HEADER_LENGTH, seqDumpFile);
|
|
if (write_res != NCA_FULL_HEADER_LENGTH)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk to the sequential dump reference file! (wrote %lu bytes)", __func__, NCA_FULL_HEADER_LENGTH, write_res);
|
|
seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
rewind(seqDumpFile);
|
|
|
|
// Update free space
|
|
freeSpace -= seqDumpFileSize;
|
|
}
|
|
}
|
|
} else {
|
|
if (progressCtx.totalSize > freeSpace)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
if (seqDumpMode)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.nsp.%02u", NSP_DUMP_PATH, dumpName, splitIndex);
|
|
} else {
|
|
// Temporary, we'll use this to check if the dump already exists (it should have the archive bit set if so)
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.nsp", NSP_DUMP_PATH, dumpName);
|
|
|
|
// Check if the dump already exists
|
|
if (!batch && 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(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
removeFile = false;
|
|
goto out;
|
|
} else {
|
|
// Remove the prompt from the screen
|
|
breaks = cur_breaks;
|
|
uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), 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
|
|
remove(dumpPath);
|
|
fsdevDeleteDirectoryRecursively(dumpPath);
|
|
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32)
|
|
{
|
|
mkdir(dumpPath, 0744);
|
|
|
|
sprintf(tmp_idx, "/%02u", splitIndex);
|
|
strcat(dumpPath, tmp_idx);
|
|
}
|
|
}
|
|
|
|
outFile = fopen(dumpPath, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open output file \"%s\"!", __func__, dumpPath);
|
|
goto out;
|
|
}
|
|
|
|
// Start dump process
|
|
if (!batch) dumpStartMsg();
|
|
appletModeOperationWarning();
|
|
uiRefreshDisplay();
|
|
|
|
if (!batch)
|
|
{
|
|
breaks++;
|
|
changeHomeButtonBlockStatus(true);
|
|
}
|
|
|
|
if (seqDumpMode)
|
|
{
|
|
// Skip the PFS0 header in the first part file
|
|
// It will be saved to an additional ".nsp.hdr" file
|
|
if (!seqNspCtx.partNumber) progressCtx.curOffset = seqDumpSessionOffset = fullPfs0HeaderSize;
|
|
} else {
|
|
// Write placeholder zeroes
|
|
write_res = fwrite(dumpBuf, 1, fullPfs0HeaderSize, outFile);
|
|
if (write_res != fullPfs0HeaderSize)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes placeholder data to file offset 0x%016lX! (wrote %lu bytes)", __func__, fullPfs0HeaderSize, (u64)0, write_res);
|
|
goto out;
|
|
}
|
|
|
|
// Advance our current offset
|
|
progressCtx.curOffset = fullPfs0HeaderSize;
|
|
}
|
|
|
|
progressCtx.line_offset = (breaks + 4);
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start));
|
|
|
|
dumping = true;
|
|
|
|
u32 startFileIndex = (seqDumpMode ? seqNspCtx.fileIndex : 0);
|
|
u64 startFileOffset;
|
|
|
|
// Write all PFS0 entries
|
|
for(i = startFileIndex; i < nspPfs0Header.file_cnt; i++, startFileIndex++)
|
|
{
|
|
char *entryFilename = NULL;
|
|
|
|
n = DUMP_BUFFER_SIZE;
|
|
|
|
startFileOffset = ((seqDumpMode && i == seqNspCtx.fileIndex) ? seqNspCtx.fileOffset : 0);
|
|
|
|
int programModIdx = -1;
|
|
|
|
// Check if we're dealing with a NCA
|
|
if (i < titleContentInfoCnt)
|
|
{
|
|
// Check if we're not dealing with the CNMT NCA
|
|
if (i < (titleContentInfoCnt - 1))
|
|
{
|
|
// Copy NCA ID
|
|
memcpy(ncaId.c, xml_content_info[i].nca_id, SHA256_HASH_SIZE / 2);
|
|
|
|
// Reset SHA-256 context if necessary
|
|
if (!seqDumpMode || (seqDumpMode && i != seqNspCtx.fileIndex)) sha256ContextCreate(&nca_hash_ctx);
|
|
|
|
// Retrieve Program NCA mod data index
|
|
if (xml_content_info[i].type == NcmContentType_Program && ncaProgramModCnt > 0)
|
|
{
|
|
for(j = 0; j < ncaProgramModCnt; j++)
|
|
{
|
|
if (ncaProgramMod[j].nca_index == i)
|
|
{
|
|
programModIdx = (int)j;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Patch CNMT NCA
|
|
breaks = (progressCtx.line_offset + 2);
|
|
|
|
proceed = patchCnmtNca(cnmtNcaBuf, xml_content_info[cnmtNcaIndex].size, &xml_program_info, xml_content_info, &ncaCnmtMod);
|
|
if (!proceed)
|
|
{
|
|
dumping = false;
|
|
break;
|
|
}
|
|
|
|
breaks = (progressCtx.line_offset - 4);
|
|
|
|
// Generate proper CNMT XML
|
|
generateCnmtXml(&xml_program_info, xml_content_info, cnmtXml);
|
|
|
|
// Fill PFS0 string table
|
|
// This is done here because we'll need to display filenames for the rest of the PFS0 entries starting with the next loop iteration
|
|
entryIdx = 0;
|
|
|
|
for(j = 0; j <= titleContentInfoCnt; j++, entryIdx++)
|
|
{
|
|
char *curFilename = (nspPfs0StrTable + nspPfs0EntryTable[entryIdx].filename_offset);
|
|
|
|
if (j < titleContentInfoCnt)
|
|
{
|
|
sprintf(curFilename, "%s.%s", xml_content_info[j].nca_id_str, (j == cnmtNcaIndex ? "cnmt.nca" : "nca"));
|
|
} else
|
|
if (j == titleContentInfoCnt)
|
|
{
|
|
sprintf(curFilename, "%s.cnmt.xml", xml_content_info[cnmtNcaIndex].nca_id_str);
|
|
}
|
|
}
|
|
|
|
for(j = 0; j < xml_rec_cnt; j++, entryIdx++)
|
|
{
|
|
u8 type = xml_content_info[xml_records[j].nca_index].type;
|
|
|
|
if (type == NcmContentType_Control && xml_records[j].nacp_icons && xml_records[j].nacp_icon_cnt)
|
|
{
|
|
// Process all icons at once
|
|
for(u32 k = 0; k < xml_records[j].nacp_icon_cnt; k++, entryIdx++)
|
|
{
|
|
char *curFilename = (nspPfs0StrTable + nspPfs0EntryTable[entryIdx].filename_offset);
|
|
sprintf(curFilename, "%s%s", xml_content_info[xml_records[j].nca_index].nca_id_str, strchr(xml_records[j].nacp_icons[k].filename, '.'));
|
|
}
|
|
}
|
|
|
|
char *curFilename = (nspPfs0StrTable + nspPfs0EntryTable[entryIdx].filename_offset);
|
|
sprintf(curFilename, "%s.%s.xml", xml_content_info[xml_records[j].nca_index].nca_id_str, (type == NcmContentType_Program ? "programinfo" : (type == NcmContentType_Control ? "nacp" : "legalinfo")));
|
|
}
|
|
|
|
if (includeTikAndCert)
|
|
{
|
|
for(j = 0; j < 2; j++, entryIdx++)
|
|
{
|
|
char *curFilename = (nspPfs0StrTable + nspPfs0EntryTable[entryIdx].filename_offset);
|
|
sprintf(curFilename, (j == 0 ? rights_info.tik_filename : rights_info.cert_filename));
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Copy current filename
|
|
entryFilename = (nspPfs0StrTable + nspPfs0EntryTable[i].filename_offset);
|
|
}
|
|
|
|
for(fileOffset = startFileOffset; fileOffset < nspPfs0EntryTable[i].file_size; fileOffset += n, progressCtx.curOffset += n, seqDumpSessionOffset += n)
|
|
{
|
|
if (seqDumpMode && seqDumpFinish)
|
|
{
|
|
ret = 0;
|
|
break;
|
|
}
|
|
|
|
uiFill(0, ((progressCtx.line_offset - 4) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 4, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 4), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(dumpPath, '/' ) + 1);
|
|
|
|
if (i < titleContentInfoCnt)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Dumping NCA \"%s\" (%s)...", xml_content_info[i].nca_id_str, getContentType(xml_content_info[i].type));
|
|
} else {
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Writing \"%s\"...", entryFilename);
|
|
}
|
|
|
|
if (n > (nspPfs0EntryTable[i].file_size - fileOffset)) n = (nspPfs0EntryTable[i].file_size - fileOffset);
|
|
|
|
// Check if the next read chunk will exceed the size of the current part file
|
|
if (seqDumpMode && (seqDumpSessionOffset + n) >= (((splitIndex - seqNspCtx.partNumber) + 1) * partSize))
|
|
{
|
|
u64 new_file_chunk_size = ((seqDumpSessionOffset + n) - (((splitIndex - seqNspCtx.partNumber) + 1) * partSize));
|
|
u64 old_file_chunk_size = (n - new_file_chunk_size);
|
|
|
|
u64 remainderDumpSize = (progressCtx.totalSize - (progressCtx.curOffset + old_file_chunk_size));
|
|
u64 remainderFreeSize = (freeSpace - (seqDumpSessionOffset + old_file_chunk_size));
|
|
|
|
// Check if we have enough space for the next part
|
|
// If so, set the chunk size to old_file_chunk_size
|
|
if ((remainderDumpSize <= partSize && remainderDumpSize > remainderFreeSize) || (remainderDumpSize > partSize && partSize > remainderFreeSize))
|
|
{
|
|
n = old_file_chunk_size;
|
|
seqDumpFinish = true;
|
|
}
|
|
}
|
|
|
|
if (i < (titleContentInfoCnt - 1))
|
|
{
|
|
breaks = (progressCtx.line_offset + 2);
|
|
|
|
proceed = readNcaDataByContentId(&ncmStorage, &ncaId, fileOffset, dumpBuf, n);
|
|
if (!proceed)
|
|
{
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to read %lu bytes chunk at offset 0x%016lX from NCA \"%s\"!", __func__, n, fileOffset, xml_content_info[i].nca_id_str);
|
|
dumping = false;
|
|
break;
|
|
}
|
|
|
|
breaks = (progressCtx.line_offset - 4);
|
|
|
|
// Replace NCA header with our modified one
|
|
if (fileOffset < NCA_FULL_HEADER_LENGTH)
|
|
{
|
|
u64 write_size = (NCA_FULL_HEADER_LENGTH - fileOffset);
|
|
if (write_size > n) write_size = n;
|
|
|
|
memcpy(dumpBuf, xml_content_info[i].encrypted_header_mod + fileOffset, write_size);
|
|
}
|
|
|
|
// Replace modified Program NCA data blocks
|
|
if (programModIdx != -1)
|
|
{
|
|
u64 internal_block_offset;
|
|
u64 internal_block_chunk_size;
|
|
|
|
u64 buffer_offset;
|
|
u64 buffer_chunk_size;
|
|
|
|
if ((fileOffset + n) > ncaProgramMod[programModIdx].hash_table_offset && (ncaProgramMod[programModIdx].hash_table_offset + ncaProgramMod[programModIdx].hash_table_size) > fileOffset)
|
|
{
|
|
internal_block_offset = (fileOffset > ncaProgramMod[programModIdx].hash_table_offset ? (fileOffset - ncaProgramMod[programModIdx].hash_table_offset) : 0);
|
|
internal_block_chunk_size = (ncaProgramMod[programModIdx].hash_table_size - internal_block_offset);
|
|
|
|
buffer_offset = (fileOffset > ncaProgramMod[programModIdx].hash_table_offset ? 0 : (ncaProgramMod[programModIdx].hash_table_offset - fileOffset));
|
|
buffer_chunk_size = ((n - buffer_offset) > internal_block_chunk_size ? internal_block_chunk_size : (n - buffer_offset));
|
|
|
|
memcpy(dumpBuf + buffer_offset, ncaProgramMod[programModIdx].hash_table + internal_block_offset, buffer_chunk_size);
|
|
}
|
|
|
|
if ((fileOffset + n) > ncaProgramMod[programModIdx].block_offset[0] && (ncaProgramMod[programModIdx].block_offset[0] + ncaProgramMod[programModIdx].block_size[0]) > fileOffset)
|
|
{
|
|
internal_block_offset = (fileOffset > ncaProgramMod[programModIdx].block_offset[0] ? (fileOffset - ncaProgramMod[programModIdx].block_offset[0]) : 0);
|
|
internal_block_chunk_size = (ncaProgramMod[programModIdx].block_size[0] - internal_block_offset);
|
|
|
|
buffer_offset = (fileOffset > ncaProgramMod[programModIdx].block_offset[0] ? 0 : (ncaProgramMod[programModIdx].block_offset[0] - fileOffset));
|
|
buffer_chunk_size = ((n - buffer_offset) > internal_block_chunk_size ? internal_block_chunk_size : (n - buffer_offset));
|
|
|
|
memcpy(dumpBuf + buffer_offset, ncaProgramMod[programModIdx].block_data[0] + internal_block_offset, buffer_chunk_size);
|
|
}
|
|
|
|
if (ncaProgramMod[programModIdx].block_mod_cnt == 2 && (fileOffset + n) > ncaProgramMod[programModIdx].block_offset[1] && (ncaProgramMod[programModIdx].block_offset[1] + ncaProgramMod[programModIdx].block_size[1]) > fileOffset)
|
|
{
|
|
internal_block_offset = (fileOffset > ncaProgramMod[programModIdx].block_offset[1] ? (fileOffset - ncaProgramMod[programModIdx].block_offset[1]) : 0);
|
|
internal_block_chunk_size = (ncaProgramMod[programModIdx].block_size[1] - internal_block_offset);
|
|
|
|
buffer_offset = (fileOffset > ncaProgramMod[programModIdx].block_offset[1] ? 0 : (ncaProgramMod[programModIdx].block_offset[1] - fileOffset));
|
|
buffer_chunk_size = ((n - buffer_offset) > internal_block_chunk_size ? internal_block_chunk_size : (n - buffer_offset));
|
|
|
|
memcpy(dumpBuf + buffer_offset, ncaProgramMod[programModIdx].block_data[1] + internal_block_offset, buffer_chunk_size);
|
|
}
|
|
}
|
|
|
|
// Update SHA-256 calculation
|
|
sha256ContextUpdate(&nca_hash_ctx, dumpBuf, n);
|
|
} else {
|
|
// Copy data using pointer array
|
|
u32 ptrIdx = (i - (titleContentInfoCnt - 1));
|
|
memcpy(dumpBuf, nspPfs0FilePtrs[ptrIdx] + fileOffset, n);
|
|
}
|
|
|
|
if ((seqDumpMode || (!seqDumpMode && progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32)) && (progressCtx.curOffset + n) >= ((splitIndex + 1) * partSize))
|
|
{
|
|
u64 new_file_chunk_size = ((progressCtx.curOffset + n) - ((splitIndex + 1) * partSize));
|
|
u64 old_file_chunk_size = (n - new_file_chunk_size);
|
|
|
|
if (old_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf, 1, old_file_chunk_size, outFile);
|
|
if (write_res != old_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, old_file_chunk_size, progressCtx.curOffset, splitIndex, write_res);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
fclose(outFile);
|
|
outFile = NULL;
|
|
|
|
if (((seqDumpMode && !seqDumpFinish) || !seqDumpMode) && (new_file_chunk_size > 0 || (progressCtx.curOffset + n) < progressCtx.totalSize))
|
|
{
|
|
splitIndex++;
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.nsp%c%02u", NSP_DUMP_PATH, dumpName, (seqDumpMode ? '.' : '/'), splitIndex);
|
|
|
|
outFile = fopen(dumpPath, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to open output file for part #%u!", __func__, splitIndex);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
if (new_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf + old_file_chunk_size, 1, new_file_chunk_size, outFile);
|
|
if (write_res != new_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, write_res);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
write_res = fwrite(dumpBuf, 1, n, outFile);
|
|
if (write_res != n)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", __func__, n, progressCtx.curOffset, write_res);
|
|
|
|
if (!seqDumpMode && (progressCtx.curOffset + n) > FAT32_FILESIZE_LIMIT)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 4), FONT_COLOR_RGB, "You're probably using a FAT32 partition. Make sure to enable the \"Split output dump\" option.");
|
|
fat32_error = true;
|
|
}
|
|
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (seqDumpMode) progressCtx.seqDumpCurOffset = seqDumpSessionOffset;
|
|
printProgressBar(&progressCtx, true, n);
|
|
|
|
if ((progressCtx.curOffset + n) < progressCtx.totalSize && cancelProcessCheck(&progressCtx))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
ret = -2;
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!proceed || ret >= 0) break;
|
|
|
|
// Support empty files
|
|
if (!nspPfs0EntryTable[i].file_size)
|
|
{
|
|
uiFill(0, ((progressCtx.line_offset - 4) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 4, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 4), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(dumpPath, '/' ) + 1);
|
|
|
|
if (i < titleContentInfoCnt)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Dumping NCA \"%s\" (%s)...", xml_content_info[i].nca_id_str, getContentType(xml_content_info[i].type));
|
|
} else {
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Writing \"%s\"...", entryFilename);
|
|
}
|
|
|
|
printProgressBar(&progressCtx, false, 0);
|
|
}
|
|
|
|
// Check if we're not dealing with the CNMT NCA
|
|
if (i < (titleContentInfoCnt - 1))
|
|
{
|
|
// Update content info
|
|
sha256ContextGetHash(&nca_hash_ctx, xml_content_info[i].hash);
|
|
convertDataToHexString(xml_content_info[i].hash, SHA256_HASH_SIZE, xml_content_info[i].hash_str, (SHA256_HASH_SIZE * 2) + 1);
|
|
memcpy(xml_content_info[i].nca_id, xml_content_info[i].hash, SHA256_HASH_SIZE / 2);
|
|
convertDataToHexString(xml_content_info[i].nca_id, SHA256_HASH_SIZE / 2, xml_content_info[i].nca_id_str, SHA256_HASH_SIZE + 1);
|
|
|
|
// If we're doing a sequential dump and we just finished dumping a NCA, copy its calculated hash
|
|
if (seqDumpMode) memcpy(seqDumpNcaHashes + (i * SHA256_HASH_SIZE), xml_content_info[i].hash, SHA256_HASH_SIZE);
|
|
}
|
|
}
|
|
|
|
if (!proceed || ret >= 0)
|
|
{
|
|
if (!proceed)
|
|
{
|
|
setProgressBarError(&progressCtx);
|
|
if (seqDumpMode) seqDumpFileRemove = true;
|
|
}
|
|
|
|
goto out;
|
|
}
|
|
|
|
uiFill(0, ((progressCtx.line_offset - 4) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 4, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 4), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(dumpPath, '/' ) + 1);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Writing PFS0 header...");
|
|
|
|
uiRefreshDisplay();
|
|
|
|
// Write our full PFS0 header
|
|
memcpy(dumpBuf, &nspPfs0Header, sizeof(pfs0_header));
|
|
memcpy(dumpBuf + sizeof(pfs0_header), nspPfs0EntryTable, (u64)nspPfs0Header.file_cnt * sizeof(pfs0_file_entry));
|
|
memcpy(dumpBuf + sizeof(pfs0_header) + ((u64)nspPfs0Header.file_cnt * sizeof(pfs0_file_entry)), nspPfs0StrTable, nspPfs0Header.str_table_size);
|
|
|
|
if (seqDumpMode)
|
|
{
|
|
// Just in case
|
|
remove(pfs0HeaderFilename);
|
|
|
|
// Check if we have enough space for the header file
|
|
u64 curFreeSpace = (freeSpace - seqDumpSessionOffset);
|
|
if (!seqNspCtx.partNumber) curFreeSpace += fullPfs0HeaderSize; // The PFS0 header size is skipped during the first sequential dump session
|
|
|
|
if (curFreeSpace < fullPfs0HeaderSize)
|
|
{
|
|
// Finish current sequential dump session
|
|
seqDumpFinish = true;
|
|
ret = 0;
|
|
goto out;
|
|
}
|
|
|
|
pfs0HeaderFile = fopen(pfs0HeaderFilename, "wb");
|
|
if (!pfs0HeaderFile)
|
|
{
|
|
setProgressBarError(&progressCtx);
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to create PFS0 header file!", __func__);
|
|
seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
|
|
write_res = fwrite(dumpBuf, 1, fullPfs0HeaderSize, pfs0HeaderFile);
|
|
fclose(pfs0HeaderFile);
|
|
|
|
if (write_res != fullPfs0HeaderSize)
|
|
{
|
|
setProgressBarError(&progressCtx);
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes PFS0 header file! (wrote %lu bytes)", __func__, fullPfs0HeaderSize, write_res);
|
|
remove(pfs0HeaderFilename);
|
|
seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
|
|
// Update free space
|
|
freeSpace -= fullPfs0HeaderSize;
|
|
} else {
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32)
|
|
{
|
|
if (outFile)
|
|
{
|
|
fclose(outFile);
|
|
outFile = NULL;
|
|
}
|
|
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.nsp/%02u", NSP_DUMP_PATH, dumpName, 0);
|
|
|
|
outFile = fopen(dumpPath, "rb+");
|
|
if (!outFile)
|
|
{
|
|
setProgressBarError(&progressCtx);
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to re-open output file for part #0!", __func__);
|
|
goto out;
|
|
}
|
|
} else {
|
|
rewind(outFile);
|
|
}
|
|
|
|
write_res = fwrite(dumpBuf, 1, fullPfs0HeaderSize, outFile);
|
|
if (write_res != fullPfs0HeaderSize)
|
|
{
|
|
setProgressBarError(&progressCtx);
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes PFS0 header to file offset 0x%016lX! (wrote %lu bytes)", __func__, fullPfs0HeaderSize, (u64)0, write_res);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
dumping = false;
|
|
|
|
breaks = (progressCtx.line_offset + 2);
|
|
|
|
if (progressCtx.curOffset >= progressCtx.totalSize || (seqDumpMode && seqDumpFinish)) ret = 0;
|
|
|
|
if (ret < 0)
|
|
{
|
|
setProgressBarError(&progressCtx);
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: underdump error! Wrote %lu bytes, expected %lu bytes.", __func__, progressCtx.curOffset, progressCtx.totalSize);
|
|
if (seqDumpMode) seqDumpFileRemove = true;
|
|
goto out;
|
|
}
|
|
|
|
// Set archive bit (only for FAT32)
|
|
if (!seqDumpMode && progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.nsp", NSP_DUMP_PATH, dumpName);
|
|
result = fsdevSetConcatenationFileAttribute(dumpPath);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Warning: failed to set archive bit on output directory! (0x%08X)", result);
|
|
breaks += 2;
|
|
}
|
|
}
|
|
|
|
out:
|
|
if (outFile) fclose(outFile);
|
|
|
|
if (ret >= 0)
|
|
{
|
|
if (seqDumpMode)
|
|
{
|
|
if (seqDumpFinish)
|
|
{
|
|
// Update line count
|
|
breaks = (progressCtx.line_offset + 2);
|
|
|
|
// Update the sequence reference file
|
|
seqNspCtx.partNumber = (splitIndex + 1);
|
|
seqNspCtx.fileIndex = startFileIndex;
|
|
seqNspCtx.fileOffset = fileOffset;
|
|
|
|
// Copy the SHA-256 context data, but only if we're not dealing with the CNMT NCA
|
|
// NCA ID/hash for the CNMT NCA is handled in patchCnmtNca()
|
|
if (seqNspCtx.fileIndex < titleContentInfoCnt && seqNspCtx.fileIndex != cnmtNcaIndex)
|
|
{
|
|
memcpy(&(seqNspCtx.hashCtx), &nca_hash_ctx, sizeof(Sha256Context));
|
|
} else {
|
|
memset(&(seqNspCtx.hashCtx), 0, sizeof(Sha256Context));
|
|
}
|
|
|
|
// Write the struct data
|
|
write_res = fwrite(&seqNspCtx, 1, sizeof(sequentialNspCtx), seqDumpFile);
|
|
if (write_res == sizeof(sequentialNspCtx))
|
|
{
|
|
// Write the NCA hashes
|
|
write_res = fwrite(seqDumpNcaHashes, 1, seqNspCtx.ncaCount * SHA256_HASH_SIZE, seqDumpFile);
|
|
if (write_res != (seqNspCtx.ncaCount * SHA256_HASH_SIZE))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk to the sequential dump reference file! (wrote %lu bytes)", __func__, seqNspCtx.ncaCount * SHA256_HASH_SIZE, write_res);
|
|
ret = -1;
|
|
seqDumpFileRemove = true;
|
|
}
|
|
} else {
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk to the sequential dump reference file! (wrote %lu bytes)", __func__, sizeof(sequentialNspCtx), write_res);
|
|
ret = -1;
|
|
seqDumpFileRemove = true;
|
|
}
|
|
} else {
|
|
// Mark the file for deletion
|
|
seqDumpFileRemove = true;
|
|
}
|
|
}
|
|
|
|
if (ret >= 0 && !batch)
|
|
{
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now));
|
|
progressCtx.now -= progressCtx.start;
|
|
|
|
if (!seqDumpMode || (seqDumpMode && !seqDumpFinish))
|
|
{
|
|
progressCtx.progress = 100;
|
|
progressCtx.remainingTime = 0;
|
|
}
|
|
|
|
printProgressBar(&progressCtx, false, 0);
|
|
|
|
formatETAString(progressCtx.now, progressCtx.etaInfo, MAX_CHARACTERS(progressCtx.etaInfo));
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Process successfully completed after %s!", progressCtx.etaInfo);
|
|
uiRefreshDisplay();
|
|
|
|
// Only perform the checksum lookup if we have finished the dump process
|
|
if (useNoIntroLookup && (!seqDumpMode || (seqDumpMode && !seqDumpFinish)))
|
|
{
|
|
if (curStorageId != NcmStorageId_GameCard && !tiklessDump)
|
|
{
|
|
// Calculate CRC32 checksum for the CNMT NCA
|
|
crc32(cnmtNcaBuf, xml_content_info[cnmtNcaIndex].size, &crc);
|
|
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "CNMT NCA CRC32 checksum: %08X.", crc);
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
// Perform checksum lookup
|
|
noIntroDumpCheck(true, crc);
|
|
} else {
|
|
if (curStorageId != NcmStorageId_GameCard && tiklessDump)
|
|
{
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Dump verification disabled (not compatible with NSP dumps with modified NCAs).");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (seqDumpMode)
|
|
{
|
|
breaks += 2;
|
|
|
|
if (seqDumpFinish)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Please remember to exit the application and transfer the generated part file(s) to a PC before continuing in the next session!");
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Do NOT move the \"%s\" file!", strrchr(seqDumpFilename, '/' ) + 1);
|
|
}
|
|
|
|
if (checkIfFileExists(pfs0HeaderFilename))
|
|
{
|
|
if (seqDumpFinish) breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "The \"%s\" file contains the PFS0 header.\nUse it as the first file when concatenating all parts!", strrchr(pfs0HeaderFilename, '/' ) + 1);
|
|
}
|
|
}
|
|
|
|
breaks += 2;
|
|
|
|
uiRefreshDisplay();
|
|
}
|
|
} else {
|
|
if (dumping)
|
|
{
|
|
breaks += 6;
|
|
if (fat32_error) breaks += 2;
|
|
}
|
|
|
|
breaks += 2;
|
|
|
|
if (removeFile)
|
|
{
|
|
if (seqDumpMode)
|
|
{
|
|
for(u8 i = 0; i <= splitIndex; i++)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.nsp.%02u", NSP_DUMP_PATH, dumpName, i);
|
|
remove(dumpPath);
|
|
}
|
|
} else {
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.nsp", NSP_DUMP_PATH, dumpName);
|
|
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32)
|
|
{
|
|
fsdevDeleteDirectoryRecursively(dumpPath);
|
|
} else {
|
|
remove(dumpPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nspPfs0FilePtrs) free(nspPfs0FilePtrs);
|
|
|
|
if (nspPfs0StrTable) free(nspPfs0StrTable);
|
|
|
|
if (nspPfs0EntryTable) free(nspPfs0EntryTable);
|
|
|
|
if (cnmtXml) free(cnmtXml);
|
|
|
|
if (cnmtNcaBuf) free(cnmtNcaBuf);
|
|
|
|
if (ncaProgramMod)
|
|
{
|
|
for(i = 0; i < ncaProgramModCnt; i++)
|
|
{
|
|
if (ncaProgramMod[i].hash_table) free(ncaProgramMod[i].hash_table);
|
|
if (ncaProgramMod[i].block_data[0]) free(ncaProgramMod[i].block_data[0]);
|
|
if (ncaProgramMod[i].block_data[1]) free(ncaProgramMod[i].block_data[1]);
|
|
}
|
|
|
|
free(ncaProgramMod);
|
|
}
|
|
|
|
if (xml_records)
|
|
{
|
|
for(i = 0; i < xml_rec_cnt; i++)
|
|
{
|
|
if (xml_records[i].xml_data) free(xml_records[i].xml_data);
|
|
if (xml_records[i].nacp_icons) free(xml_records[i].nacp_icons);
|
|
}
|
|
|
|
free(xml_records);
|
|
}
|
|
|
|
if (xml_content_info) free(xml_content_info);
|
|
|
|
ncmContentStorageClose(&ncmStorage);
|
|
|
|
if (curStorageId == NcmStorageId_GameCard) closeGameCardStoragePartition();
|
|
|
|
if (titleContentInfos) free(titleContentInfos);
|
|
|
|
if (seqDumpNcaHashes) free(seqDumpNcaHashes);
|
|
|
|
if (seqDumpFile) fclose(seqDumpFile);
|
|
|
|
if (seqDumpFileRemove) remove(seqDumpFilename);
|
|
|
|
if (dumpName) free(dumpName);
|
|
|
|
if (!batch) changeHomeButtonBlockStatus(false);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int batchEntryCmp(const void *a, const void *b)
|
|
{
|
|
batchEntry *batchEntry1 = (batchEntry*)a;
|
|
batchEntry *batchEntry2 = (batchEntry*)b;
|
|
|
|
return strcasecmp(batchEntry1->nspFilename, batchEntry2->nspFilename);
|
|
}
|
|
|
|
int dumpNintendoSubmissionPackageBatch(batchOptions *batchDumpCfg)
|
|
{
|
|
int ret = -1;
|
|
|
|
if (!batchDumpCfg)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid batch dump configuration struct!", __func__);
|
|
breaks += 2;
|
|
return ret;
|
|
}
|
|
|
|
bool dumpAppTitles = batchDumpCfg->dumpAppTitles;
|
|
bool dumpPatchTitles = batchDumpCfg->dumpPatchTitles;
|
|
bool dumpAddOnTitles = batchDumpCfg->dumpAddOnTitles;
|
|
bool isFat32 = batchDumpCfg->isFat32;
|
|
bool removeConsoleData = batchDumpCfg->removeConsoleData;
|
|
bool tiklessDump = batchDumpCfg->tiklessDump;
|
|
bool npdmAcidRsaPatch = batchDumpCfg->npdmAcidRsaPatch;
|
|
bool dumpDeltaFragments = batchDumpCfg->dumpDeltaFragments;
|
|
bool skipDumpedTitles = batchDumpCfg->skipDumpedTitles;
|
|
bool rememberDumpedTitles = batchDumpCfg->rememberDumpedTitles;
|
|
bool haltOnErrors = batchDumpCfg->haltOnErrors;
|
|
bool useBrackets = batchDumpCfg->useBrackets;
|
|
batchModeSourceStorage batchModeSrc = batchDumpCfg->batchModeSrc;
|
|
|
|
if ((!dumpAppTitles && !dumpPatchTitles && !dumpAddOnTitles) || (batchModeSrc == BATCH_SOURCE_ALL && ((dumpAppTitles && !titleAppCount) || (dumpPatchTitles && !titlePatchCount) || (dumpAddOnTitles && !titleAddOnCount))) || (batchModeSrc == BATCH_SOURCE_SDCARD && ((dumpAppTitles && !sdCardTitleAppCount) || (dumpPatchTitles && !sdCardTitlePatchCount) || (dumpAddOnTitles && !sdCardTitleAddOnCount))) || (batchModeSrc == BATCH_SOURCE_EMMC && ((dumpAppTitles && !emmcTitleAppCount) || (dumpPatchTitles && !emmcTitlePatchCount) || (dumpAddOnTitles && !emmcTitleAddOnCount))) || batchModeSrc >= BATCH_SOURCE_CNT)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to perform batch NSP dump!", __func__);
|
|
breaks += 2;
|
|
return ret;
|
|
}
|
|
|
|
u32 i, j;
|
|
|
|
u32 totalTitleCount = 0, totalAppCount = 0, totalPatchCount = 0, totalAddOnCount = 0;
|
|
|
|
u32 titleCount = 0, titleIndex = 0;
|
|
|
|
char *dumpName = NULL;
|
|
char summary_str[128] = {'\0'};
|
|
|
|
int initial_breaks = breaks, cur_breaks;
|
|
|
|
const u32 maxSummaryFileCount = 8;
|
|
u32 summaryPage = 0, selectedSummaryEntry = 0;
|
|
u32 xpos = 0, ypos = 0;
|
|
u64 keysDown = 0, keysHeld = 0;
|
|
|
|
u32 maxEntryCount = 0, batchEntryIndex = 0, disabledEntryCount = 0;
|
|
batchEntry *batchEntries = NULL, *tmpBatchEntries = NULL;
|
|
|
|
bool proceed = true;
|
|
|
|
// Generate NSP configuration struct
|
|
nspOptions nspDumpCfg;
|
|
|
|
nspDumpCfg.isFat32 = isFat32;
|
|
nspDumpCfg.useNoIntroLookup = false;
|
|
nspDumpCfg.removeConsoleData = removeConsoleData;
|
|
nspDumpCfg.tiklessDump = tiklessDump;
|
|
nspDumpCfg.npdmAcidRsaPatch = npdmAcidRsaPatch;
|
|
nspDumpCfg.dumpDeltaFragments = dumpDeltaFragments;
|
|
nspDumpCfg.useBrackets = useBrackets;
|
|
|
|
// Allocate memory for the batch entries
|
|
if (dumpAppTitles) maxEntryCount += (batchModeSrc == BATCH_SOURCE_ALL ? titleAppCount : (batchModeSrc == BATCH_SOURCE_SDCARD ? sdCardTitleAppCount : emmcTitleAppCount));
|
|
if (dumpPatchTitles) maxEntryCount += (batchModeSrc == BATCH_SOURCE_ALL ? titlePatchCount : (batchModeSrc == BATCH_SOURCE_SDCARD ? sdCardTitlePatchCount : emmcTitlePatchCount));
|
|
if (dumpAddOnTitles) maxEntryCount += (batchModeSrc == BATCH_SOURCE_ALL ? titleAddOnCount : (batchModeSrc == BATCH_SOURCE_SDCARD ? sdCardTitleAddOnCount : emmcTitleAddOnCount));
|
|
|
|
batchEntries = calloc(maxEntryCount, sizeof(batchEntry));
|
|
if (!batchEntries)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to allocate memory for batch entries!", __func__);
|
|
breaks += 2;
|
|
return ret;
|
|
}
|
|
|
|
for(i = 0; i < 3; i++)
|
|
{
|
|
if ((i == 0 && !dumpAppTitles) || (i == 1 && !dumpPatchTitles) || (i == 2 && !dumpAddOnTitles)) continue;
|
|
|
|
u32 emmcRefTitleCount = 0;
|
|
nspDumpType curNspDumpType = DUMP_APP_NSP;
|
|
|
|
switch(i)
|
|
{
|
|
case 0:
|
|
titleCount = (batchModeSrc == BATCH_SOURCE_ALL ? titleAppCount : (batchModeSrc == BATCH_SOURCE_SDCARD ? sdCardTitleAppCount : emmcTitleAppCount));
|
|
emmcRefTitleCount = sdCardTitleAppCount;
|
|
curNspDumpType = DUMP_APP_NSP;
|
|
break;
|
|
case 1:
|
|
titleCount = (batchModeSrc == BATCH_SOURCE_ALL ? titlePatchCount : (batchModeSrc == BATCH_SOURCE_SDCARD ? sdCardTitlePatchCount : emmcTitlePatchCount));
|
|
emmcRefTitleCount = sdCardTitlePatchCount;
|
|
curNspDumpType = DUMP_PATCH_NSP;
|
|
break;
|
|
case 2:
|
|
titleCount = (batchModeSrc == BATCH_SOURCE_ALL ? titleAddOnCount : (batchModeSrc == BATCH_SOURCE_SDCARD ? sdCardTitleAddOnCount : emmcTitleAddOnCount));
|
|
emmcRefTitleCount = sdCardTitleAddOnCount;
|
|
curNspDumpType = DUMP_ADDON_NSP;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
for(j = 0; j < titleCount; j++)
|
|
{
|
|
titleIndex = ((batchModeSrc == BATCH_SOURCE_ALL || batchModeSrc == BATCH_SOURCE_SDCARD) ? j : (j + emmcRefTitleCount));
|
|
|
|
dumpName = generateNSPDumpName(curNspDumpType, titleIndex, false);
|
|
if (!dumpName)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to generate output dump name!", __func__);
|
|
breaks += 2;
|
|
goto out;
|
|
}
|
|
|
|
// Check if an override file already exists for this dump
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s%s.nsp", BATCH_OVERRIDES_PATH, dumpName);
|
|
|
|
if (checkIfFileExists(strbuf))
|
|
{
|
|
free(dumpName);
|
|
dumpName = NULL;
|
|
continue;
|
|
}
|
|
|
|
snprintf(batchEntries[batchEntryIndex].nspFilename, MAX_CHARACTERS(batchEntries[batchEntryIndex].nspFilename), strrchr(strbuf, '/') + 1);
|
|
snprintf(batchEntries[batchEntryIndex].truncatedNspFilename, MAX_CHARACTERS(batchEntries[batchEntryIndex].truncatedNspFilename), batchEntries[batchEntryIndex].nspFilename);
|
|
|
|
if (useBrackets)
|
|
{
|
|
// Generate output name with brackets
|
|
free(dumpName);
|
|
dumpName = NULL;
|
|
|
|
dumpName = generateNSPDumpName(curNspDumpType, titleIndex, true);
|
|
if (!dumpName)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to generate output dump name with brackets!", __func__);
|
|
breaks += 2;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
// Check if this title has already been dumped
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s%s.nsp", NSP_DUMP_PATH, dumpName);
|
|
|
|
free(dumpName);
|
|
dumpName = NULL;
|
|
|
|
if (skipDumpedTitles && checkIfFileExists(strbuf)) continue;
|
|
|
|
// Save title properties
|
|
batchEntries[batchEntryIndex].enabled = true;
|
|
batchEntries[batchEntryIndex].titleType = curNspDumpType;
|
|
batchEntries[batchEntryIndex].titleIndex = titleIndex;
|
|
batchEntries[batchEntryIndex].contentSize = (i == 0 ? baseAppEntries[titleIndex].contentSize : (i == 1 ? patchEntries[titleIndex].contentSize : addOnEntries[titleIndex].contentSize));
|
|
batchEntries[batchEntryIndex].contentSizeStr = (i == 0 ? baseAppEntries[titleIndex].contentSizeStr : (i == 1 ? patchEntries[titleIndex].contentSizeStr : addOnEntries[titleIndex].contentSizeStr));
|
|
|
|
// Fix entry name length
|
|
truncateBrowserEntryName(batchEntries[batchEntryIndex].truncatedNspFilename);
|
|
|
|
// Increase batch entry index
|
|
batchEntryIndex++;
|
|
|
|
// Increase total base application count
|
|
if (i == 0) totalAppCount++;
|
|
|
|
// Increase total patch count
|
|
if (i == 1) totalPatchCount++;
|
|
|
|
// Increase total addon count
|
|
if (i == 2) totalAddOnCount++;
|
|
}
|
|
}
|
|
|
|
// Calculate total title count
|
|
totalTitleCount = (totalAppCount + totalPatchCount + totalAddOnCount);
|
|
if (!totalTitleCount)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "You have already dumped all titles matching the selected settings!");
|
|
breaks += 2;
|
|
goto out;
|
|
}
|
|
|
|
// Sort batch entries by name
|
|
qsort(batchEntries, totalTitleCount, sizeof(batchEntry), batchEntryCmp);
|
|
|
|
if (totalTitleCount < maxEntryCount)
|
|
{
|
|
tmpBatchEntries = realloc(batchEntries, totalTitleCount * sizeof(batchEntry));
|
|
if (!tmpBatchEntries)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "%s: failed to reallocate batch entries!", __func__);
|
|
breaks += 2;
|
|
goto out;
|
|
}
|
|
|
|
batchEntries = tmpBatchEntries;
|
|
tmpBatchEntries = NULL;
|
|
}
|
|
|
|
// Display summary controls
|
|
if (totalTitleCount > maxSummaryFileCount)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "[ " NINTENDO_FONT_DPAD " / " NINTENDO_FONT_LSTICK " / " NINTENDO_FONT_RSTICK " ] Move | [ " NINTENDO_FONT_ZL " / " NINTENDO_FONT_ZR " ] Change page | [ " NINTENDO_FONT_A " ] Proceed | [ " NINTENDO_FONT_B " ] Cancel | [ " NINTENDO_FONT_Y " ] Toggle selected entry | [ " NINTENDO_FONT_L " ] Disable all entries | [ " NINTENDO_FONT_R " ] Enable all entries\n[ " NINTENDO_FONT_PLUS " ] Exit");
|
|
} else {
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "[ " NINTENDO_FONT_DPAD " / " NINTENDO_FONT_LSTICK " / " NINTENDO_FONT_RSTICK " ] Move | [ " NINTENDO_FONT_A " ] Proceed | [ " NINTENDO_FONT_B " ] Cancel | [ " NINTENDO_FONT_Y " ] Toggle selected entry | [ " NINTENDO_FONT_L " ] Disable all entries | [ " NINTENDO_FONT_R " ] Enable all entries | [ " NINTENDO_FONT_PLUS " ] Exit");
|
|
}
|
|
|
|
breaks += 2;
|
|
|
|
// Display free space
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Free SD card space: %s (%lu bytes).", freeSpaceStr, freeSpace);
|
|
breaks += 2;
|
|
|
|
// Display summary
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Summary:");
|
|
breaks += 2;
|
|
|
|
if (totalAppCount)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "BASE: %u", totalAppCount);
|
|
strcat(summary_str, strbuf);
|
|
}
|
|
|
|
if (totalPatchCount)
|
|
{
|
|
if (totalAppCount) strcat(summary_str, " | ");
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "UPD: %u", totalPatchCount);
|
|
strcat(summary_str, strbuf);
|
|
}
|
|
|
|
if (totalAddOnCount)
|
|
{
|
|
if (totalAppCount || totalPatchCount) strcat(summary_str, " | ");
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "DLC: %u", totalAddOnCount);
|
|
strcat(summary_str, strbuf);
|
|
}
|
|
|
|
strcat(summary_str, " | ");
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "Total: %u", totalTitleCount);
|
|
strcat(summary_str, strbuf);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, summary_str);
|
|
breaks++;
|
|
|
|
while(true)
|
|
{
|
|
cur_breaks = breaks;
|
|
|
|
uiFill(0, 8 + (cur_breaks * LINE_HEIGHT), FB_WIDTH, FB_HEIGHT - (8 + (cur_breaks * LINE_HEIGHT)), BG_COLOR_RGB);
|
|
|
|
// Calculate the number of selected titles
|
|
j = 0;
|
|
u64 totalOutSize = 0;
|
|
char totalOutSizeStr[32] = {'\0'};
|
|
|
|
for(i = 0; i < totalTitleCount; i++)
|
|
{
|
|
if (batchEntries[i].enabled)
|
|
{
|
|
j++;
|
|
totalOutSize += batchEntries[i].contentSize;
|
|
}
|
|
}
|
|
|
|
convertSize(totalOutSize, totalOutSizeStr, MAX_CHARACTERS(totalOutSizeStr));
|
|
|
|
if (totalTitleCount > maxSummaryFileCount)
|
|
{
|
|
if (j && totalOutSize)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(cur_breaks), FONT_COLOR_RGB, "Current page: %u | Selected titles: %u | Approximate total dump size: %s (%lu bytes)", summaryPage + 1, j, totalOutSizeStr, totalOutSize);
|
|
} else {
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(cur_breaks), FONT_COLOR_RGB, "Current page: %u | Selected titles: %u", summaryPage + 1, j);
|
|
}
|
|
} else {
|
|
if (j && totalOutSize)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(cur_breaks), FONT_COLOR_RGB, "Selected titles: %u | Approximate total dump size: %s (%lu bytes)", j, totalOutSizeStr, totalOutSize);
|
|
} else {
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(cur_breaks), FONT_COLOR_RGB, "Selected titles: %u", j);
|
|
}
|
|
}
|
|
|
|
cur_breaks += 2;
|
|
|
|
j = 0;
|
|
highlight = false;
|
|
|
|
for(i = (summaryPage * maxSummaryFileCount); i < ((summaryPage + 1) * maxSummaryFileCount); i++, j++)
|
|
{
|
|
if (i >= totalTitleCount) break;
|
|
|
|
xpos = STRING_X_POS;
|
|
ypos = (8 + (cur_breaks * LINE_HEIGHT) + (j * (font_height + 12)) + 6);
|
|
|
|
if (i == selectedSummaryEntry)
|
|
{
|
|
highlight = true;
|
|
uiFill(0, ypos - 6, FB_WIDTH, font_height + 12, HIGHLIGHT_BG_COLOR_RGB);
|
|
}
|
|
|
|
uiDrawIcon((highlight ? (batchEntries[i].enabled ? enabledHighlightIconBuf : disabledHighlightIconBuf) : (batchEntries[i].enabled ? enabledNormalIconBuf : disabledNormalIconBuf)), BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, xpos, ypos);
|
|
xpos += (BROWSER_ICON_DIMENSION + 8);
|
|
|
|
if (highlight)
|
|
{
|
|
uiDrawString(xpos, ypos, HIGHLIGHT_FONT_COLOR_RGB, batchEntries[i].truncatedNspFilename);
|
|
|
|
if (batchEntries[i].contentSize)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "(%s)", batchEntries[i].contentSizeStr);
|
|
uiDrawString(FB_WIDTH - (8 + uiGetStrWidth(strbuf)), ypos, HIGHLIGHT_FONT_COLOR_RGB, strbuf);
|
|
}
|
|
} else {
|
|
uiDrawString(xpos, ypos, FONT_COLOR_RGB, batchEntries[i].truncatedNspFilename);
|
|
|
|
if (batchEntries[i].contentSize)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "(%s)", batchEntries[i].contentSizeStr);
|
|
uiDrawString(FB_WIDTH - (8 + uiGetStrWidth(strbuf)), ypos, FONT_COLOR_RGB, strbuf);
|
|
}
|
|
}
|
|
|
|
if (i == selectedSummaryEntry) highlight = false;
|
|
}
|
|
|
|
while(true)
|
|
{
|
|
uiUpdateStatusMsg();
|
|
uiRefreshDisplay();
|
|
|
|
hidScanInput();
|
|
|
|
keysDown = hidKeysAllDown(CONTROLLER_P1_AUTO);
|
|
keysHeld = hidKeysAllHeld(CONTROLLER_P1_AUTO);
|
|
|
|
if ((keysDown && !(keysDown & KEY_TOUCH)) || (keysHeld && !(keysHeld & KEY_TOUCH))) break;
|
|
}
|
|
|
|
// Exit
|
|
if (keysDown & KEY_PLUS)
|
|
{
|
|
ret = -2;
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
// Start batch dump process
|
|
if (keysDown & KEY_A)
|
|
{
|
|
// Check if we have at least a single enabled entry
|
|
for(i = 0; i < totalTitleCount; i++)
|
|
{
|
|
if (batchEntries[i].enabled) break;
|
|
}
|
|
|
|
if (i < totalTitleCount)
|
|
{
|
|
proceed = true;
|
|
break;
|
|
} else {
|
|
uiStatusMsg("Please enable at least one entry from the list.");
|
|
}
|
|
}
|
|
|
|
// Cancel batch dump process
|
|
if (keysDown & KEY_B)
|
|
{
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
// Toggle selected entry
|
|
if (keysDown & KEY_Y) batchEntries[selectedSummaryEntry].enabled ^= 0x01;
|
|
|
|
// Disable all entries
|
|
if (keysDown & KEY_L)
|
|
{
|
|
for(i = 0; i < totalTitleCount; i++) batchEntries[i].enabled = false;
|
|
}
|
|
|
|
// Enable all entries
|
|
if (keysDown & KEY_R)
|
|
{
|
|
for(i = 0; i < totalTitleCount; i++) batchEntries[i].enabled = true;
|
|
}
|
|
|
|
// Change page (left)
|
|
if ((keysDown & KEY_ZL) && totalTitleCount > maxSummaryFileCount)
|
|
{
|
|
if (summaryPage > 0)
|
|
{
|
|
summaryPage--;
|
|
selectedSummaryEntry = (summaryPage * maxSummaryFileCount);
|
|
}
|
|
}
|
|
|
|
// Change page (right)
|
|
if ((keysDown & KEY_ZR) && totalTitleCount > maxSummaryFileCount)
|
|
{
|
|
if (((summaryPage + 1) * maxSummaryFileCount) < totalTitleCount)
|
|
{
|
|
summaryPage++;
|
|
selectedSummaryEntry = (summaryPage * maxSummaryFileCount);
|
|
}
|
|
}
|
|
|
|
// Go up
|
|
if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP))
|
|
{
|
|
if (selectedSummaryEntry > (summaryPage * maxSummaryFileCount))
|
|
{
|
|
selectedSummaryEntry--;
|
|
} else {
|
|
if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP))
|
|
{
|
|
if (((summaryPage + 1) * maxSummaryFileCount) < totalTitleCount)
|
|
{
|
|
selectedSummaryEntry = (((summaryPage + 1) * maxSummaryFileCount) - 1);
|
|
} else {
|
|
selectedSummaryEntry = (totalTitleCount - 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Go down
|
|
if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN))
|
|
{
|
|
if (((((summaryPage + 1) * maxSummaryFileCount) < totalTitleCount) && selectedSummaryEntry < (((summaryPage + 1) * maxSummaryFileCount) - 1)) || ((((summaryPage + 1) * maxSummaryFileCount) >= totalTitleCount) && selectedSummaryEntry < (totalTitleCount - 1)))
|
|
{
|
|
selectedSummaryEntry++;
|
|
} else {
|
|
if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN))
|
|
{
|
|
selectedSummaryEntry = (summaryPage * maxSummaryFileCount);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
uiClearStatusMsg();
|
|
|
|
breaks = initial_breaks;
|
|
uiFill(0, 8 + (breaks * LINE_HEIGHT), FB_WIDTH, FB_HEIGHT - (8 + (breaks * LINE_HEIGHT)), BG_COLOR_RGB);
|
|
|
|
if (!proceed)
|
|
{
|
|
if (ret != -2)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Process canceled");
|
|
breaks += 2;
|
|
}
|
|
|
|
goto out;
|
|
}
|
|
|
|
// Calculate the disabled entry count
|
|
for(i = 0; i < totalTitleCount; i++)
|
|
{
|
|
if (!batchEntries[i].enabled) disabledEntryCount++;
|
|
}
|
|
|
|
// Start dump process
|
|
dumpStartMsg();
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
changeHomeButtonBlockStatus(true);
|
|
|
|
initial_breaks = breaks;
|
|
|
|
j = 0;
|
|
|
|
for(i = 0; i < totalTitleCount; i++)
|
|
{
|
|
if (!batchEntries[i].enabled) continue;
|
|
|
|
breaks = initial_breaks;
|
|
|
|
uiFill(0, 8 + (breaks * LINE_HEIGHT), FB_WIDTH, FB_HEIGHT - (8 + (breaks * LINE_HEIGHT)), BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Title: %.*s [%u / %u].", strlen(batchEntries[i].nspFilename) - 4, batchEntries[i].nspFilename, j + 1, totalTitleCount - disabledEntryCount);
|
|
breaks++;
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Free SD card space: %s (%lu bytes).", freeSpaceStr, freeSpace);
|
|
breaks++;
|
|
|
|
uiRefreshDisplay();
|
|
|
|
// Dump title
|
|
int nspRet = dumpNintendoSubmissionPackage(batchEntries[i].titleType, batchEntries[i].titleIndex, &nspDumpCfg, true);
|
|
if (nspRet >= 0)
|
|
{
|
|
// Create override file if necessary
|
|
if (rememberDumpedTitles)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s%s", BATCH_OVERRIDES_PATH, batchEntries[i].nspFilename);
|
|
FILE *overrideFile = fopen(strbuf, "wb");
|
|
if (overrideFile) fclose(overrideFile);
|
|
}
|
|
} else {
|
|
// If "Halt dump process on errors" is disabled, just wait a little bit and keep going (unless the process was truly canceled by the user)
|
|
if (!haltOnErrors && nspRet != -2)
|
|
{
|
|
delay(5);
|
|
} else {
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
// Update free space
|
|
updateFreeSpace();
|
|
|
|
j++;
|
|
}
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Process successfully completed!");
|
|
breaks += 2;
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
if (batchEntries) free(batchEntries);
|
|
|
|
changeHomeButtonBlockStatus(false);
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool dumpRawHfs0Partition(u32 partition, bool doSplitting)
|
|
{
|
|
if (!gameCardInfo.rootHfs0Header || !gameCardInfo.hfs0PartitionCnt || partition >= gameCardInfo.hfs0PartitionCnt || !gameCardInfo.hfs0Partitions || !gameCardInfo.hfs0Partitions[partition].size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to dump raw HFS0 partition!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
Result result;
|
|
u64 partitionOffset;
|
|
bool success = false, fat32_error = false;
|
|
u64 n = DUMP_BUFFER_SIZE;
|
|
char dumpPath[NAME_BUF_LEN] = {'\0'};
|
|
FILE *outFile = NULL;
|
|
u8 splitIndex = 0;
|
|
openIStoragePartition storageIndex;
|
|
|
|
memset(dumpBuf, 0, DUMP_BUFFER_SIZE);
|
|
|
|
progress_ctx_t progressCtx;
|
|
memset(&progressCtx, 0, sizeof(progress_ctx_t));
|
|
|
|
size_t write_res;
|
|
|
|
char *dumpName = generateGameCardDumpName(false);
|
|
if (!dumpName)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to generate output dump name!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
// The IStorage instance returned for partition == 0 contains the gamecard header, the gamecard certificate, the root HFS0 header and:
|
|
// * The "update" (0) partition and the "normal" (1) partition (for gamecard type 0x01)
|
|
// * The "update" (0) partition, the "logo" (1) partition and the "normal" (2) partition (for gamecard type 0x02)
|
|
// The IStorage instance returned for partition == 1 contains the "secure" partition (which can either be 2 or 3 depending on the gamecard type)
|
|
// This makes sure we just dump the *actual* raw HFS0 partition, without preceding data, padding, etc.
|
|
// Oddly enough, IFileSystem instances actually point to the specified partition ID filesystem. I don't understand why it doesn't work like that for IStorage, but whatever
|
|
// NOTE: Using partition == 2 returns error 0x149002, and using higher values probably do so, too
|
|
partitionOffset = gameCardInfo.hfs0Partitions[partition].offset; // Relative to IStorage instance
|
|
progressCtx.totalSize = gameCardInfo.hfs0Partitions[partition].size;
|
|
storageIndex = (openIStoragePartition)(HFS0_TO_ISTORAGE_IDX(gameCardInfo.hfs0PartitionCnt, partition) + 1);
|
|
|
|
convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, MAX_CHARACTERS(progressCtx.totalSizeStr));
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "HFS0 partition size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize);
|
|
breaks += 2;
|
|
|
|
if (progressCtx.totalSize > freeSpace)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s - Partition %u (%s).hfs0.%02u", HFS0_DUMP_PATH, dumpName, partition, GAMECARD_PARTITION_NAME(gameCardInfo.hfs0PartitionCnt, partition), splitIndex);
|
|
} else {
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s - Partition %u (%s).hfs0", HFS0_DUMP_PATH, dumpName, partition, GAMECARD_PARTITION_NAME(gameCardInfo.hfs0PartitionCnt, partition));
|
|
}
|
|
|
|
// Check if the dump already exists
|
|
if (checkIfFileExists(dumpPath))
|
|
{
|
|
// Ask the user if they want to proceed anyway
|
|
int cur_breaks = breaks;
|
|
|
|
if (!yesNoPrompt("You have already dumped this content. Do you wish to proceed anyway?"))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
goto out;
|
|
} else {
|
|
// Remove the prompt from the screen
|
|
breaks = cur_breaks;
|
|
uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), BG_COLOR_RGB);
|
|
}
|
|
}
|
|
|
|
result = openGameCardStoragePartition(storageIndex);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open IStorage partition #%u! (0x%08X)", __func__, storageIndex - 1, result);
|
|
goto out;
|
|
}
|
|
|
|
outFile = fopen(dumpPath, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open output file \"%s\"!", __func__, dumpPath);
|
|
goto out;
|
|
}
|
|
|
|
// Start dump process
|
|
dumpStartMsg();
|
|
appletModeOperationWarning();
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
changeHomeButtonBlockStatus(true);
|
|
|
|
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) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(dumpPath, '/' ) + 1);
|
|
|
|
if (n > (progressCtx.totalSize - progressCtx.curOffset)) n = (progressCtx.totalSize - progressCtx.curOffset);
|
|
|
|
result = readGameCardStoragePartition(partitionOffset + progressCtx.curOffset, dumpBuf, n);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to read %lu bytes chunk at offset 0x%016lX from IStorage partition #%u! (0x%08X)", __func__, n, partitionOffset + progressCtx.curOffset, storageIndex - 1, result);
|
|
break;
|
|
}
|
|
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting && (progressCtx.curOffset + n) >= ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE))
|
|
{
|
|
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);
|
|
|
|
if (old_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf, 1, old_file_chunk_size, outFile);
|
|
if (write_res != old_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, old_file_chunk_size, progressCtx.curOffset, splitIndex, write_res);
|
|
break;
|
|
}
|
|
}
|
|
|
|
fclose(outFile);
|
|
outFile = NULL;
|
|
|
|
if (new_file_chunk_size > 0 || (progressCtx.curOffset + n) < progressCtx.totalSize)
|
|
{
|
|
splitIndex++;
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s - Partition %u (%s).hfs0.%02u", HFS0_DUMP_PATH, dumpName, partition, GAMECARD_PARTITION_NAME(gameCardInfo.hfs0PartitionCnt, partition), splitIndex);
|
|
|
|
outFile = fopen(dumpPath, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to open output file for part #%u!", __func__, splitIndex);
|
|
break;
|
|
}
|
|
|
|
if (new_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf + old_file_chunk_size, 1, new_file_chunk_size, outFile);
|
|
if (write_res != new_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, write_res);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
write_res = fwrite(dumpBuf, 1, n, outFile);
|
|
if (write_res != n)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", __func__, n, progressCtx.curOffset, write_res);
|
|
|
|
if ((progressCtx.curOffset + n) > FAT32_FILESIZE_LIMIT)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 4), FONT_COLOR_RGB, "You're probably using a FAT32 partition. Make sure to enable file splitting.");
|
|
fat32_error = true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
printProgressBar(&progressCtx, true, n);
|
|
|
|
if ((progressCtx.curOffset + n) < progressCtx.totalSize && cancelProcessCheck(&progressCtx))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (progressCtx.curOffset >= progressCtx.totalSize) success = true;
|
|
|
|
// Support empty files
|
|
if (!progressCtx.totalSize)
|
|
{
|
|
uiFill(0, ((progressCtx.line_offset - 2) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(dumpPath, '/' ) + 1);
|
|
|
|
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, MAX_CHARACTERS(progressCtx.etaInfo));
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Process successfully completed after %s!", progressCtx.etaInfo);
|
|
} else {
|
|
setProgressBarError(&progressCtx);
|
|
if (fat32_error) breaks += 2;
|
|
}
|
|
|
|
out:
|
|
if (outFile) fclose(outFile);
|
|
|
|
if (!success)
|
|
{
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting)
|
|
{
|
|
for(u8 i = 0; i <= splitIndex; i++)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s - Partition %u (%s).hfs0.%02u", HFS0_DUMP_PATH, dumpName, partition, GAMECARD_PARTITION_NAME(gameCardInfo.hfs0PartitionCnt, partition), i);
|
|
remove(dumpPath);
|
|
}
|
|
} else {
|
|
remove(dumpPath);
|
|
}
|
|
}
|
|
|
|
closeGameCardStoragePartition();
|
|
|
|
if (dumpName) free(dumpName);
|
|
|
|
breaks += 2;
|
|
|
|
changeHomeButtonBlockStatus(false);
|
|
|
|
return success;
|
|
}
|
|
|
|
bool copyFileFromHfs0Partition(u32 partition, const char *dest, const char *source, const u64 fileOffset, const u64 fileSize, progress_ctx_t *progressCtx, bool doSplitting)
|
|
{
|
|
if (!gameCardInfo.rootHfs0Header || !gameCardInfo.hfs0PartitionCnt || partition >= gameCardInfo.hfs0PartitionCnt || !gameCardInfo.hfs0Partitions || !gameCardInfo.hfs0Partitions[partition].header || !gameCardInfo.hfs0Partitions[partition].header_size || !dest || !strlen(dest) || !source || !strlen(source) || !progressCtx)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to copy file from HFS0 partition!", __func__);
|
|
return false;
|
|
}
|
|
|
|
if (!gameCardInfo.hfs0Partitions[partition].file_cnt)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: the selected HFS0 partition is empty!", __func__);
|
|
return false;
|
|
}
|
|
|
|
// IStorage handle must have been retrieved beforehand by the caller function
|
|
|
|
Result result;
|
|
bool success = false, fat32_error = false;
|
|
char splitFilename[NAME_BUF_LEN * 3] = {'\0'};
|
|
size_t destLen = strlen(dest);
|
|
FILE *outFile = NULL;
|
|
u64 off, n = DUMP_BUFFER_SIZE;
|
|
u8 splitIndex = 0;
|
|
openIStoragePartition storageIndex = (openIStoragePartition)(HFS0_TO_ISTORAGE_IDX(gameCardInfo.hfs0PartitionCnt, partition) + 1);
|
|
|
|
size_t write_res;
|
|
|
|
memset(dumpBuf, 0, DUMP_BUFFER_SIZE);
|
|
|
|
uiFill(0, ((progressCtx->line_offset - 4) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 4, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset - 4), FONT_COLOR_RGB, "Copying \"%s\"...", source);
|
|
|
|
if ((destLen + 1) >= MAX_CHARACTERS(splitFilename))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: destination path is too long! (%lu bytes)", __func__, destLen);
|
|
return false;
|
|
}
|
|
|
|
if (fileSize > FAT32_FILESIZE_LIMIT && doSplitting) snprintf(splitFilename, MAX_CHARACTERS(splitFilename), "%s.%02u", dest, splitIndex);
|
|
|
|
outFile = fopen(((fileSize > FAT32_FILESIZE_LIMIT && doSplitting) ? splitFilename : dest), "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to open output file!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
for (off = 0; off < fileSize; off += n, progressCtx->curOffset += n)
|
|
{
|
|
uiFill(0, ((progressCtx->line_offset - 2) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset - 2), FONT_COLOR_RGB, "Output file: \"%s\".", ((fileSize > FAT32_FILESIZE_LIMIT && doSplitting) ? (strrchr(splitFilename, '/') + 1) : (strrchr(dest, '/') + 1)));
|
|
|
|
uiRefreshDisplay();
|
|
|
|
if (n > (fileSize - off)) n = (fileSize - off);
|
|
|
|
result = readGameCardStoragePartition(fileOffset + off, dumpBuf, n);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to read %lu bytes chunk at offset 0x%016lX from IStorage partition #%u! (0x%08X)", __func__, n, fileOffset + off, storageIndex - 1, result);
|
|
break;
|
|
}
|
|
|
|
if (fileSize > FAT32_FILESIZE_LIMIT && doSplitting && (off + n) >= ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE))
|
|
{
|
|
u64 new_file_chunk_size = ((off + n) - ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE));
|
|
u64 old_file_chunk_size = (n - new_file_chunk_size);
|
|
|
|
if (old_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf, 1, old_file_chunk_size, outFile);
|
|
if (write_res != old_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, old_file_chunk_size, off, splitIndex, write_res);
|
|
break;
|
|
}
|
|
}
|
|
|
|
fclose(outFile);
|
|
outFile = NULL;
|
|
|
|
if (new_file_chunk_size > 0 || (off + n) < fileSize)
|
|
{
|
|
splitIndex++;
|
|
snprintf(splitFilename, MAX_CHARACTERS(splitFilename), "%s.%02u", dest, splitIndex);
|
|
|
|
outFile = fopen(splitFilename, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to open output file for part #%u!", __func__, splitIndex);
|
|
break;
|
|
}
|
|
|
|
if (new_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf + old_file_chunk_size, 1, new_file_chunk_size, outFile);
|
|
if (write_res != new_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, new_file_chunk_size, off + old_file_chunk_size, splitIndex, write_res);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
write_res = fwrite(dumpBuf, 1, n, outFile);
|
|
if (write_res != n)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", __func__, n, off, write_res);
|
|
|
|
if ((off + n) > FAT32_FILESIZE_LIMIT)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 4), FONT_COLOR_RGB, "You're probably using a FAT32 partition. Make sure to enable file splitting.");
|
|
fat32_error = true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
printProgressBar(progressCtx, true, n);
|
|
|
|
if (((off + n) < fileSize || (progressCtx->curOffset + n) < progressCtx->totalSize) && cancelProcessCheck(progressCtx))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (off >= fileSize) success = true;
|
|
|
|
// Support empty files
|
|
if (!fileSize)
|
|
{
|
|
uiFill(0, ((progressCtx->line_offset - 2) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset - 2), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(dest, '/') + 1);
|
|
|
|
if (progressCtx->totalSize == fileSize) progressCtx->progress = 100;
|
|
|
|
printProgressBar(progressCtx, false, 0);
|
|
}
|
|
|
|
if (!success)
|
|
{
|
|
setProgressBarError(progressCtx);
|
|
breaks = (progressCtx->line_offset + 2);
|
|
if (fat32_error) breaks += 2;
|
|
}
|
|
|
|
out:
|
|
if (outFile) fclose(outFile);
|
|
|
|
if (!success)
|
|
{
|
|
if (fileSize > FAT32_FILESIZE_LIMIT && doSplitting)
|
|
{
|
|
for(u8 i = 0; i <= splitIndex; i++)
|
|
{
|
|
snprintf(splitFilename, MAX_CHARACTERS(splitFilename), "%s.%02u", dest, i);
|
|
remove(splitFilename);
|
|
}
|
|
} else {
|
|
remove(dest);
|
|
}
|
|
}
|
|
|
|
breaks += 2;
|
|
|
|
return success;
|
|
}
|
|
|
|
bool copyHfs0PartitionContents(u32 partition, progress_ctx_t *progressCtx, const char *dest, bool splitting)
|
|
{
|
|
if (!gameCardInfo.rootHfs0Header || !gameCardInfo.hfs0PartitionCnt || partition >= gameCardInfo.hfs0PartitionCnt || !gameCardInfo.hfs0Partitions || !gameCardInfo.hfs0Partitions[partition].header || !gameCardInfo.hfs0Partitions[partition].header_size || !progressCtx || !dest || !strlen(dest))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to copy HFS0 partition contents!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (!gameCardInfo.hfs0Partitions[partition].file_cnt)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: the selected HFS0 partition is empty!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
Result result;
|
|
char dbuf[NAME_BUF_LEN * 2] = {'\0'};
|
|
hfs0_file_entry entry;
|
|
size_t dest_len = strlen(dest);
|
|
openIStoragePartition storageIndex = (openIStoragePartition)(HFS0_TO_ISTORAGE_IDX(gameCardInfo.hfs0PartitionCnt, partition) + 1);
|
|
|
|
u32 i;
|
|
bool success = false;
|
|
|
|
if ((dest_len + 1) >= MAX_CHARACTERS(dbuf))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: destination directory name is too long! (%lu bytes)", __func__, dest_len);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
snprintf(dbuf, MAX_CHARACTERS(dbuf), dest);
|
|
mkdir(dbuf, 0744);
|
|
|
|
dbuf[dest_len] = '/';
|
|
dest_len++;
|
|
|
|
result = openGameCardStoragePartition(storageIndex);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open IStorage partition #%u! (0x%08X)", __func__, storageIndex - 1, result);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx->start));
|
|
|
|
for(i = 0; i < gameCardInfo.hfs0Partitions[partition].file_cnt; i++)
|
|
{
|
|
memcpy(&entry, gameCardInfo.hfs0Partitions[partition].header + sizeof(hfs0_header) + (i * sizeof(hfs0_file_entry)), sizeof(hfs0_file_entry));
|
|
|
|
char *filename = (char*)(gameCardInfo.hfs0Partitions[partition].header + sizeof(hfs0_header) + (gameCardInfo.hfs0Partitions[partition].file_cnt * sizeof(hfs0_file_entry)) + entry.filename_offset);
|
|
|
|
dbuf[dest_len] = '\0';
|
|
strcat(dbuf, filename);
|
|
removeIllegalCharacters(dbuf + dest_len);
|
|
|
|
u64 fileOffset = (gameCardInfo.hfs0Partitions[partition].offset + gameCardInfo.hfs0Partitions[partition].header_size + entry.file_offset);
|
|
|
|
success = copyFileFromHfs0Partition(partition, dbuf, filename, fileOffset, entry.file_size, progressCtx, splitting);
|
|
if (!success) break;
|
|
}
|
|
|
|
closeGameCardStoragePartition();
|
|
|
|
return success;
|
|
}
|
|
|
|
bool dumpHfs0PartitionData(u32 partition, bool doSplitting)
|
|
{
|
|
if (!gameCardInfo.rootHfs0Header || !gameCardInfo.hfs0PartitionCnt || partition >= gameCardInfo.hfs0PartitionCnt || !gameCardInfo.hfs0Partitions || !gameCardInfo.hfs0Partitions[partition].header)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to dump HFS0 partition data!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (!gameCardInfo.hfs0Partitions[partition].file_cnt)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: the selected HFS0 partition is empty!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
u32 i;
|
|
hfs0_file_entry entry;
|
|
char dumpPath[NAME_BUF_LEN] = {'\0'};
|
|
|
|
progress_ctx_t progressCtx;
|
|
memset(&progressCtx, 0, sizeof(progress_ctx_t));
|
|
|
|
bool success = false;
|
|
|
|
char *dumpName = generateGameCardDumpName(false);
|
|
if (!dumpName)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to generate output dump name!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
// Calculate total size
|
|
for(i = 0; i < gameCardInfo.hfs0Partitions[partition].file_cnt; i++)
|
|
{
|
|
memcpy(&entry, gameCardInfo.hfs0Partitions[partition].header + sizeof(hfs0_header) + (i * sizeof(hfs0_file_entry)), sizeof(hfs0_file_entry));
|
|
progressCtx.totalSize += entry.file_size;
|
|
}
|
|
|
|
convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, MAX_CHARACTERS(progressCtx.totalSizeStr));
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Total partition data size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize);
|
|
breaks += 2;
|
|
|
|
if (progressCtx.totalSize > freeSpace)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s - Partition %u (%s)", HFS0_DUMP_PATH, dumpName, partition, GAMECARD_PARTITION_NAME(gameCardInfo.hfs0PartitionCnt, partition));
|
|
|
|
// Start dump process
|
|
dumpStartMsg();
|
|
appletModeOperationWarning();
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
changeHomeButtonBlockStatus(true);
|
|
|
|
progressCtx.line_offset = (breaks + 4);
|
|
|
|
success = copyHfs0PartitionContents(partition, &progressCtx, dumpPath, doSplitting);
|
|
|
|
if (success)
|
|
{
|
|
breaks = (progressCtx.line_offset + 2);
|
|
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now));
|
|
progressCtx.now -= progressCtx.start;
|
|
|
|
formatETAString(progressCtx.now, progressCtx.etaInfo, MAX_CHARACTERS(progressCtx.etaInfo));
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Process successfully completed after %s!", progressCtx.etaInfo);
|
|
} else {
|
|
removeDirectoryWithVerbose(dumpPath, "Deleting output directory. Please wait...");
|
|
}
|
|
|
|
out:
|
|
free(dumpName);
|
|
|
|
breaks += 2;
|
|
|
|
changeHomeButtonBlockStatus(false);
|
|
|
|
return success;
|
|
}
|
|
|
|
bool dumpFileFromHfs0Partition(u32 partition, u32 fileIndex, char *filename, bool doSplitting)
|
|
{
|
|
if (!gameCardInfo.rootHfs0Header || !gameCardInfo.hfs0PartitionCnt || partition >= gameCardInfo.hfs0PartitionCnt || !gameCardInfo.hfs0Partitions || !gameCardInfo.hfs0Partitions[partition].header || !gameCardInfo.hfs0Partitions[partition].header_size || !filename || !strlen(filename))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to dump file from HFS0 partition!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (!gameCardInfo.hfs0Partitions[partition].file_cnt)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: the selected HFS0 partition is empty!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (fileIndex >= gameCardInfo.hfs0Partitions[partition].file_cnt)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid file index!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
Result result;
|
|
hfs0_file_entry entry;
|
|
|
|
progress_ctx_t progressCtx;
|
|
memset(&progressCtx, 0, sizeof(progress_ctx_t));
|
|
|
|
u64 fileOffset = 0;
|
|
openIStoragePartition storageIndex = (openIStoragePartition)(HFS0_TO_ISTORAGE_IDX(gameCardInfo.hfs0PartitionCnt, partition) + 1);
|
|
|
|
char destCopyPath[NAME_BUF_LEN * 2] = {'\0'};
|
|
|
|
bool success = false;
|
|
|
|
char *dumpName = generateGameCardDumpName(false);
|
|
if (!dumpName)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to generate output dump name!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
memcpy(&entry, gameCardInfo.hfs0Partitions[partition].header + sizeof(hfs0_header) + (fileIndex * sizeof(hfs0_file_entry)), sizeof(hfs0_file_entry));
|
|
|
|
fileOffset = (gameCardInfo.hfs0Partitions[partition].offset + gameCardInfo.hfs0Partitions[partition].header_size + entry.file_offset);
|
|
|
|
progressCtx.totalSize = entry.file_size;
|
|
convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, MAX_CHARACTERS(progressCtx.totalSizeStr));
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "File size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize);
|
|
breaks += 2;
|
|
|
|
if (progressCtx.totalSize > freeSpace)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
snprintf(destCopyPath, MAX_CHARACTERS(destCopyPath), "%s%s - Partition %u (%s)", HFS0_DUMP_PATH, dumpName, partition, GAMECARD_PARTITION_NAME(gameCardInfo.hfs0PartitionCnt, partition));
|
|
mkdir(destCopyPath, 0744);
|
|
|
|
strcat(destCopyPath, "/");
|
|
size_t cur_len = strlen(destCopyPath);
|
|
strcat(destCopyPath, filename);
|
|
removeIllegalCharacters(destCopyPath + cur_len);
|
|
|
|
// Check if the dump already exists
|
|
if (checkIfFileExists(destCopyPath))
|
|
{
|
|
// Ask the user if they want to proceed anyway
|
|
int cur_breaks = breaks;
|
|
|
|
if (!yesNoPrompt("You have already dumped this content. Do you wish to proceed anyway?"))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
goto out;
|
|
} else {
|
|
// Remove the prompt from the screen
|
|
breaks = cur_breaks;
|
|
uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), BG_COLOR_RGB);
|
|
}
|
|
}
|
|
|
|
result = openGameCardStoragePartition(storageIndex);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open IStorage partition #%u! (0x%08X)", __func__, storageIndex - 1, result);
|
|
goto out;
|
|
}
|
|
|
|
// Start dump process
|
|
dumpStartMsg();
|
|
appletModeOperationWarning();
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
changeHomeButtonBlockStatus(true);
|
|
|
|
progressCtx.line_offset = (breaks + 4);
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start));
|
|
|
|
success = copyFileFromHfs0Partition(partition, destCopyPath, filename, fileOffset, progressCtx.totalSize, &progressCtx, doSplitting);
|
|
|
|
closeGameCardStoragePartition();
|
|
|
|
if (success)
|
|
{
|
|
breaks = (progressCtx.line_offset + 2);
|
|
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now));
|
|
progressCtx.now -= progressCtx.start;
|
|
|
|
formatETAString(progressCtx.now, progressCtx.etaInfo, MAX_CHARACTERS(progressCtx.etaInfo));
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Process successfully completed after %s!", progressCtx.etaInfo);
|
|
} else {
|
|
breaks -= 2;
|
|
}
|
|
|
|
out:
|
|
free(dumpName);
|
|
|
|
breaks += 2;
|
|
|
|
changeHomeButtonBlockStatus(false);
|
|
|
|
return success;
|
|
}
|
|
|
|
bool dumpExeFsSectionData(u32 titleIndex, bool usePatch, ncaFsOptions *exeFsDumpCfg)
|
|
{
|
|
if (!exeFsDumpCfg)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid ExeFS configuration struct!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
bool isFat32 = exeFsDumpCfg->isFat32;
|
|
bool useLayeredFSDir = exeFsDumpCfg->useLayeredFSDir;
|
|
|
|
u32 i;
|
|
u64 n = 0, offset = 0;
|
|
FILE *outFile = NULL;
|
|
u8 splitIndex = 0;
|
|
size_t write_res;
|
|
bool proceed = true, success = false, fat32_error = false;
|
|
|
|
char tmp_idx[5] = {'\0'};
|
|
char *dumpName = NULL;
|
|
char dumpPath[NAME_BUF_LEN] = {'\0'}, curDumpPath[NAME_BUF_LEN * 2] = {'\0'};
|
|
|
|
progress_ctx_t progressCtx;
|
|
memset(&progressCtx, 0, sizeof(progress_ctx_t));
|
|
|
|
memset(dumpBuf, 0, DUMP_BUFFER_SIZE);
|
|
|
|
if ((!usePatch && !titleAppCount) || (usePatch && !titlePatchCount))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid title count!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if ((!usePatch && titleIndex > (titleAppCount - 1)) || (usePatch && titleIndex > (titlePatchCount - 1)))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid title index!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (!useLayeredFSDir)
|
|
{
|
|
dumpName = generateNSPDumpName((!usePatch ? DUMP_APP_NSP : DUMP_PATCH_NSP), titleIndex, false);
|
|
if (!dumpName)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to generate output dump name!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Retrieve ExeFS from Program NCA
|
|
if (!readNcaExeFsSection(titleIndex, usePatch))
|
|
{
|
|
if (dumpName) free(dumpName);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
// Calculate total dump size
|
|
if (!calculateExeFsExtractedDataSize(&(progressCtx.totalSize))) goto out;
|
|
|
|
convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, MAX_CHARACTERS(progressCtx.totalSizeStr));
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Extracted ExeFS dump size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize);
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
if (progressCtx.totalSize > freeSpace)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Generate output path
|
|
if (!useLayeredFSDir)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s", EXEFS_DUMP_PATH, dumpName);
|
|
|
|
if (exeFsContext.idOffset > 0)
|
|
{
|
|
sprintf(strbuf, " (ID offset #%u)", exeFsContext.idOffset);
|
|
strcat(dumpPath, strbuf);
|
|
}
|
|
} else {
|
|
mkdir(cfwDirStr, 0744);
|
|
|
|
// Always use the base application title ID
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%016lX", cfwDirStr, (usePatch ? (patchEntries[titleIndex].titleId & ~APPLICATION_PATCH_BITMASK) : baseAppEntries[titleIndex].titleId) + exeFsContext.idOffset);
|
|
mkdir(dumpPath, 0744);
|
|
|
|
strcat(dumpPath, "/exefs");
|
|
}
|
|
|
|
mkdir(dumpPath, 0744);
|
|
|
|
// Start dump process
|
|
breaks++;
|
|
dumpStartMsg();
|
|
appletModeOperationWarning();
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
changeHomeButtonBlockStatus(true);
|
|
|
|
progressCtx.line_offset = (breaks + 4);
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start));
|
|
|
|
for(i = 0; i < exeFsContext.exefs_header.file_cnt; i++)
|
|
{
|
|
n = DUMP_BUFFER_SIZE;
|
|
outFile = NULL;
|
|
splitIndex = 0;
|
|
|
|
char *exeFsFilename = (exeFsContext.exefs_str_table + exeFsContext.exefs_entries[i].filename_offset);
|
|
|
|
// Check if we're dealing with a nameless file
|
|
if (!strlen(exeFsFilename))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: file entry without name in ExeFS section!", __func__);
|
|
break;
|
|
}
|
|
|
|
snprintf(curDumpPath, MAX_CHARACTERS(curDumpPath), "%s/%s", dumpPath, exeFsFilename);
|
|
removeIllegalCharacters(curDumpPath + strlen(dumpPath) + 1);
|
|
|
|
if (exeFsContext.exefs_entries[i].file_size > FAT32_FILESIZE_LIMIT && isFat32)
|
|
{
|
|
mkdir(curDumpPath, 0744);
|
|
sprintf(tmp_idx, "/%02u", splitIndex);
|
|
strcat(curDumpPath, tmp_idx);
|
|
}
|
|
|
|
outFile = fopen(curDumpPath, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to open output file \"%s\"!", __func__, curDumpPath);
|
|
break;
|
|
}
|
|
|
|
uiFill(0, ((progressCtx.line_offset - 4) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 4), FONT_COLOR_RGB, "Copying \"%s\"...", exeFsFilename);
|
|
|
|
for(offset = 0; offset < exeFsContext.exefs_entries[i].file_size; offset += n, progressCtx.curOffset += n)
|
|
{
|
|
uiFill(0, ((progressCtx.line_offset - 2) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(curDumpPath, '/') + 1);
|
|
|
|
uiRefreshDisplay();
|
|
|
|
if (n > (exeFsContext.exefs_entries[i].file_size - offset)) n = (exeFsContext.exefs_entries[i].file_size - offset);
|
|
|
|
breaks = (progressCtx.line_offset + 2);
|
|
proceed = processNcaCtrSectionBlock(&(exeFsContext.ncmStorage), &(exeFsContext.ncaId), &(exeFsContext.aes_ctx), exeFsContext.exefs_data_offset + exeFsContext.exefs_entries[i].file_offset + offset, dumpBuf, n, false);
|
|
breaks = (progressCtx.line_offset - 4);
|
|
|
|
if (!proceed) break;
|
|
|
|
if (exeFsContext.exefs_entries[i].file_size > FAT32_FILESIZE_LIMIT && isFat32 && (offset + n) >= ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE))
|
|
{
|
|
u64 new_file_chunk_size = ((offset + n) - ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE));
|
|
u64 old_file_chunk_size = (n - new_file_chunk_size);
|
|
|
|
if (old_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf, 1, old_file_chunk_size, outFile);
|
|
if (write_res != old_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, old_file_chunk_size, offset, splitIndex, write_res);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
fclose(outFile);
|
|
outFile = NULL;
|
|
|
|
if (new_file_chunk_size > 0 || (offset + n) < exeFsContext.exefs_entries[i].file_size)
|
|
{
|
|
char *tmp = strrchr(curDumpPath, '/');
|
|
if (tmp != NULL) *tmp = '\0';
|
|
|
|
splitIndex++;
|
|
sprintf(tmp_idx, "/%02u", splitIndex);
|
|
strcat(curDumpPath, tmp_idx);
|
|
|
|
outFile = fopen(curDumpPath, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to open output file for part #%u!", __func__, splitIndex);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
if (new_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf + old_file_chunk_size, 1, new_file_chunk_size, outFile);
|
|
if (write_res != new_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, new_file_chunk_size, offset + old_file_chunk_size, splitIndex, write_res);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
write_res = fwrite(dumpBuf, 1, n, outFile);
|
|
if (write_res != n)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", __func__, n, offset, write_res);
|
|
|
|
if ((offset + n) > FAT32_FILESIZE_LIMIT)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 4), FONT_COLOR_RGB, "You're probably using a FAT32 partition. Make sure to enable file splitting.");
|
|
fat32_error = true;
|
|
}
|
|
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
printProgressBar(&progressCtx, true, n);
|
|
|
|
if ((progressCtx.curOffset + n) < progressCtx.totalSize && cancelProcessCheck(&progressCtx))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (outFile) fclose(outFile);
|
|
|
|
if (!proceed) break;
|
|
|
|
// Support empty files
|
|
if (!exeFsContext.exefs_entries[i].file_size)
|
|
{
|
|
uiFill(0, ((progressCtx.line_offset - 2) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(curDumpPath, '/') + 1);
|
|
|
|
if (progressCtx.totalSize == exeFsContext.exefs_entries[i].file_size) progressCtx.progress = 100;
|
|
|
|
printProgressBar(&progressCtx, false, 0);
|
|
}
|
|
|
|
// Set archive bit (only for FAT32)
|
|
if (exeFsContext.exefs_entries[i].file_size > FAT32_FILESIZE_LIMIT && isFat32)
|
|
{
|
|
char *tmp = strrchr(curDumpPath, '/');
|
|
if (tmp != NULL) *tmp = '\0';
|
|
fsdevSetConcatenationFileAttribute(curDumpPath);
|
|
}
|
|
}
|
|
|
|
if (proceed)
|
|
{
|
|
if (progressCtx.curOffset >= progressCtx.totalSize)
|
|
{
|
|
success = true;
|
|
} else {
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: underdump error! Wrote %lu bytes, expected %lu bytes.", __func__, progressCtx.curOffset, progressCtx.totalSize);
|
|
}
|
|
}
|
|
|
|
breaks = (progressCtx.line_offset + 2);
|
|
|
|
if (success)
|
|
{
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now));
|
|
progressCtx.now -= progressCtx.start;
|
|
|
|
formatETAString(progressCtx.now, progressCtx.etaInfo, MAX_CHARACTERS(progressCtx.etaInfo));
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Process successfully completed after %s!", progressCtx.etaInfo);
|
|
} else {
|
|
setProgressBarError(&progressCtx);
|
|
if (fat32_error) breaks += 2;
|
|
removeDirectoryWithVerbose(dumpPath, "Deleting output directory. Please wait...");
|
|
}
|
|
|
|
out:
|
|
freeExeFsContext();
|
|
|
|
if (dumpName) free(dumpName);
|
|
|
|
breaks += 2;
|
|
|
|
changeHomeButtonBlockStatus(false);
|
|
|
|
return success;
|
|
}
|
|
|
|
bool dumpFileFromExeFsSection(u32 titleIndex, u32 fileIndex, bool usePatch, ncaFsOptions *exeFsDumpCfg)
|
|
{
|
|
if (!exeFsDumpCfg)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid ExeFS configuration struct!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
bool isFat32 = exeFsDumpCfg->isFat32;
|
|
bool useLayeredFSDir = exeFsDumpCfg->useLayeredFSDir;
|
|
|
|
if (!exeFsContext.exefs_header.file_cnt || fileIndex > (exeFsContext.exefs_header.file_cnt - 1) || !exeFsContext.exefs_entries || !exeFsContext.exefs_str_table || exeFsContext.exefs_data_offset <= exeFsContext.exefs_offset || (!usePatch && titleIndex > (titleAppCount - 1)) || (usePatch && titleIndex > (titlePatchCount - 1)))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to parse file entry from ExeFS section!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
u64 n = DUMP_BUFFER_SIZE;
|
|
FILE *outFile = NULL;
|
|
u8 splitIndex = 0;
|
|
size_t write_res;
|
|
bool proceed = true, success = false, fat32_error = false, removeFile = true;
|
|
|
|
char tmp_idx[5];
|
|
char *dumpName = NULL;
|
|
char dumpPath[NAME_BUF_LEN] = {'\0'};
|
|
|
|
progress_ctx_t progressCtx;
|
|
memset(&progressCtx, 0, sizeof(progress_ctx_t));
|
|
|
|
memset(dumpBuf, 0, DUMP_BUFFER_SIZE);
|
|
|
|
char *exeFsFilename = (exeFsContext.exefs_str_table + exeFsContext.exefs_entries[fileIndex].filename_offset);
|
|
|
|
// Check if we're dealing with a nameless file
|
|
if (!strlen(exeFsFilename))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: file entry without name in ExeFS section!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
// Generate output path
|
|
if (!useLayeredFSDir)
|
|
{
|
|
dumpName = generateNSPDumpName((!usePatch ? DUMP_APP_NSP : DUMP_PATCH_NSP), titleIndex, false);
|
|
if (!dumpName)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to generate output dump name!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s", EXEFS_DUMP_PATH, dumpName);
|
|
|
|
if (exeFsContext.idOffset > 0)
|
|
{
|
|
sprintf(strbuf, " (ID offset #%u)", exeFsContext.idOffset);
|
|
strcat(dumpPath, strbuf);
|
|
}
|
|
} else {
|
|
mkdir(cfwDirStr, 0744);
|
|
|
|
// Always use the base application title ID
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%016lX", cfwDirStr, (usePatch ? (patchEntries[titleIndex].titleId & ~APPLICATION_PATCH_BITMASK) : baseAppEntries[titleIndex].titleId) + + exeFsContext.idOffset);
|
|
mkdir(dumpPath, 0744);
|
|
|
|
strcat(dumpPath, "/exefs");
|
|
}
|
|
|
|
mkdir(dumpPath, 0744);
|
|
|
|
strcat(dumpPath, "/");
|
|
size_t cur_len = strlen(dumpPath);
|
|
strcat(dumpPath, exeFsFilename);
|
|
removeIllegalCharacters(dumpPath + cur_len);
|
|
|
|
progressCtx.totalSize = exeFsContext.exefs_entries[fileIndex].file_size;
|
|
convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, MAX_CHARACTERS(progressCtx.totalSizeStr));
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "File size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize);
|
|
breaks++;
|
|
|
|
if (progressCtx.totalSize > freeSpace)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
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(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
removeFile = false;
|
|
goto out;
|
|
} else {
|
|
// Remove the prompt from the screen
|
|
breaks = cur_breaks;
|
|
uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), BG_COLOR_RGB);
|
|
}
|
|
}
|
|
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32)
|
|
{
|
|
// 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
|
|
remove(dumpPath);
|
|
fsdevDeleteDirectoryRecursively(dumpPath);
|
|
|
|
mkdir(dumpPath, 0744);
|
|
sprintf(tmp_idx, "/%02u", splitIndex);
|
|
strcat(dumpPath, tmp_idx);
|
|
}
|
|
|
|
// Start dump process
|
|
dumpStartMsg();
|
|
appletModeOperationWarning();
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
changeHomeButtonBlockStatus(true);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Copying \"%s\"...", exeFsFilename);
|
|
breaks += 2;
|
|
|
|
uiRefreshDisplay();
|
|
|
|
outFile = fopen(dumpPath, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open output file!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
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) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(dumpPath, '/') + 1);
|
|
|
|
uiRefreshDisplay();
|
|
|
|
if (n > (progressCtx.totalSize - progressCtx.curOffset)) n = (progressCtx.totalSize - progressCtx.curOffset);
|
|
|
|
breaks = (progressCtx.line_offset + 2);
|
|
proceed = processNcaCtrSectionBlock(&(exeFsContext.ncmStorage), &(exeFsContext.ncaId), &(exeFsContext.aes_ctx), exeFsContext.exefs_data_offset + exeFsContext.exefs_entries[fileIndex].file_offset + progressCtx.curOffset, dumpBuf, n, false);
|
|
breaks = (progressCtx.line_offset - 2);
|
|
|
|
if (!proceed) break;
|
|
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32 && (progressCtx.curOffset + n) >= ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE))
|
|
{
|
|
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);
|
|
|
|
if (old_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf, 1, old_file_chunk_size, outFile);
|
|
if (write_res != old_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, old_file_chunk_size, progressCtx.curOffset, splitIndex, write_res);
|
|
break;
|
|
}
|
|
}
|
|
|
|
fclose(outFile);
|
|
outFile = NULL;
|
|
|
|
if (new_file_chunk_size > 0 || (progressCtx.curOffset + n) < progressCtx.totalSize)
|
|
{
|
|
char *tmp = strrchr(dumpPath, '/');
|
|
if (tmp != NULL) *tmp = '\0';
|
|
|
|
splitIndex++;
|
|
sprintf(tmp_idx, "/%02u", splitIndex);
|
|
strcat(dumpPath, tmp_idx);
|
|
|
|
outFile = fopen(dumpPath, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to open output file for part #%u!", __func__, splitIndex);
|
|
break;
|
|
}
|
|
|
|
if (new_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf + old_file_chunk_size, 1, new_file_chunk_size, outFile);
|
|
if (write_res != new_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, write_res);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
write_res = fwrite(dumpBuf, 1, n, outFile);
|
|
if (write_res != n)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", __func__, n, progressCtx.curOffset, write_res);
|
|
|
|
if ((progressCtx.curOffset + n) > FAT32_FILESIZE_LIMIT)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 4), FONT_COLOR_RGB, "You're probably using a FAT32 partition. Make sure to enable file splitting.");
|
|
fat32_error = true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
printProgressBar(&progressCtx, true, n);
|
|
|
|
if ((progressCtx.curOffset + n) < progressCtx.totalSize && cancelProcessCheck(&progressCtx))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (progressCtx.curOffset >= progressCtx.totalSize) success = true;
|
|
|
|
// Support empty files
|
|
if (!progressCtx.totalSize)
|
|
{
|
|
uiFill(0, ((progressCtx.line_offset - 2) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(dumpPath, '/') + 1);
|
|
|
|
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, MAX_CHARACTERS(progressCtx.etaInfo));
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Process successfully completed after %s!", progressCtx.etaInfo);
|
|
} else {
|
|
setProgressBarError(&progressCtx);
|
|
if (fat32_error) breaks += 2;
|
|
}
|
|
|
|
out:
|
|
if (outFile) fclose(outFile);
|
|
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32)
|
|
{
|
|
char *tmp = strrchr(dumpPath, '/');
|
|
if (tmp != NULL) *tmp = '\0';
|
|
|
|
if (success)
|
|
{
|
|
// Set archive bit (only for FAT32)
|
|
fsdevSetConcatenationFileAttribute(dumpPath);
|
|
} else {
|
|
if (removeFile) fsdevDeleteDirectoryRecursively(dumpPath);
|
|
}
|
|
} else {
|
|
if (!success && removeFile) remove(dumpPath);
|
|
}
|
|
|
|
if (dumpName) free(dumpName);
|
|
|
|
breaks += 2;
|
|
|
|
changeHomeButtonBlockStatus(false);
|
|
|
|
return success;
|
|
}
|
|
|
|
bool recursiveDumpRomFsFile(u32 file_offset, char *romfs_path, char *output_path, progress_ctx_t *progressCtx, bool usePatch, bool isFat32)
|
|
{
|
|
if ((!usePatch && (!romFsContext.romfs_filetable_size || file_offset > romFsContext.romfs_filetable_size || !romFsContext.romfs_file_entries)) || (usePatch && (!bktrContext.romfs_filetable_size || file_offset > bktrContext.romfs_filetable_size || !bktrContext.romfs_file_entries)) || !romfs_path || !output_path || !progressCtx)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to parse file entry from RomFS section!", __func__);
|
|
return false;
|
|
}
|
|
|
|
size_t orig_romfs_path_len = strlen(romfs_path);
|
|
size_t orig_output_path_len = strlen(output_path);
|
|
|
|
u64 n = DUMP_BUFFER_SIZE;
|
|
FILE *outFile = NULL;
|
|
u8 splitIndex = 0;
|
|
bool proceed = true, success = false, fat32_error = false;
|
|
|
|
// Used to overcome issues related to the max entry count per directory in FAT32
|
|
int dir_limit_counter = -1;
|
|
|
|
u32 romfs_file_offset = file_offset;
|
|
romfs_file *entry = NULL;
|
|
|
|
u64 off = 0;
|
|
|
|
size_t write_res;
|
|
|
|
char tmp_idx[16];
|
|
|
|
memset(dumpBuf, 0, DUMP_BUFFER_SIZE);
|
|
|
|
while(romfs_file_offset != ROMFS_ENTRY_EMPTY)
|
|
{
|
|
romfs_path[orig_romfs_path_len] = '\0';
|
|
output_path[orig_output_path_len] = '\0';
|
|
|
|
n = DUMP_BUFFER_SIZE;
|
|
splitIndex = 0;
|
|
|
|
entry = (!usePatch ? (romfs_file*)((u8*)romFsContext.romfs_file_entries + romfs_file_offset) : (romfs_file*)((u8*)bktrContext.romfs_file_entries + romfs_file_offset));
|
|
|
|
// Check if we're dealing with a nameless file
|
|
if (!entry->nameLen)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: file entry without name in RomFS section!", __func__);
|
|
break;
|
|
}
|
|
|
|
if (dir_limit_counter >= 0)
|
|
{
|
|
sprintf(tmp_idx, "_%d", dir_limit_counter);
|
|
} else {
|
|
tmp_idx[0] = '\0';
|
|
}
|
|
|
|
if ((orig_romfs_path_len + 1 + entry->nameLen) >= (NAME_BUF_LEN * 2) || (orig_output_path_len + strlen(tmp_idx) + 1 + entry->nameLen) >= (NAME_BUF_LEN * 2))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: RomFS section file path is too long!", __func__);
|
|
break;
|
|
}
|
|
|
|
// Generate current path
|
|
strcat(romfs_path, "/");
|
|
strncat(romfs_path, (char*)entry->name, entry->nameLen);
|
|
|
|
if (dir_limit_counter >= 0) strcat(output_path, tmp_idx);
|
|
strcat(output_path, "/");
|
|
strncat(output_path, (char*)entry->name, entry->nameLen);
|
|
removeIllegalCharacters(output_path + orig_output_path_len + strlen(tmp_idx) + 1);
|
|
|
|
if (entry->dataSize > FAT32_FILESIZE_LIMIT && isFat32)
|
|
{
|
|
mkdir(output_path, 0744);
|
|
sprintf(tmp_idx, "/%02u", splitIndex);
|
|
strcat(output_path, tmp_idx);
|
|
}
|
|
|
|
// Start dump process
|
|
uiFill(0, ((progressCtx->line_offset - 4) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 4, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset - 4), FONT_COLOR_RGB, "Copying \"romfs:%s\"...", romfs_path);
|
|
|
|
outFile = fopen(output_path, "wb");
|
|
if (!outFile)
|
|
{
|
|
if (entry->dataSize <= FAT32_FILESIZE_LIMIT || !isFat32)
|
|
{
|
|
output_path[orig_output_path_len] = '\0';
|
|
|
|
dir_limit_counter++;
|
|
sprintf(tmp_idx, "_%d", dir_limit_counter);
|
|
strcat(output_path, tmp_idx);
|
|
mkdir(output_path, 0744);
|
|
|
|
strcat(output_path, "/");
|
|
strncat(output_path, (char*)entry->name, entry->nameLen);
|
|
removeIllegalCharacters(output_path + orig_output_path_len + strlen(tmp_idx) + 1);
|
|
|
|
outFile = fopen(output_path, "wb");
|
|
}
|
|
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to open output file \"%s\"!", __func__, output_path);
|
|
break;
|
|
}
|
|
}
|
|
|
|
for(off = 0; off < entry->dataSize; off += n, progressCtx->curOffset += n)
|
|
{
|
|
uiFill(0, ((progressCtx->line_offset - 2) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset - 2), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(output_path, '/') + 1);
|
|
|
|
uiRefreshDisplay();
|
|
|
|
if (n > (entry->dataSize - off)) n = (entry->dataSize - off);
|
|
|
|
breaks = (progressCtx->line_offset + 2);
|
|
|
|
if (!usePatch)
|
|
{
|
|
proceed = processNcaCtrSectionBlock(&(romFsContext.ncmStorage), &(romFsContext.ncaId), &(romFsContext.aes_ctx), romFsContext.romfs_filedata_offset + entry->dataOff + off, dumpBuf, n, false);
|
|
} else {
|
|
proceed = readBktrSectionBlock(bktrContext.romfs_filedata_offset + entry->dataOff + off, dumpBuf, n);
|
|
}
|
|
|
|
breaks = (progressCtx->line_offset - 4);
|
|
|
|
if (!proceed) break;
|
|
|
|
if (entry->dataSize > FAT32_FILESIZE_LIMIT && isFat32 && (off + n) >= ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE))
|
|
{
|
|
u64 new_file_chunk_size = ((off + n) - ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE));
|
|
u64 old_file_chunk_size = (n - new_file_chunk_size);
|
|
|
|
if (old_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf, 1, old_file_chunk_size, outFile);
|
|
if (write_res != old_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, old_file_chunk_size, off, splitIndex, write_res);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
fclose(outFile);
|
|
outFile = NULL;
|
|
|
|
if (new_file_chunk_size > 0 || (off + n) < entry->dataSize)
|
|
{
|
|
char *tmp = strrchr(output_path, '/');
|
|
if (tmp != NULL) *tmp = '\0';
|
|
|
|
splitIndex++;
|
|
sprintf(tmp_idx, "/%02u", splitIndex);
|
|
strcat(output_path, tmp_idx);
|
|
|
|
outFile = fopen(output_path, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to open output file for part #%u!", __func__, splitIndex);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
if (new_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf + old_file_chunk_size, 1, new_file_chunk_size, outFile);
|
|
if (write_res != new_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, new_file_chunk_size, off + old_file_chunk_size, splitIndex, write_res);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
write_res = fwrite(dumpBuf, 1, n, outFile);
|
|
if (write_res != n)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", __func__, n, off, write_res);
|
|
|
|
if ((off + n) > FAT32_FILESIZE_LIMIT)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 4), FONT_COLOR_RGB, "You're probably using a FAT32 partition. Make sure to enable file splitting.");
|
|
fat32_error = true;
|
|
}
|
|
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
printProgressBar(progressCtx, true, n);
|
|
|
|
if (((off + n) < entry->dataSize || (progressCtx->curOffset + n) < progressCtx->totalSize) && cancelProcessCheck(progressCtx))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
proceed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (outFile)
|
|
{
|
|
fclose(outFile);
|
|
outFile = NULL;
|
|
}
|
|
|
|
if (!proceed || off < entry->dataSize) break;
|
|
|
|
// Support empty files
|
|
if (!entry->dataSize)
|
|
{
|
|
uiFill(0, ((progressCtx->line_offset - 2) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset - 2), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(output_path, '/') + 1);
|
|
|
|
if (progressCtx->totalSize == entry->dataSize) progressCtx->progress = 100;
|
|
|
|
printProgressBar(progressCtx, false, 0);
|
|
}
|
|
|
|
// Set archive bit (only for FAT32)
|
|
if (entry->dataSize > FAT32_FILESIZE_LIMIT && isFat32)
|
|
{
|
|
char *tmp = strrchr(output_path, '/');
|
|
if (tmp != NULL) *tmp = '\0';
|
|
fsdevSetConcatenationFileAttribute(output_path);
|
|
}
|
|
|
|
romfs_file_offset = entry->sibling;
|
|
if (romfs_file_offset == ROMFS_ENTRY_EMPTY) success = true;
|
|
}
|
|
|
|
if (!success)
|
|
{
|
|
breaks = (progressCtx->line_offset + 2);
|
|
if (fat32_error) breaks += 2;
|
|
}
|
|
|
|
romfs_path[orig_romfs_path_len] = '\0';
|
|
output_path[orig_output_path_len] = '\0';
|
|
|
|
return success;
|
|
}
|
|
|
|
bool recursiveDumpRomFsDir(u32 dir_offset, char *romfs_path, char *output_path, progress_ctx_t *progressCtx, bool usePatch, bool dumpSiblingDir, bool isFat32)
|
|
{
|
|
if ((!usePatch && (!romFsContext.romfs_dirtable_size || dir_offset > romFsContext.romfs_dirtable_size || !romFsContext.romfs_dir_entries || !romFsContext.romfs_filetable_size || !romFsContext.romfs_file_entries)) || (usePatch && (!bktrContext.romfs_dirtable_size || dir_offset > bktrContext.romfs_dirtable_size || !bktrContext.romfs_dir_entries || !bktrContext.romfs_filetable_size || !bktrContext.romfs_file_entries)) || !romfs_path || !output_path || !progressCtx)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to parse directory entry from RomFS section!", __func__);
|
|
return false;
|
|
}
|
|
|
|
size_t orig_romfs_path_len = strlen(romfs_path);
|
|
size_t orig_output_path_len = strlen(output_path);
|
|
|
|
romfs_dir *entry = (!usePatch ? (romfs_dir*)((u8*)romFsContext.romfs_dir_entries + dir_offset) : (romfs_dir*)((u8*)bktrContext.romfs_dir_entries + dir_offset));
|
|
|
|
// Check if we're dealing with a nameless directory that's not the root directory
|
|
if (!entry->nameLen && dir_offset > 0)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: directory entry without name in RomFS section!", __func__);
|
|
return false;
|
|
}
|
|
|
|
if ((orig_romfs_path_len + 1 + entry->nameLen) >= (NAME_BUF_LEN * 2) || (orig_output_path_len + 1 + entry->nameLen) >= (NAME_BUF_LEN * 2))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx->line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: RomFS section directory path is too long!", __func__);
|
|
return false;
|
|
}
|
|
|
|
// Generate current path
|
|
if (entry->nameLen)
|
|
{
|
|
strcat(romfs_path, "/");
|
|
strncat(romfs_path, (char*)entry->name, entry->nameLen);
|
|
|
|
strcat(output_path, "/");
|
|
strncat(output_path, (char*)entry->name, entry->nameLen);
|
|
removeIllegalCharacters(output_path + orig_output_path_len + 1);
|
|
mkdir(output_path, 0744);
|
|
}
|
|
|
|
if (entry->childFile != ROMFS_ENTRY_EMPTY)
|
|
{
|
|
if (!recursiveDumpRomFsFile(entry->childFile, romfs_path, output_path, progressCtx, usePatch, isFat32))
|
|
{
|
|
romfs_path[orig_romfs_path_len] = '\0';
|
|
output_path[orig_output_path_len] = '\0';
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (entry->childDir != ROMFS_ENTRY_EMPTY)
|
|
{
|
|
if (!recursiveDumpRomFsDir(entry->childDir, romfs_path, output_path, progressCtx, usePatch, true, isFat32))
|
|
{
|
|
romfs_path[orig_romfs_path_len] = '\0';
|
|
output_path[orig_output_path_len] = '\0';
|
|
return false;
|
|
}
|
|
}
|
|
|
|
romfs_path[orig_romfs_path_len] = '\0';
|
|
output_path[orig_output_path_len] = '\0';
|
|
|
|
if (dumpSiblingDir && entry->sibling != ROMFS_ENTRY_EMPTY)
|
|
{
|
|
if (!recursiveDumpRomFsDir(entry->sibling, romfs_path, output_path, progressCtx, usePatch, true, isFat32)) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool dumpRomFsSectionData(u32 titleIndex, selectedRomFsType curRomFsType, ncaFsOptions *romFsDumpCfg)
|
|
{
|
|
if (!romFsDumpCfg)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid RomFS configuration struct!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
bool isFat32 = romFsDumpCfg->isFat32;
|
|
bool useLayeredFSDir = romFsDumpCfg->useLayeredFSDir;
|
|
|
|
progress_ctx_t progressCtx;
|
|
memset(&progressCtx, 0, sizeof(progress_ctx_t));
|
|
|
|
char *dumpName = NULL;
|
|
char romFsPath[NAME_BUF_LEN * 2] = {'\0'}, dumpPath[NAME_BUF_LEN * 2] = {'\0'};
|
|
|
|
bool success = false;
|
|
|
|
if ((curRomFsType == ROMFS_TYPE_APP && !titleAppCount) || (curRomFsType == ROMFS_TYPE_PATCH && !titlePatchCount) || (curRomFsType == ROMFS_TYPE_ADDON && !titleAddOnCount))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid title count!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if ((curRomFsType == ROMFS_TYPE_APP && titleIndex > (titleAppCount - 1)) || (curRomFsType == ROMFS_TYPE_PATCH && titleIndex > (titlePatchCount - 1)) || (curRomFsType == ROMFS_TYPE_ADDON && titleIndex > (titleAddOnCount - 1)))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid title index!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (!useLayeredFSDir)
|
|
{
|
|
dumpName = generateNSPDumpName((curRomFsType == ROMFS_TYPE_APP ? DUMP_APP_NSP : (curRomFsType == ROMFS_TYPE_PATCH ? DUMP_PATCH_NSP : DUMP_ADDON_NSP)), titleIndex, false);
|
|
if (!dumpName)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to generate output dump name!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Retrieve RomFS from Program NCA
|
|
if (readNcaRomFsSection(titleIndex, curRomFsType, -1) != 0)
|
|
{
|
|
free(dumpName);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
// Calculate total dump size
|
|
if (!calculateRomFsFullExtractedSize((curRomFsType == ROMFS_TYPE_PATCH), &(progressCtx.totalSize))) goto out;
|
|
|
|
convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, MAX_CHARACTERS(progressCtx.totalSizeStr));
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Extracted RomFS dump size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize);
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
if (progressCtx.totalSize > freeSpace)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Generate output path
|
|
if (!useLayeredFSDir)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s", ROMFS_DUMP_PATH, dumpName);
|
|
|
|
if ((curRomFsType != ROMFS_TYPE_PATCH && romFsContext.idOffset > 0) || (curRomFsType == ROMFS_TYPE_PATCH && bktrContext.idOffset > 0))
|
|
{
|
|
sprintf(strbuf, " (ID offset #%u)", (curRomFsType != ROMFS_TYPE_PATCH ? romFsContext.idOffset : bktrContext.idOffset));
|
|
strcat(dumpPath, strbuf);
|
|
}
|
|
} else {
|
|
mkdir(cfwDirStr, 0744);
|
|
|
|
// Base applications and updates: always use the base application title ID
|
|
// DLCs: use DLC title ID
|
|
u64 titleId = (curRomFsType == ROMFS_TYPE_APP ? baseAppEntries[titleIndex].titleId : (curRomFsType == ROMFS_TYPE_PATCH ? (patchEntries[titleIndex].titleId & ~APPLICATION_PATCH_BITMASK) : addOnEntries[titleIndex].titleId));
|
|
titleId += (curRomFsType != ROMFS_TYPE_PATCH ? romFsContext.idOffset : bktrContext.idOffset);
|
|
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%016lX", cfwDirStr, titleId);
|
|
mkdir(dumpPath, 0744);
|
|
|
|
strcat(dumpPath, "/romfs");
|
|
}
|
|
|
|
mkdir(dumpPath, 0744);
|
|
|
|
// Start dump process
|
|
breaks++;
|
|
dumpStartMsg();
|
|
appletModeOperationWarning();
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
changeHomeButtonBlockStatus(true);
|
|
|
|
progressCtx.line_offset = (breaks + 4);
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start));
|
|
|
|
success = recursiveDumpRomFsDir(0, romFsPath, dumpPath, &progressCtx, (curRomFsType == ROMFS_TYPE_PATCH), true, isFat32);
|
|
|
|
if (success)
|
|
{
|
|
breaks = (progressCtx.line_offset + 2);
|
|
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now));
|
|
progressCtx.now -= progressCtx.start;
|
|
|
|
formatETAString(progressCtx.now, progressCtx.etaInfo, MAX_CHARACTERS(progressCtx.etaInfo));
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Process successfully completed after %s!", progressCtx.etaInfo);
|
|
} else {
|
|
setProgressBarError(&progressCtx);
|
|
removeDirectoryWithVerbose(dumpPath, "Deleting output directory. Please wait...");
|
|
}
|
|
|
|
out:
|
|
if (curRomFsType == ROMFS_TYPE_PATCH) freeBktrContext();
|
|
|
|
freeRomFsContext();
|
|
|
|
if (dumpName) free(dumpName);
|
|
|
|
breaks += 2;
|
|
|
|
changeHomeButtonBlockStatus(false);
|
|
|
|
return success;
|
|
}
|
|
|
|
bool dumpFileFromRomFsSection(u32 titleIndex, u32 file_offset, selectedRomFsType curRomFsType, ncaFsOptions *romFsDumpCfg)
|
|
{
|
|
if (!romFsDumpCfg)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid RomFS configuration struct!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
bool isFat32 = romFsDumpCfg->isFat32;
|
|
bool useLayeredFSDir = romFsDumpCfg->useLayeredFSDir;
|
|
|
|
if ((curRomFsType != ROMFS_TYPE_PATCH && (!romFsContext.romfs_filetable_size || file_offset > romFsContext.romfs_filetable_size || !romFsContext.romfs_file_entries)) || (curRomFsType == ROMFS_TYPE_PATCH && (!bktrContext.romfs_filetable_size || file_offset > bktrContext.romfs_filetable_size || !bktrContext.romfs_file_entries)) || (curRomFsType == ROMFS_TYPE_APP && titleIndex > (titleAppCount - 1)) || (curRomFsType == ROMFS_TYPE_PATCH && titleIndex > (titlePatchCount - 1)) || (curRomFsType == ROMFS_TYPE_ADDON && titleIndex > (titleAddOnCount - 1)))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to parse file entry from RomFS section!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
u64 n = DUMP_BUFFER_SIZE;
|
|
FILE *outFile = NULL;
|
|
u8 splitIndex = 0;
|
|
size_t write_res;
|
|
bool proceed = true, success = false, fat32_error = false, removeFile = true;
|
|
|
|
char tmp_idx[5];
|
|
char *dumpName = NULL;
|
|
char dumpPath[NAME_BUF_LEN * 2] = {'\0'};
|
|
|
|
progress_ctx_t progressCtx;
|
|
memset(&progressCtx, 0, sizeof(progress_ctx_t));
|
|
|
|
memset(dumpBuf, 0, DUMP_BUFFER_SIZE);
|
|
|
|
romfs_file *entry = (curRomFsType != ROMFS_TYPE_PATCH ? (romfs_file*)((u8*)romFsContext.romfs_file_entries + file_offset) : (romfs_file*)((u8*)bktrContext.romfs_file_entries + file_offset));
|
|
|
|
// Check if we're dealing with a nameless file
|
|
if (!entry->nameLen)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: file entry without name in RomFS section!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
progressCtx.totalSize = entry->dataSize;
|
|
convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, MAX_CHARACTERS(progressCtx.totalSizeStr));
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "File size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize);
|
|
breaks++;
|
|
|
|
if (progressCtx.totalSize > freeSpace)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
breaks++;
|
|
|
|
// Generate output path
|
|
if (!useLayeredFSDir)
|
|
{
|
|
dumpName = generateNSPDumpName((curRomFsType == ROMFS_TYPE_APP ? DUMP_APP_NSP : (curRomFsType == ROMFS_TYPE_PATCH ? DUMP_PATCH_NSP : DUMP_ADDON_NSP)), titleIndex, false);
|
|
if (!dumpName)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to generate output dump name!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s", ROMFS_DUMP_PATH, dumpName);
|
|
|
|
if ((curRomFsType != ROMFS_TYPE_PATCH && romFsContext.idOffset > 0) || (curRomFsType == ROMFS_TYPE_PATCH && bktrContext.idOffset > 0))
|
|
{
|
|
sprintf(strbuf, " (ID offset #%u)", (curRomFsType != ROMFS_TYPE_PATCH ? romFsContext.idOffset : bktrContext.idOffset));
|
|
strcat(dumpPath, strbuf);
|
|
}
|
|
} else {
|
|
mkdir(cfwDirStr, 0744);
|
|
|
|
// Base applications and updates: always use the base application title ID
|
|
// DLCs: use DLC title ID
|
|
u64 titleId = (curRomFsType == ROMFS_TYPE_APP ? baseAppEntries[titleIndex].titleId : (curRomFsType == ROMFS_TYPE_PATCH ? (patchEntries[titleIndex].titleId & ~APPLICATION_PATCH_BITMASK) : addOnEntries[titleIndex].titleId));
|
|
titleId += (curRomFsType != ROMFS_TYPE_PATCH ? romFsContext.idOffset : bktrContext.idOffset);
|
|
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%016lX", cfwDirStr, titleId);
|
|
mkdir(dumpPath, 0744);
|
|
|
|
strcat(dumpPath, "/romfs");
|
|
}
|
|
|
|
mkdir(dumpPath, 0744);
|
|
|
|
// Create subdirectories
|
|
char *tmp1 = NULL, *tmp2 = NULL;
|
|
size_t cur_len;
|
|
|
|
tmp1 = strchr(curRomFsPath, '/');
|
|
|
|
while(tmp1 != NULL)
|
|
{
|
|
tmp1++;
|
|
|
|
if (!strlen(tmp1)) break;
|
|
|
|
strcat(dumpPath, "/");
|
|
|
|
cur_len = strlen(dumpPath);
|
|
|
|
tmp2 = strchr(tmp1, '/');
|
|
if (tmp2 != NULL)
|
|
{
|
|
strncat(dumpPath, tmp1, tmp2 - tmp1);
|
|
|
|
tmp1 = tmp2;
|
|
} else {
|
|
strcat(dumpPath, tmp1);
|
|
|
|
tmp1 = NULL;
|
|
}
|
|
|
|
removeIllegalCharacters(dumpPath + cur_len);
|
|
|
|
mkdir(dumpPath, 0744);
|
|
}
|
|
|
|
strcat(dumpPath, "/");
|
|
cur_len = strlen(dumpPath);
|
|
strncat(dumpPath, (char*)entry->name, entry->nameLen);
|
|
removeIllegalCharacters(dumpPath + cur_len);
|
|
|
|
// 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(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
removeFile = false;
|
|
goto out;
|
|
} else {
|
|
// Remove the prompt from the screen
|
|
breaks = cur_breaks;
|
|
uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), BG_COLOR_RGB);
|
|
}
|
|
}
|
|
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32)
|
|
{
|
|
// 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
|
|
remove(dumpPath);
|
|
fsdevDeleteDirectoryRecursively(dumpPath);
|
|
|
|
mkdir(dumpPath, 0744);
|
|
sprintf(tmp_idx, "/%02u", splitIndex);
|
|
strcat(dumpPath, tmp_idx);
|
|
}
|
|
|
|
// Start dump process
|
|
dumpStartMsg();
|
|
appletModeOperationWarning();
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
changeHomeButtonBlockStatus(true);
|
|
|
|
if (strlen(curRomFsPath) > 1)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Copying \"romfs:%s/%.*s\"...", curRomFsPath, entry->nameLen, entry->name);
|
|
} else {
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Copying \"romfs:/%.*s\"...", entry->nameLen, entry->name);
|
|
}
|
|
|
|
breaks += 2;
|
|
|
|
outFile = fopen(dumpPath, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open output file \"%s\"!", __func__, dumpPath);
|
|
goto out;
|
|
}
|
|
|
|
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) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(dumpPath, '/') + 1);
|
|
|
|
uiRefreshDisplay();
|
|
|
|
if (n > (progressCtx.totalSize - progressCtx.curOffset)) n = (progressCtx.totalSize - progressCtx.curOffset);
|
|
|
|
breaks = (progressCtx.line_offset + 2);
|
|
|
|
if (curRomFsType != ROMFS_TYPE_PATCH)
|
|
{
|
|
proceed = processNcaCtrSectionBlock(&(romFsContext.ncmStorage), &(romFsContext.ncaId), &(romFsContext.aes_ctx), romFsContext.romfs_filedata_offset + entry->dataOff + progressCtx.curOffset, dumpBuf, n, false);
|
|
} else {
|
|
proceed = readBktrSectionBlock(bktrContext.romfs_filedata_offset + entry->dataOff + progressCtx.curOffset, dumpBuf, n);
|
|
}
|
|
|
|
breaks = (progressCtx.line_offset - 2);
|
|
|
|
if (!proceed) break;
|
|
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32 && (progressCtx.curOffset + n) >= ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE))
|
|
{
|
|
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);
|
|
|
|
if (old_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf, 1, old_file_chunk_size, outFile);
|
|
if (write_res != old_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, old_file_chunk_size, progressCtx.curOffset, splitIndex, write_res);
|
|
break;
|
|
}
|
|
}
|
|
|
|
fclose(outFile);
|
|
outFile = NULL;
|
|
|
|
if (new_file_chunk_size > 0 || (progressCtx.curOffset + n) < progressCtx.totalSize)
|
|
{
|
|
char *tmp = strrchr(dumpPath, '/');
|
|
if (tmp != NULL) *tmp = '\0';
|
|
|
|
splitIndex++;
|
|
sprintf(tmp_idx, "/%02u", splitIndex);
|
|
strcat(dumpPath, tmp_idx);
|
|
|
|
outFile = fopen(dumpPath, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to open output file for part #%u!", __func__, splitIndex);
|
|
break;
|
|
}
|
|
|
|
if (new_file_chunk_size > 0)
|
|
{
|
|
write_res = fwrite(dumpBuf + old_file_chunk_size, 1, new_file_chunk_size, outFile);
|
|
if (write_res != new_file_chunk_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", __func__, new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, write_res);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
write_res = fwrite(dumpBuf, 1, n, outFile);
|
|
if (write_res != n)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "%s: failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", __func__, n, progressCtx.curOffset, write_res);
|
|
|
|
if ((progressCtx.curOffset + n) > FAT32_FILESIZE_LIMIT)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 4), FONT_COLOR_RGB, "You're probably using a FAT32 partition. Make sure to enable file splitting.");
|
|
fat32_error = true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
printProgressBar(&progressCtx, true, n);
|
|
|
|
if ((progressCtx.curOffset + n) < progressCtx.totalSize && cancelProcessCheck(&progressCtx))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset + 2), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (progressCtx.curOffset >= progressCtx.totalSize) success = true;
|
|
|
|
// Support empty files
|
|
if (!progressCtx.totalSize)
|
|
{
|
|
uiFill(0, ((progressCtx.line_offset - 2) * LINE_HEIGHT) + 8, FB_WIDTH, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(progressCtx.line_offset - 2), FONT_COLOR_RGB, "Output file: \"%s\".", strrchr(dumpPath, '/') + 1);
|
|
|
|
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, MAX_CHARACTERS(progressCtx.etaInfo));
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Process successfully completed after %s!", progressCtx.etaInfo);
|
|
} else {
|
|
setProgressBarError(&progressCtx);
|
|
if (fat32_error) breaks += 2;
|
|
}
|
|
|
|
out:
|
|
if (outFile) fclose(outFile);
|
|
|
|
if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32)
|
|
{
|
|
char *tmp = strrchr(dumpPath, '/');
|
|
if (tmp != NULL) *tmp = '\0';
|
|
|
|
if (success)
|
|
{
|
|
// Set archive bit (only for FAT32)
|
|
fsdevSetConcatenationFileAttribute(dumpPath);
|
|
} else {
|
|
if (removeFile) fsdevDeleteDirectoryRecursively(dumpPath);
|
|
}
|
|
} else {
|
|
if (!success && removeFile) remove(dumpPath);
|
|
}
|
|
|
|
if (dumpName) free(dumpName);
|
|
|
|
breaks += 2;
|
|
|
|
changeHomeButtonBlockStatus(false);
|
|
|
|
return success;
|
|
}
|
|
|
|
bool dumpCurrentDirFromRomFsSection(u32 titleIndex, selectedRomFsType curRomFsType, ncaFsOptions *romFsDumpCfg)
|
|
{
|
|
if (!romFsDumpCfg)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid RomFS configuration struct!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
bool isFat32 = romFsDumpCfg->isFat32;
|
|
bool useLayeredFSDir = romFsDumpCfg->useLayeredFSDir;
|
|
|
|
progress_ctx_t progressCtx;
|
|
memset(&progressCtx, 0, sizeof(progress_ctx_t));
|
|
|
|
char *dumpName = NULL;
|
|
char romFsPath[NAME_BUF_LEN * 2] = {'\0'}, dumpPath[NAME_BUF_LEN * 2] = {'\0'};
|
|
|
|
bool success = false;
|
|
|
|
if ((curRomFsType == ROMFS_TYPE_APP && !titleAppCount) || (curRomFsType == ROMFS_TYPE_PATCH && !titlePatchCount) || (curRomFsType == ROMFS_TYPE_ADDON && !titleAddOnCount))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid title count!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if ((curRomFsType == ROMFS_TYPE_APP && titleIndex > (titleAppCount - 1)) || (curRomFsType == ROMFS_TYPE_PATCH && titleIndex > (titlePatchCount - 1)) || (curRomFsType == ROMFS_TYPE_ADDON && titleIndex > (titleAddOnCount - 1)))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid title index!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (!useLayeredFSDir)
|
|
{
|
|
dumpName = generateNSPDumpName((curRomFsType == ROMFS_TYPE_APP ? DUMP_APP_NSP : (curRomFsType == ROMFS_TYPE_PATCH ? DUMP_PATCH_NSP : DUMP_ADDON_NSP)), titleIndex, false);
|
|
if (!dumpName)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to generate output dump name!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Calculate total dump size
|
|
if (!calculateRomFsExtractedDirSize(curRomFsDirOffset, (curRomFsType == ROMFS_TYPE_PATCH), &(progressCtx.totalSize))) goto out;
|
|
|
|
convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, MAX_CHARACTERS(progressCtx.totalSizeStr));
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Extracted RomFS directory size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize);
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
if (progressCtx.totalSize > freeSpace)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if (strlen(curRomFsPath) > 1)
|
|
{
|
|
// Copy the whole current path and remove the last element (current directory) from it
|
|
// It will be re-added later
|
|
snprintf(romFsPath, MAX_CHARACTERS(romFsPath), curRomFsPath);
|
|
char *slash = strrchr(romFsPath, '/');
|
|
if (slash) *slash = '\0';
|
|
}
|
|
|
|
// Generate output path
|
|
if (!useLayeredFSDir)
|
|
{
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s", ROMFS_DUMP_PATH, dumpName);
|
|
|
|
if ((curRomFsType != ROMFS_TYPE_PATCH && romFsContext.idOffset > 0) || (curRomFsType == ROMFS_TYPE_PATCH && bktrContext.idOffset > 0))
|
|
{
|
|
sprintf(strbuf, " (ID offset #%u)", (curRomFsType != ROMFS_TYPE_PATCH ? romFsContext.idOffset : bktrContext.idOffset));
|
|
strcat(dumpPath, strbuf);
|
|
}
|
|
} else {
|
|
mkdir(cfwDirStr, 0744);
|
|
|
|
// Base applications and updates: always use the base application title ID
|
|
// DLCs: use DLC title ID
|
|
u64 titleId = (curRomFsType == ROMFS_TYPE_APP ? baseAppEntries[titleIndex].titleId : (curRomFsType == ROMFS_TYPE_PATCH ? (patchEntries[titleIndex].titleId & ~APPLICATION_PATCH_BITMASK) : addOnEntries[titleIndex].titleId));
|
|
titleId += (curRomFsType != ROMFS_TYPE_PATCH ? romFsContext.idOffset : bktrContext.idOffset);
|
|
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%016lX", cfwDirStr, titleId);
|
|
mkdir(dumpPath, 0744);
|
|
|
|
strcat(dumpPath, "/romfs");
|
|
}
|
|
|
|
mkdir(dumpPath, 0744);
|
|
|
|
// Create subdirectories
|
|
char *tmp1 = NULL, *tmp2 = NULL;
|
|
size_t cur_len;
|
|
|
|
tmp1 = strchr(curRomFsPath, '/');
|
|
|
|
while(tmp1 != NULL)
|
|
{
|
|
tmp1++;
|
|
|
|
if (!strlen(tmp1)) break;
|
|
|
|
tmp2 = strchr(tmp1, '/');
|
|
if (tmp2 != NULL)
|
|
{
|
|
strcat(dumpPath, "/");
|
|
|
|
cur_len = strlen(dumpPath);
|
|
|
|
strncat(dumpPath, tmp1, tmp2 - tmp1);
|
|
|
|
removeIllegalCharacters(dumpPath + cur_len);
|
|
|
|
mkdir(dumpPath, 0744);
|
|
|
|
tmp1 = tmp2;
|
|
} else {
|
|
// Skip last entry
|
|
tmp1 = NULL;
|
|
}
|
|
}
|
|
|
|
// Start dump process
|
|
breaks++;
|
|
dumpStartMsg();
|
|
appletModeOperationWarning();
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
changeHomeButtonBlockStatus(true);
|
|
|
|
progressCtx.line_offset = (breaks + 4);
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start));
|
|
|
|
success = recursiveDumpRomFsDir(curRomFsDirOffset, romFsPath, dumpPath, &progressCtx, (curRomFsType == ROMFS_TYPE_PATCH), false, isFat32);
|
|
|
|
if (success)
|
|
{
|
|
breaks = (progressCtx.line_offset + 2);
|
|
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now));
|
|
progressCtx.now -= progressCtx.start;
|
|
|
|
formatETAString(progressCtx.now, progressCtx.etaInfo, MAX_CHARACTERS(progressCtx.etaInfo));
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Process successfully completed after %s!", progressCtx.etaInfo);
|
|
} else {
|
|
setProgressBarError(&progressCtx);
|
|
removeDirectoryWithVerbose(dumpPath, "Deleting output directory. Please wait...");
|
|
}
|
|
|
|
out:
|
|
if (dumpName) free(dumpName);
|
|
|
|
breaks += 2;
|
|
|
|
changeHomeButtonBlockStatus(false);
|
|
|
|
return success;
|
|
}
|
|
|
|
bool dumpGameCardCertificate()
|
|
{
|
|
u32 crc = 0;
|
|
Result result;
|
|
bool success = false;
|
|
FILE *outFile = NULL;
|
|
char dumpPath[NAME_BUF_LEN] = {'\0'};
|
|
size_t write_res;
|
|
|
|
memset(dumpBuf, 0, DUMP_BUFFER_SIZE);
|
|
|
|
char *dumpName = generateGameCardDumpName(false);
|
|
if (!dumpName)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to generate output dump name!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Dumping gamecard certificate. Please wait.");
|
|
breaks++;
|
|
|
|
appletModeOperationWarning();
|
|
breaks++;
|
|
|
|
uiRefreshDisplay();
|
|
|
|
result = openGameCardStoragePartition(ISTORAGE_PARTITION_NORMAL);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open IStorage partition #0! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
|
|
if (CERT_SIZE > freeSpace)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
result = readGameCardStoragePartition(CERT_OFFSET, dumpBuf, CERT_SIZE);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to read %u bytes long certificate at offset 0x%016lX from IStorage partition #0! (0x%08X)", __func__, CERT_SIZE, CERT_OFFSET, result);
|
|
goto out;
|
|
}
|
|
|
|
// Calculate CRC32
|
|
crc32(dumpBuf, CERT_SIZE, &crc);
|
|
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s - Certificate (%08X).bin", CERT_DUMP_PATH, dumpName, crc);
|
|
|
|
// Check if the dump already exists
|
|
if (checkIfFileExists(dumpPath))
|
|
{
|
|
// Ask the user if they want to proceed anyway
|
|
int cur_breaks = breaks;
|
|
|
|
if (!yesNoPrompt("You have already dumped this content. Do you wish to proceed anyway?"))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
goto out;
|
|
} else {
|
|
// Remove the prompt from the screen
|
|
breaks = cur_breaks;
|
|
uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), BG_COLOR_RGB);
|
|
}
|
|
}
|
|
|
|
outFile = fopen(dumpPath, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open output file \"%s\"!", __func__, dumpPath);
|
|
goto out;
|
|
}
|
|
|
|
write_res = fwrite(dumpBuf, 1, CERT_SIZE, outFile);
|
|
if (write_res != CERT_SIZE)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to write %u bytes certificate data! (wrote %lu bytes)", __func__, CERT_SIZE, write_res);
|
|
goto out;
|
|
}
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Process successfully completed!");
|
|
breaks++;
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Certificate dumped to: \"%s\".", strrchr(dumpPath, '/' ) + 1);
|
|
|
|
success = true;
|
|
|
|
out:
|
|
if (outFile) fclose(outFile);
|
|
|
|
if (!success && strlen(dumpPath)) remove(dumpPath);
|
|
|
|
closeGameCardStoragePartition();
|
|
|
|
free(dumpName);
|
|
|
|
breaks += 2;
|
|
|
|
return success;
|
|
}
|
|
|
|
bool dumpTicketFromTitle(u32 titleIndex, selectedTicketType curTikType, ticketOptions *tikDumpCfg)
|
|
{
|
|
if (!tikDumpCfg)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid ticket dump configuration struct!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
bool removeConsoleData = tikDumpCfg->removeConsoleData;
|
|
|
|
u32 i = 0;
|
|
Result result;
|
|
|
|
NcmStorageId curStorageId;
|
|
NcmContentMetaType metaType;
|
|
u32 titleCount = 0, ncmTitleIndex = 0;
|
|
|
|
char *dumpName = NULL;
|
|
char dumpPath[NAME_BUF_LEN] = {'\0'};
|
|
|
|
NcmContentInfo *titleContentInfos = NULL;
|
|
u32 titleContentInfoCnt = 0;
|
|
|
|
NcmContentId ncaId;
|
|
char ncaIdStr[SHA256_HASH_SIZE + 1] = {'\0'};
|
|
|
|
NcmContentStorage ncmStorage;
|
|
memset(&ncmStorage, 0, sizeof(NcmContentStorage));
|
|
|
|
u8 ncaHeader[NCA_FULL_HEADER_LENGTH] = {0};
|
|
nca_header_t dec_nca_header;
|
|
|
|
u8 decrypted_nca_keys[NCA_KEY_AREA_SIZE];
|
|
|
|
title_rights_ctx rights_info;
|
|
memset(&rights_info, 0, sizeof(title_rights_ctx));
|
|
|
|
char encTitleKeyStr[0x21] = {'\0'};
|
|
char decTitleKeyStr[0x21] = {'\0'};
|
|
|
|
FILE *outFile = NULL;
|
|
|
|
bool success = false, proceed = true, foundRightsIdAndTik = false, removeFile = false;
|
|
|
|
if (curTikType != TICKET_TYPE_APP && curTikType != TICKET_TYPE_PATCH && curTikType != TICKET_TYPE_ADDON)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid ticket title type!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if ((curTikType == TICKET_TYPE_APP && !baseAppEntries) || (curTikType == TICKET_TYPE_PATCH && !patchEntries) || (curTikType == TICKET_TYPE_ADDON && !addOnEntries))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: title storage ID unavailable!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if ((curTikType == TICKET_TYPE_APP && titleIndex >= titleAppCount) || (curTikType == TICKET_TYPE_PATCH && titleIndex >= titlePatchCount) || (curTikType == TICKET_TYPE_ADDON && titleIndex >= titleAddOnCount))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid title index!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
curStorageId = (curTikType == TICKET_TYPE_APP ? baseAppEntries[titleIndex].storageId : (curTikType == TICKET_TYPE_PATCH ? patchEntries[titleIndex].storageId : addOnEntries[titleIndex].storageId));
|
|
|
|
ncmTitleIndex = (curTikType == TICKET_TYPE_APP ? baseAppEntries[titleIndex].ncmIndex : (curTikType == TICKET_TYPE_PATCH ? patchEntries[titleIndex].ncmIndex : addOnEntries[titleIndex].ncmIndex));
|
|
|
|
metaType = (curTikType == TICKET_TYPE_APP ? NcmContentMetaType_Application : (curTikType == TICKET_TYPE_PATCH ? NcmContentMetaType_Patch : NcmContentMetaType_AddOnContent));
|
|
|
|
if (curStorageId == NcmStorageId_GameCard)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid title storage ID!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if (sizeof(rsa2048_sha256_ticket) > freeSpace)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: not enough free space available in the SD card!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
switch(curStorageId)
|
|
{
|
|
case NcmStorageId_SdCard:
|
|
titleCount = (curTikType == TICKET_TYPE_APP ? sdCardTitleAppCount : (curTikType == TICKET_TYPE_PATCH ? sdCardTitlePatchCount : sdCardTitleAddOnCount));
|
|
break;
|
|
case NcmStorageId_BuiltInUser:
|
|
titleCount = (curTikType == TICKET_TYPE_APP ? emmcTitleAppCount : (curTikType == TICKET_TYPE_PATCH ? emmcTitlePatchCount : emmcTitleAddOnCount));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
dumpName = generateNSPDumpName((nspDumpType)curTikType, titleIndex, false);
|
|
if (!dumpName)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to generate output dump name!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
snprintf(dumpPath, MAX_CHARACTERS(dumpPath), "%s%s.tik", TICKET_PATH, dumpName);
|
|
|
|
// 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(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Process canceled.");
|
|
goto out;
|
|
} else {
|
|
// Remove the prompt from the screen
|
|
breaks = cur_breaks;
|
|
uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), BG_COLOR_RGB);
|
|
}
|
|
}
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Retrieving Rights ID and Ticket for the selected %s...", (curTikType == TICKET_TYPE_APP ? "base application" : (curTikType == TICKET_TYPE_PATCH ? "update" : "DLC")));
|
|
breaks++;
|
|
|
|
appletModeOperationWarning();
|
|
breaks++;
|
|
|
|
uiRefreshDisplay();
|
|
|
|
if (!retrieveContentInfosFromTitle(curStorageId, metaType, titleCount, ncmTitleIndex, &titleContentInfos, &titleContentInfoCnt))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, strbuf);
|
|
goto out;
|
|
}
|
|
|
|
result = ncmOpenContentStorage(&ncmStorage, curStorageId);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: ncmOpenContentStorage failed! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
|
|
for(i = 0; i < titleContentInfoCnt; i++)
|
|
{
|
|
memcpy(&ncaId, &(titleContentInfos[i].content_id), sizeof(NcmContentId));
|
|
convertDataToHexString(titleContentInfos[i].content_id.c, SHA256_HASH_SIZE / 2, ncaIdStr, SHA256_HASH_SIZE + 1);
|
|
|
|
if (!readNcaDataByContentId(&ncmStorage, &ncaId, 0, ncaHeader, NCA_FULL_HEADER_LENGTH))
|
|
{
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to read header from NCA \"%s\"!", __func__, ncaIdStr);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
// Decrypt the NCA header
|
|
proceed = decryptNcaHeader(ncaHeader, NCA_FULL_HEADER_LENGTH, &dec_nca_header, &rights_info, decrypted_nca_keys, true);
|
|
if (!proceed) break;
|
|
|
|
// Check if we hit the right spot
|
|
if (rights_info.has_rights_id && rights_info.retrieved_tik)
|
|
{
|
|
convertDataToHexString(rights_info.enc_titlekey, 0x10, encTitleKeyStr, 0x21);
|
|
convertDataToHexString(rights_info.dec_titlekey, 0x10, decTitleKeyStr, 0x21);
|
|
foundRightsIdAndTik = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!proceed) goto out;
|
|
|
|
if (!foundRightsIdAndTik)
|
|
{
|
|
if (!rights_info.has_rights_id)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: the selected %s doesn't use titlekey crypto! Rights ID field is empty in all the NCAs!", __func__, (curTikType == TICKET_TYPE_APP ? "base application" : (curTikType == TICKET_TYPE_PATCH ? "update" : "DLC")));
|
|
goto out;
|
|
}
|
|
|
|
if (rights_info.missing_tik)
|
|
{
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: the selected %s uses titlekey crypto, but no ticket for it is available! This is probably a pre-install.", __func__, (curTikType == TICKET_TYPE_APP ? "base application" : (curTikType == TICKET_TYPE_PATCH ? "update" : "DLC")));
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Rights ID: \"%s\".", rights_info.rights_id_str);
|
|
breaks++;
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Ticket type: %s (0x%02X).", (rights_info.tik_data.titlekey_type == ETICKET_TITLEKEY_COMMON ? "common" : (rights_info.tik_data.titlekey_type == ETICKET_TITLEKEY_PERSONALIZED ? "personalized" : "unknown")), rights_info.tik_data.titlekey_type);
|
|
breaks++;
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Encrypted title key: \"%s\".", encTitleKeyStr);
|
|
breaks++;
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Decrypted title key: \"%s\".", decTitleKeyStr);
|
|
breaks += 2;
|
|
|
|
uiRefreshDisplay();
|
|
|
|
// Only mess with the ticket data if removeConsoleData is true and if we're dealing with a personalized ticket (checked in removeConsoleDataFromTicket())
|
|
if (removeConsoleData) removeConsoleDataFromTicket(&rights_info);
|
|
|
|
outFile = fopen(dumpPath, "wb");
|
|
if (!outFile)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open output file \"%s\"!", __func__, dumpPath);
|
|
goto out;
|
|
}
|
|
|
|
size_t wr = fwrite(&(rights_info.tik_data), 1, sizeof(rsa2048_sha256_ticket), outFile);
|
|
if (wr != sizeof(rsa2048_sha256_ticket))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to write %u bytes long ticket data to \"%s\"! Wrote %lu bytes.", __func__, sizeof(rsa2048_sha256_ticket), dumpPath, wr);
|
|
removeFile = true;
|
|
goto out;
|
|
}
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Process successfully finished!");
|
|
breaks++;
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Ticket saved to \"%s\".", dumpPath);
|
|
|
|
success = true;
|
|
|
|
out:
|
|
breaks += 2;
|
|
|
|
if (outFile) fclose(outFile);
|
|
|
|
if (!success && removeFile) remove(dumpPath);
|
|
|
|
ncmContentStorageClose(&ncmStorage);
|
|
|
|
if (titleContentInfos) free(titleContentInfos);
|
|
|
|
if (dumpName) free(dumpName);
|
|
|
|
return success;
|
|
}
|