mirror of
https://github.com/DarkMatterCore/nxdumptool.git
synced 2024-11-26 20:22:17 +00:00
42fef7d3f1
This puts a smile on my face.
2439 lines
79 KiB
C
2439 lines
79 KiB
C
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
#include <math.h>
|
|
#include <dirent.h>
|
|
#include <unistd.h>
|
|
#include <ctype.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/statvfs.h>
|
|
#include <sys/socket.h>
|
|
#include <switch/services/ncm.h>
|
|
#include <switch/services/ns.h>
|
|
#include <libxml2/libxml/globals.h>
|
|
#include <libxml2/libxml/xpath.h>
|
|
#include <curl/curl.h>
|
|
#include <json-c/json.h>
|
|
#include <pthread.h>
|
|
|
|
#include "dumper.h"
|
|
#include "fs_ext.h"
|
|
#include "keys.h"
|
|
#include "ui.h"
|
|
#include "util.h"
|
|
#include "fatfs/ff.h"
|
|
|
|
/* Extern variables */
|
|
|
|
extern bool highlight;
|
|
|
|
extern int breaks;
|
|
extern int font_height;
|
|
|
|
extern int cursor;
|
|
extern int scroll;
|
|
|
|
extern curMenuType menuType;
|
|
|
|
extern u8 *fileNormalIconBuf;
|
|
extern u8 *fileHighlightIconBuf;
|
|
|
|
extern nca_keyset_t nca_keyset;
|
|
|
|
/* Statically allocated variables */
|
|
|
|
static bool initNcm = false, initNs = false, initCsrng = false, initSpl = false, initPmdmnt = false, initPl = false, initNet = false;
|
|
static bool openFsDevOp = false, openGcEvtNotifier = false, loadGcKernEvt = false, gcThreadInit = false, homeBtnBlocked = false;
|
|
|
|
dumpOptions dumpCfg;
|
|
|
|
bool keysFileAvailable = false;
|
|
|
|
static pthread_t gameCardDetectionThread;
|
|
static UEvent exitEvent;
|
|
|
|
static AppletType programAppletType;
|
|
|
|
char cfwDirStr[32] = {'\0'};
|
|
|
|
gamecard_ctx_t gameCardInfo;
|
|
|
|
u32 titleAppCount = 0, titlePatchCount = 0, titleAddOnCount = 0;
|
|
u32 sdCardTitleAppCount = 0, sdCardTitlePatchCount = 0, sdCardTitleAddOnCount = 0;
|
|
u32 emmcTitleAppCount = 0, emmcTitlePatchCount = 0, emmcTitleAddOnCount = 0;
|
|
u32 gameCardSdCardEmmcPatchCount = 0, gameCardSdCardEmmcAddOnCount = 0;
|
|
|
|
base_app_ctx_t *baseAppEntries = NULL;
|
|
patch_addon_ctx_t *patchEntries = NULL, *addOnEntries = NULL;
|
|
|
|
static volatile bool gameCardInfoLoaded = false;
|
|
static bool sdCardAndEmmcTitleInfoLoaded = false;
|
|
|
|
exefs_ctx_t exeFsContext;
|
|
romfs_ctx_t romFsContext;
|
|
bktr_ctx_t bktrContext;
|
|
|
|
char curRomFsPath[NAME_BUF_LEN] = {'\0'};
|
|
u32 curRomFsDirOffset = 0;
|
|
romfs_browser_entry *romFsBrowserEntries = NULL;
|
|
|
|
static char *result_buf = NULL;
|
|
static size_t result_sz = 0;
|
|
static size_t result_written = 0;
|
|
|
|
char **filenameBuffer = NULL;
|
|
int filenameCount = 0, filenameIndex = 0;
|
|
|
|
u8 *dumpBuf = NULL;
|
|
u8 *gcReadBuf = NULL;
|
|
u8 *ncaCtrBuf = NULL;
|
|
|
|
orphan_patch_addon_entry *orphanEntries = NULL;
|
|
u32 orphanEntriesCnt = 0;
|
|
|
|
char strbuf[NAME_BUF_LEN] = {'\0'};
|
|
|
|
static const char *appLaunchPath = NULL;
|
|
|
|
FsStorage fatFsStorage = {0};
|
|
static FATFS *fatFsObj = NULL;
|
|
|
|
u64 freeSpace = 0;
|
|
char freeSpaceStr[32] = {'\0'};
|
|
|
|
browser_entry_size_info *hfs0ExeFsEntriesSizes = NULL;
|
|
|
|
void loadConfig()
|
|
{
|
|
// Set default configuration values
|
|
memset(&dumpCfg, 0x00, sizeof(dumpOptions));
|
|
|
|
dumpCfg.xciDumpCfg.isFat32 = true;
|
|
dumpCfg.xciDumpCfg.calcCrc = true;
|
|
|
|
dumpCfg.nspDumpCfg.isFat32 = true;
|
|
dumpCfg.nspDumpCfg.useNoIntroLookup = true;
|
|
dumpCfg.nspDumpCfg.npdmAcidRsaPatch = true;
|
|
|
|
dumpCfg.batchDumpCfg.isFat32 = true;
|
|
dumpCfg.batchDumpCfg.npdmAcidRsaPatch = true;
|
|
dumpCfg.batchDumpCfg.skipDumpedTitles = true;
|
|
dumpCfg.batchDumpCfg.haltOnErrors = true;
|
|
dumpCfg.batchDumpCfg.batchModeSrc = BATCH_SOURCE_ALL;
|
|
|
|
dumpCfg.exeFsDumpCfg.isFat32 = true;
|
|
|
|
dumpCfg.romFsDumpCfg.isFat32 = true;
|
|
|
|
FILE *configFile = fopen(CONFIG_PATH, "rb");
|
|
if (!configFile) return;
|
|
|
|
fseek(configFile, 0, SEEK_END);
|
|
size_t configFileSize = ftell(configFile);
|
|
rewind(configFile);
|
|
|
|
if (configFileSize != sizeof(dumpOptions))
|
|
{
|
|
fclose(configFile);
|
|
remove(CONFIG_PATH);
|
|
return;
|
|
}
|
|
|
|
dumpOptions tmpCfg;
|
|
size_t read_res = fread(&tmpCfg, 1, sizeof(dumpOptions), configFile);
|
|
fclose(configFile);
|
|
|
|
if (read_res != sizeof(dumpOptions))
|
|
{
|
|
remove(CONFIG_PATH);
|
|
return;
|
|
}
|
|
|
|
memcpy(&dumpCfg, &tmpCfg, sizeof(dumpOptions));
|
|
|
|
// Check if the configuration is correct
|
|
if (dumpCfg.xciDumpCfg.setXciArchiveBit && !dumpCfg.xciDumpCfg.isFat32) dumpCfg.xciDumpCfg.setXciArchiveBit = false;
|
|
|
|
if (dumpCfg.nspDumpCfg.tiklessDump && !dumpCfg.nspDumpCfg.removeConsoleData) dumpCfg.nspDumpCfg.tiklessDump = false;
|
|
|
|
if (dumpCfg.batchDumpCfg.tiklessDump && !dumpCfg.batchDumpCfg.removeConsoleData) dumpCfg.batchDumpCfg.tiklessDump = false;
|
|
|
|
if (dumpCfg.batchDumpCfg.batchModeSrc >= BATCH_SOURCE_CNT) dumpCfg.batchDumpCfg.batchModeSrc = BATCH_SOURCE_ALL;
|
|
}
|
|
|
|
void saveConfig()
|
|
{
|
|
FILE *configFile = fopen(CONFIG_PATH, "wb");
|
|
if (!configFile) return;
|
|
|
|
size_t write_res = fwrite(&dumpCfg, 1, sizeof(dumpOptions), configFile);
|
|
fclose(configFile);
|
|
|
|
if (write_res != sizeof(dumpOptions)) remove(CONFIG_PATH);
|
|
}
|
|
|
|
static void retrieveRunningCfwDir()
|
|
{
|
|
bool txService = isServiceRunning("tx");
|
|
bool rnxService = isServiceRunning("rnx");
|
|
|
|
if (!txService && !rnxService)
|
|
{
|
|
// Atmosphere
|
|
snprintf(cfwDirStr, MAX_CHARACTERS(cfwDirStr), CFW_PATH_ATMOSPHERE);
|
|
} else
|
|
if (txService && !rnxService)
|
|
{
|
|
// SX OS
|
|
snprintf(cfwDirStr, MAX_CHARACTERS(cfwDirStr), CFW_PATH_SXOS);
|
|
} else {
|
|
// ReiNX
|
|
snprintf(cfwDirStr, MAX_CHARACTERS(cfwDirStr), CFW_PATH_REINX);
|
|
}
|
|
}
|
|
|
|
static void createOutputDirectories()
|
|
{
|
|
mkdir(HBLOADER_BASE_PATH, 0744);
|
|
mkdir(APP_BASE_PATH, 0744);
|
|
mkdir(XCI_DUMP_PATH, 0744);
|
|
mkdir(NSP_DUMP_PATH, 0744);
|
|
mkdir(HFS0_DUMP_PATH, 0744);
|
|
mkdir(EXEFS_DUMP_PATH, 0744);
|
|
mkdir(ROMFS_DUMP_PATH, 0744);
|
|
mkdir(CERT_DUMP_PATH, 0744);
|
|
mkdir(BATCH_OVERRIDES_PATH, 0744);
|
|
mkdir(TICKET_PATH, 0744);
|
|
}
|
|
|
|
void freeFilenameBuffer(void)
|
|
{
|
|
if (!filenameBuffer) return;
|
|
|
|
for(int i = 0; i < filenameCount; i++)
|
|
{
|
|
if (filenameBuffer[i]) free(filenameBuffer[i]);
|
|
}
|
|
|
|
filenameCount = filenameIndex = 0;
|
|
|
|
free(filenameBuffer);
|
|
filenameBuffer = NULL;
|
|
}
|
|
|
|
static bool allocateFilenameBuffer(u32 cnt)
|
|
{
|
|
if (!cnt) return false;
|
|
|
|
freeFilenameBuffer();
|
|
|
|
filenameBuffer = calloc(cnt, sizeof(char*));
|
|
if (!filenameBuffer) return false;
|
|
|
|
filenameCount = (int)cnt;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool addStringToFilenameBuffer(const char *str)
|
|
{
|
|
if (!str || !strlen(str) || (filenameIndex + 1) > filenameCount) return false;
|
|
|
|
filenameBuffer[filenameIndex] = strdup(str);
|
|
if (!filenameBuffer[filenameIndex])
|
|
{
|
|
freeFilenameBuffer();
|
|
return false;
|
|
}
|
|
|
|
filenameIndex++;
|
|
|
|
return true;
|
|
}
|
|
|
|
void freeRomFsBrowserEntries()
|
|
{
|
|
if (romFsBrowserEntries != NULL)
|
|
{
|
|
free(romFsBrowserEntries);
|
|
romFsBrowserEntries = NULL;
|
|
}
|
|
}
|
|
|
|
void freeHfs0ExeFsEntriesSizes()
|
|
{
|
|
if (hfs0ExeFsEntriesSizes)
|
|
{
|
|
free(hfs0ExeFsEntriesSizes);
|
|
hfs0ExeFsEntriesSizes = NULL;
|
|
}
|
|
}
|
|
|
|
void consoleErrorScreen(const char *fmt, ...)
|
|
{
|
|
consoleInit(NULL);
|
|
|
|
va_list va;
|
|
va_start(va, fmt);
|
|
vprintf(fmt, va);
|
|
va_end(va);
|
|
|
|
printf("\nPress any button to exit.\n");
|
|
|
|
while(appletMainLoop())
|
|
{
|
|
hidScanInput();
|
|
|
|
u64 keysDown = hidKeysAllDown(CONTROLLER_P1_AUTO);
|
|
|
|
if (keysDown && !((keysDown & KEY_TOUCH) || (keysDown & KEY_LSTICK_LEFT) || (keysDown & KEY_LSTICK_RIGHT) || (keysDown & KEY_LSTICK_UP) || (keysDown & KEY_LSTICK_DOWN) || \
|
|
(keysDown & KEY_RSTICK_LEFT) || (keysDown & KEY_RSTICK_RIGHT) || (keysDown & KEY_RSTICK_UP) || (keysDown & KEY_RSTICK_DOWN))) break;
|
|
|
|
consoleUpdate(NULL);
|
|
}
|
|
|
|
consoleExit(NULL);
|
|
}
|
|
|
|
bool initApplicationResources(int argc, char **argv)
|
|
{
|
|
Result result = 0;
|
|
bool success = false;
|
|
|
|
/* Copy launch path */
|
|
if (argc > 0 && argv && !envIsNso())
|
|
{
|
|
for(int i = 0; i < argc; i++)
|
|
{
|
|
if (argv[i] && strlen(argv[i]) > 10 && !strncasecmp(argv[i], "sdmc:/", 6) && !strncasecmp(argv[i] + strlen(argv[i]) - 4, ".nro", 4))
|
|
{
|
|
appLaunchPath = (const char*)argv[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Initialize services */
|
|
if (!initServices()) return false;
|
|
|
|
/* Initialize UI */
|
|
if (!uiInit()) return false;
|
|
|
|
/* Zero out gamecard info struct */
|
|
memset(&gameCardInfo, 0, sizeof(gamecard_ctx_t));
|
|
|
|
/* Zero out NCA keyset */
|
|
memset(&nca_keyset, 0, sizeof(nca_keyset_t));
|
|
|
|
/* Init ExeFS context */
|
|
initExeFsContext();
|
|
|
|
/* Init RomFS context */
|
|
initRomFsContext();
|
|
|
|
/* Init BKTR context */
|
|
initBktrContext();
|
|
|
|
/* Make sure output directories exist */
|
|
createOutputDirectories();
|
|
|
|
/* Check if the Lockpick_RCM keys file is available */
|
|
keysFileAvailable = checkIfFileExists(KEYS_FILE_PATH);
|
|
|
|
/* Retrieve running CFW directory */
|
|
retrieveRunningCfwDir();
|
|
|
|
/* Get applet type */
|
|
programAppletType = appletGetAppletType();
|
|
|
|
/* Disable screen dimming and auto sleep */
|
|
appletSetMediaPlaybackState(true);
|
|
|
|
/* Enable CPU boost mode */
|
|
appletSetCpuBoostMode(ApmCpuBoostMode_Type1);
|
|
|
|
/* Mount eMMC BIS System partition */
|
|
if (!mountSysEmmcPartition()) goto out;
|
|
|
|
/* Allocate memory for the general purpose dump buffer */
|
|
dumpBuf = calloc(DUMP_BUFFER_SIZE, sizeof(u8));
|
|
if (!dumpBuf)
|
|
{
|
|
uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "%s: failed to allocate memory for the dump buffer!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
/* Allocate memory for the gamecard read buffer */
|
|
gcReadBuf = calloc(GAMECARD_READ_BUFFER_SIZE, sizeof(u8));
|
|
if (!gcReadBuf)
|
|
{
|
|
uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "%s: failed to allocate memory for the gamecard read buffer!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
/* Allocate memory for the NCA AES-CTR operation buffer */
|
|
ncaCtrBuf = calloc(NCA_CTR_BUFFER_SIZE, sizeof(u8));
|
|
if (!ncaCtrBuf)
|
|
{
|
|
uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "%s: failed to allocate memory for the NCA AES-CTR operation buffer!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
/* Open device operator */
|
|
result = fsOpenDeviceOperator(&(gameCardInfo.fsOperatorInstance));
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "%s: failed to open device operator! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
|
|
openFsDevOp = true;
|
|
|
|
/* Open gamecard detection event notifier */
|
|
result = fsOpenGameCardDetectionEventNotifier(&(gameCardInfo.fsGameCardEventNotifier));
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "%s: failed to open gamecard detection event notifier! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
|
|
openGcEvtNotifier = true;
|
|
|
|
/* Retrieve gamecard detection kernel event */
|
|
result = fsEventNotifierGetEventHandle(&(gameCardInfo.fsGameCardEventNotifier), &(gameCardInfo.fsGameCardKernelEvent), true);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "%s: failed to retrieve gamecard detection event handle! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
|
|
loadGcKernEvt = true;
|
|
|
|
/* Create usermode exit event */
|
|
ueventCreate(&exitEvent, false);
|
|
|
|
/* Create and start gamecard detection thread */
|
|
if (!createGameCardDetectionThread()) goto out;
|
|
|
|
gcThreadInit = true;
|
|
|
|
/* Load settings from configuration file */
|
|
loadConfig();
|
|
|
|
/* Update free space */
|
|
updateFreeSpace();
|
|
|
|
/* Set output status */
|
|
success = true;
|
|
|
|
out:
|
|
if (!success)
|
|
{
|
|
uiRefreshDisplay();
|
|
delay(5);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
void deinitApplicationResources()
|
|
{
|
|
/* Free global resources */
|
|
freeGlobalData();
|
|
|
|
/* Save current settings to configuration file */
|
|
saveConfig();
|
|
|
|
if (gcThreadInit)
|
|
{
|
|
/* Signal the exit event to terminate the gamecard detection thread */
|
|
ueventSignal(&exitEvent);
|
|
|
|
/* Wait for the gamecard detection thread to exit */
|
|
pthread_join(gameCardDetectionThread, NULL);
|
|
}
|
|
|
|
/* Close gamecard detection kernel event */
|
|
if (loadGcKernEvt) eventClose(&(gameCardInfo.fsGameCardKernelEvent));
|
|
|
|
/* Close gamecard detection event notifier */
|
|
if (openGcEvtNotifier) fsEventNotifierClose(&(gameCardInfo.fsGameCardEventNotifier));
|
|
|
|
/* Close device operator */
|
|
if (openFsDevOp) fsDeviceOperatorClose(&(gameCardInfo.fsOperatorInstance));
|
|
|
|
/* Free NCA AES-CTR operation buffer */
|
|
if (ncaCtrBuf) free(ncaCtrBuf);
|
|
|
|
/* Free gamecard read buffer */
|
|
if (gcReadBuf) free(gcReadBuf);
|
|
|
|
/* Free general purpose dump buffer */
|
|
if (dumpBuf) free(dumpBuf);
|
|
|
|
/* Unmount eMMC BIS System partition */
|
|
unmountSysEmmcPartition();
|
|
|
|
/* Disable CPU boost mode */
|
|
appletSetCpuBoostMode(ApmCpuBoostMode_Disabled);
|
|
|
|
/* Enable screen dimming and auto sleep */
|
|
appletSetMediaPlaybackState(false);
|
|
|
|
/* Deinitialize UI */
|
|
uiDeinit();
|
|
|
|
/* Deinitialize services */
|
|
deinitServices();
|
|
}
|
|
|
|
void appletModeOperationWarning()
|
|
{
|
|
if (!appletModeCheck()) return;
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem.");
|
|
breaks++;
|
|
}
|
|
|
|
void formatETAString(u64 curTime, char *out, size_t outSize)
|
|
{
|
|
if (!out || !outSize) return;
|
|
|
|
u64 i, hour = 0, min = 0, sec = 0;
|
|
|
|
for(i = 0; i < curTime; i++)
|
|
{
|
|
sec++;
|
|
if (sec == 60)
|
|
{
|
|
sec = 0;
|
|
min++;
|
|
if (min == 60)
|
|
{
|
|
min = 0;
|
|
hour++;
|
|
}
|
|
}
|
|
}
|
|
|
|
snprintf(out, outSize, "%02luH%02luM%02luS", hour, min, sec);
|
|
}
|
|
|
|
void generateSdCardEmmcTitleList()
|
|
{
|
|
if (!titleAppCount || !baseAppEntries) return;
|
|
|
|
if (!allocateFilenameBuffer(titleAppCount)) return;
|
|
|
|
for(u32 i = 0; i < titleAppCount; i++)
|
|
{
|
|
if (!addStringToFilenameBuffer(baseAppEntries[i].name)) return;
|
|
}
|
|
}
|
|
|
|
void truncateBrowserEntryName(char *str)
|
|
{
|
|
if (!str || !strlen(str)) return;
|
|
|
|
u32 strWidth = uiGetStrWidth(str);
|
|
u32 limit = (u32)(FB_WIDTH - (font_height * 8));
|
|
|
|
if ((BROWSER_ICON_DIMENSION + 16 + strWidth) >= limit)
|
|
{
|
|
while((BROWSER_ICON_DIMENSION + 16 + strWidth) >= limit)
|
|
{
|
|
str[strlen(str) - 1] = '\0';
|
|
strWidth = uiGetStrWidth(str);
|
|
}
|
|
|
|
strcat(str, "...");
|
|
}
|
|
}
|
|
|
|
bool getHfs0FileList(u32 partition)
|
|
{
|
|
if (partition >= gameCardInfo.hfs0PartitionCnt)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid HFS0 partition index!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (!gameCardInfo.hfs0Partitions || !gameCardInfo.hfs0Partitions[partition].header || !gameCardInfo.hfs0Partitions[partition].header_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: HFS0 partition header information unavailable!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (!gameCardInfo.hfs0Partitions[partition].file_cnt || !gameCardInfo.hfs0Partitions[partition].str_table_size)
|
|
{
|
|
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 curName[NAME_BUF_LEN] = {'\0'};
|
|
|
|
freeHfs0ExeFsEntriesSizes();
|
|
|
|
hfs0ExeFsEntriesSizes = calloc(gameCardInfo.hfs0Partitions[partition].file_cnt, sizeof(browser_entry_size_info));
|
|
if (!hfs0ExeFsEntriesSizes)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to allocate memory for HFS0 entries size info!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (!allocateFilenameBuffer(gameCardInfo.hfs0Partitions[partition].file_cnt))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to allocate memory for the filename buffer!", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
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 *cur_filename = (char*)(gameCardInfo.hfs0Partitions[partition].header + sizeof(hfs0_header) + (gameCardInfo.hfs0Partitions[partition].file_cnt * sizeof(hfs0_file_entry)) + entry.filename_offset);
|
|
|
|
snprintf(curName, MAX_CHARACTERS(curName), cur_filename);
|
|
|
|
// Fix entry name length
|
|
truncateBrowserEntryName(curName);
|
|
|
|
if (!addStringToFilenameBuffer(curName))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to allocate memory for filename entry in filename buffer!", __func__);
|
|
breaks += 2;
|
|
freeHfs0ExeFsEntriesSizes();
|
|
return false;
|
|
}
|
|
|
|
// Save entry size
|
|
hfs0ExeFsEntriesSizes[i].size = entry.file_size;
|
|
convertSize(hfs0ExeFsEntriesSizes[i].size, hfs0ExeFsEntriesSizes[i].sizeStr, MAX_CHARACTERS(hfs0ExeFsEntriesSizes[i].sizeStr));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool listDesiredNcaType(NcmContentInfo *titleContentInfos, u32 titleContentInfoCnt, u8 type, int desiredIdOffset, u32 *outIndex, u32 *outCount)
|
|
{
|
|
if (!titleContentInfos || !titleContentInfoCnt || type > NcmContentType_DeltaFragment || !outIndex || !outCount)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters.", __func__);
|
|
return false;
|
|
}
|
|
|
|
int idx = -1;
|
|
u32 i, cnt = 0;
|
|
bool success = false;
|
|
u32 *indexes = NULL, *tmpIndexes = NULL;
|
|
|
|
int cur_breaks, initial_breaks = breaks;
|
|
u32 selectedContent = 0;
|
|
u64 keysDown = 0, keysHeld = 0;
|
|
|
|
char nca_id[SHA256_HASH_SIZE + 1] = {'\0'};
|
|
|
|
for(i = 0; i < titleContentInfoCnt; i++)
|
|
{
|
|
if (titleContentInfos[i].content_type == type)
|
|
{
|
|
if (desiredIdOffset >= 0)
|
|
{
|
|
if (titleContentInfos[i].id_offset == (u8)desiredIdOffset)
|
|
{
|
|
// Save the index for the content with the desired ID offset
|
|
idx = (int)i;
|
|
}
|
|
} else {
|
|
tmpIndexes = realloc(indexes, (cnt + 1) * sizeof(u32));
|
|
if (!tmpIndexes)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to reallocate indexes buffer!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
indexes = tmpIndexes;
|
|
tmpIndexes = NULL;
|
|
|
|
indexes[cnt] = i;
|
|
}
|
|
|
|
cnt++;
|
|
}
|
|
}
|
|
|
|
if (!cnt)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to find any %s NCAs!", __func__, getContentType(type));
|
|
goto out;
|
|
}
|
|
|
|
if (desiredIdOffset >= 0)
|
|
{
|
|
if (idx < 0)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to find %s NCA with ID offset %d!", __func__, getContentType(type), desiredIdOffset);
|
|
goto out;
|
|
}
|
|
} else {
|
|
// If only a single NCA with the desired content type was detected, save its index right away
|
|
if (cnt == 1) idx = (int)indexes[0];
|
|
}
|
|
|
|
// Return immediately if necessary
|
|
if (idx >= 0)
|
|
{
|
|
success = true;
|
|
goto out;
|
|
}
|
|
|
|
// Display a selection list
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Select one of the available %s NCAs from the list below:", getContentType(type));
|
|
breaks += 2;
|
|
|
|
while(true)
|
|
{
|
|
cur_breaks = breaks;
|
|
|
|
uiFill(0, 8 + (cur_breaks * LINE_HEIGHT), FB_WIDTH, FB_HEIGHT - (8 + (cur_breaks * LINE_HEIGHT)), BG_COLOR_RGB);
|
|
|
|
for(i = 0; i < cnt; i++)
|
|
{
|
|
u32 xpos = STRING_X_POS;
|
|
u32 ypos = (8 + (cur_breaks * LINE_HEIGHT) + (i * (font_height + 12)) + 6);
|
|
|
|
if (i == selectedContent)
|
|
{
|
|
highlight = true;
|
|
uiFill(0, ypos - 6, FB_WIDTH, font_height + 12, HIGHLIGHT_BG_COLOR_RGB);
|
|
}
|
|
|
|
uiDrawIcon((highlight ? fileHighlightIconBuf : fileNormalIconBuf), BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, xpos, ypos);
|
|
xpos += (BROWSER_ICON_DIMENSION + 8);
|
|
|
|
convertDataToHexString(titleContentInfos[indexes[i]].content_id.c, SHA256_HASH_SIZE / 2, nca_id, SHA256_HASH_SIZE + 1);
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s.nca (ID Offset: %u)", nca_id, titleContentInfos[indexes[i]].id_offset);
|
|
|
|
if (highlight)
|
|
{
|
|
uiDrawString(xpos, ypos, HIGHLIGHT_FONT_COLOR_RGB, strbuf);
|
|
} else {
|
|
uiDrawString(xpos, ypos, FONT_COLOR_RGB, strbuf);
|
|
}
|
|
|
|
if (i == selectedContent) 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;
|
|
}
|
|
|
|
if (keysDown & KEY_A)
|
|
{
|
|
idx = (int)indexes[selectedContent];
|
|
break;
|
|
}
|
|
|
|
if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP))
|
|
{
|
|
if (selectedContent > 0) selectedContent--;
|
|
}
|
|
|
|
if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN))
|
|
{
|
|
if (selectedContent < (cnt - 1)) selectedContent++;
|
|
}
|
|
}
|
|
|
|
breaks = initial_breaks;
|
|
uiFill(0, 8 + (breaks * LINE_HEIGHT), FB_WIDTH, FB_HEIGHT - (8 + (breaks * LINE_HEIGHT)), BG_COLOR_RGB);
|
|
uiRefreshDisplay();
|
|
|
|
success = true;
|
|
|
|
out:
|
|
if (indexes) free(indexes);
|
|
|
|
if (success)
|
|
{
|
|
*outIndex = (u32)idx;
|
|
*outCount = cnt;
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
bool readNcaExeFsSection(u32 titleIndex, bool usePatch)
|
|
{
|
|
u32 i = 0;
|
|
Result result;
|
|
|
|
NcmStorageId curStorageId = NcmStorageId_None;
|
|
NcmContentMetaType metaType;
|
|
u32 titleCount = 0, ncmTitleIndex = 0;
|
|
|
|
NcmContentInfo *titleContentInfos = NULL;
|
|
u32 titleContentInfoCnt = 0;
|
|
|
|
u32 contentIndex = 0, desiredNcaTypeCount = 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;
|
|
|
|
title_rights_ctx rights_info;
|
|
memset(&rights_info, 0, sizeof(title_rights_ctx));
|
|
|
|
u8 decrypted_nca_keys[NCA_KEY_AREA_SIZE];
|
|
|
|
bool success = false;
|
|
|
|
if ((!usePatch && !baseAppEntries) || (usePatch && !patchEntries))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: title storage ID unavailable!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if ((!usePatch && titleIndex >= titleAppCount) || (usePatch && titleIndex >= titlePatchCount))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid title index!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
curStorageId = (!usePatch ? baseAppEntries[titleIndex].storageId : patchEntries[titleIndex].storageId);
|
|
|
|
ncmTitleIndex = (!usePatch ? baseAppEntries[titleIndex].ncmIndex : patchEntries[titleIndex].ncmIndex);
|
|
|
|
metaType = (!usePatch ? NcmContentMetaType_Application : NcmContentMetaType_Patch);
|
|
|
|
switch(curStorageId)
|
|
{
|
|
case NcmStorageId_GameCard:
|
|
titleCount = (!usePatch ? titleAppCount : titlePatchCount);
|
|
break;
|
|
case NcmStorageId_SdCard:
|
|
titleCount = (!usePatch ? sdCardTitleAppCount : sdCardTitlePatchCount);
|
|
break;
|
|
case NcmStorageId_BuiltInUser:
|
|
titleCount = (!usePatch ? emmcTitleAppCount : emmcTitlePatchCount);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// If we're dealing with a gamecard, open the Secure HFS0 partition (IStorage partition #1)
|
|
if (curStorageId == NcmStorageId_GameCard)
|
|
{
|
|
result = openGameCardStoragePartition(ISTORAGE_PARTITION_SECURE);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open IStorage partition #1! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Looking for the Program NCA (%s)...", (!usePatch ? "base application" : "update"));
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
if (!retrieveContentInfosFromTitle(curStorageId, metaType, titleCount, ncmTitleIndex, &titleContentInfos, &titleContentInfoCnt))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, strbuf);
|
|
goto out;
|
|
}
|
|
|
|
if (!listDesiredNcaType(titleContentInfos, titleContentInfoCnt, NcmContentType_Program, -1, &contentIndex, &desiredNcaTypeCount)) goto out;
|
|
|
|
memcpy(&ncaId, &(titleContentInfos[contentIndex].content_id), sizeof(NcmContentId));
|
|
convertDataToHexString(titleContentInfos[contentIndex].content_id.c, SHA256_HASH_SIZE / 2, ncaIdStr, SHA256_HASH_SIZE + 1);
|
|
|
|
if (desiredNcaTypeCount == 1)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Found Program NCA: \"%s.nca\".", ncaIdStr);
|
|
uiRefreshDisplay();
|
|
breaks += 2;
|
|
}
|
|
|
|
/*uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Retrieving ExeFS entries...");
|
|
uiRefreshDisplay();
|
|
breaks++;*/
|
|
|
|
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;
|
|
}
|
|
|
|
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 Program NCA!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Decrypt the NCA header
|
|
if (!decryptNcaHeader(ncaHeader, NCA_FULL_HEADER_LENGTH, &dec_nca_header, &rights_info, decrypted_nca_keys, (curStorageId != NcmStorageId_GameCard || (curStorageId == NcmStorageId_GameCard && usePatch)))) goto out;
|
|
|
|
if (curStorageId == NcmStorageId_GameCard)
|
|
{
|
|
bool has_rights_id = false;
|
|
|
|
for(i = 0; i < 0x10; i++)
|
|
{
|
|
if (dec_nca_header.rights_id[i] != 0)
|
|
{
|
|
has_rights_id = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (has_rights_id)
|
|
{
|
|
if (usePatch)
|
|
{
|
|
// Retrieve the ticket from the HFS0 partition in the gamecard
|
|
if (!retrieveTitleKeyFromGameCardTicket(&rights_info, decrypted_nca_keys)) goto out;
|
|
} else {
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: Rights ID field in Program NCA header not empty!", __func__);
|
|
goto out;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read file entries from the ExeFS section
|
|
success = parseExeFsEntryFromNca(&ncmStorage, &ncaId, &dec_nca_header, decrypted_nca_keys);
|
|
if (success)
|
|
{
|
|
exeFsContext.storageId = curStorageId;
|
|
exeFsContext.idOffset = titleContentInfos[contentIndex].id_offset;
|
|
}
|
|
|
|
out:
|
|
if (!success)
|
|
{
|
|
ncmContentStorageClose(&ncmStorage);
|
|
if (curStorageId == NcmStorageId_GameCard) closeGameCardStoragePartition();
|
|
}
|
|
|
|
if (titleContentInfos) free(titleContentInfos);
|
|
|
|
return success;
|
|
}
|
|
|
|
int readNcaRomFsSection(u32 titleIndex, selectedRomFsType curRomFsType, int desiredIdOffset)
|
|
{
|
|
u32 i = 0;
|
|
Result result;
|
|
|
|
NcmStorageId curStorageId = NcmStorageId_None;
|
|
NcmContentMetaType metaType;
|
|
u32 titleCount = 0, ncmTitleIndex = 0;
|
|
|
|
NcmContentInfo *titleContentInfos = NULL;
|
|
u32 titleContentInfoCnt = 0;
|
|
|
|
u32 contentIndex = 0, desiredNcaTypeCount = 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;
|
|
|
|
title_rights_ctx rights_info;
|
|
memset(&rights_info, 0, sizeof(title_rights_ctx));
|
|
|
|
u8 decrypted_nca_keys[NCA_KEY_AREA_SIZE];
|
|
|
|
int ret = -1;
|
|
|
|
if (curRomFsType != ROMFS_TYPE_APP && curRomFsType != ROMFS_TYPE_PATCH && curRomFsType != ROMFS_TYPE_ADDON)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid RomFS title type!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if ((curRomFsType == ROMFS_TYPE_APP && !baseAppEntries) || (curRomFsType == ROMFS_TYPE_PATCH && !patchEntries) || (curRomFsType == ROMFS_TYPE_ADDON && !addOnEntries))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: title storage ID unavailable!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if ((curRomFsType == ROMFS_TYPE_APP && titleIndex >= titleAppCount) || (curRomFsType == ROMFS_TYPE_PATCH && titleIndex >= titlePatchCount) || (curRomFsType == ROMFS_TYPE_ADDON && titleIndex >= titleAddOnCount))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid title index!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
curStorageId = (curRomFsType == ROMFS_TYPE_APP ? baseAppEntries[titleIndex].storageId : (curRomFsType == ROMFS_TYPE_PATCH ? patchEntries[titleIndex].storageId : addOnEntries[titleIndex].storageId));
|
|
|
|
ncmTitleIndex = (curRomFsType == ROMFS_TYPE_APP ? baseAppEntries[titleIndex].ncmIndex : (curRomFsType == ROMFS_TYPE_PATCH ? patchEntries[titleIndex].ncmIndex : addOnEntries[titleIndex].ncmIndex));
|
|
|
|
metaType = (curRomFsType == ROMFS_TYPE_APP ? NcmContentMetaType_Application : (curRomFsType == ROMFS_TYPE_PATCH ? NcmContentMetaType_Patch : NcmContentMetaType_AddOnContent));
|
|
|
|
switch(curStorageId)
|
|
{
|
|
case NcmStorageId_GameCard:
|
|
titleCount = (curRomFsType == ROMFS_TYPE_APP ? titleAppCount : (curRomFsType == ROMFS_TYPE_PATCH ? titlePatchCount : titleAddOnCount));
|
|
break;
|
|
case NcmStorageId_SdCard:
|
|
titleCount = (curRomFsType == ROMFS_TYPE_APP ? sdCardTitleAppCount : (curRomFsType == ROMFS_TYPE_PATCH ? sdCardTitlePatchCount : sdCardTitleAddOnCount));
|
|
break;
|
|
case NcmStorageId_BuiltInUser:
|
|
titleCount = (curRomFsType == ROMFS_TYPE_APP ? emmcTitleAppCount : (curRomFsType == ROMFS_TYPE_PATCH ? emmcTitlePatchCount : emmcTitleAddOnCount));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// If we're dealing with a gamecard, open the Secure HFS0 partition (IStorage partition #1)
|
|
if (curStorageId == NcmStorageId_GameCard)
|
|
{
|
|
result = openGameCardStoragePartition(ISTORAGE_PARTITION_SECURE);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open IStorage partition #1! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Looking for the %s NCA (%s)...", (curRomFsType == ROMFS_TYPE_ADDON ? "Data" : "Program"), (curRomFsType == ROMFS_TYPE_APP ? "base application" : (curRomFsType == ROMFS_TYPE_PATCH ? "update" : "DLC")));
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
if (!retrieveContentInfosFromTitle(curStorageId, metaType, titleCount, ncmTitleIndex, &titleContentInfos, &titleContentInfoCnt))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, strbuf);
|
|
goto out;
|
|
}
|
|
|
|
if (!listDesiredNcaType(titleContentInfos, titleContentInfoCnt, (curRomFsType == ROMFS_TYPE_ADDON ? NcmContentType_Data : NcmContentType_Program), desiredIdOffset, &contentIndex, &desiredNcaTypeCount)) goto out;
|
|
|
|
memcpy(&ncaId, &(titleContentInfos[contentIndex].content_id), sizeof(NcmContentId));
|
|
convertDataToHexString(titleContentInfos[contentIndex].content_id.c, SHA256_HASH_SIZE / 2, ncaIdStr, SHA256_HASH_SIZE + 1);
|
|
|
|
if (desiredNcaTypeCount == 1)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Found %s NCA: \"%s.nca\".", (curRomFsType == ROMFS_TYPE_ADDON ? "Data" : "Program"), ncaIdStr);
|
|
uiRefreshDisplay();
|
|
breaks += 2;
|
|
}
|
|
|
|
/*uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Retrieving RomFS entry tables...");
|
|
uiRefreshDisplay();
|
|
breaks++;*/
|
|
|
|
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;
|
|
}
|
|
|
|
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 %s NCA!", __func__, (curRomFsType == ROMFS_TYPE_ADDON ? "Data" : "Program"));
|
|
goto out;
|
|
}
|
|
|
|
// Decrypt the NCA header
|
|
if (!decryptNcaHeader(ncaHeader, NCA_FULL_HEADER_LENGTH, &dec_nca_header, &rights_info, decrypted_nca_keys, (curStorageId != NcmStorageId_GameCard || (curStorageId == NcmStorageId_GameCard && curRomFsType == ROMFS_TYPE_PATCH)))) goto out;
|
|
|
|
if (curStorageId == NcmStorageId_GameCard)
|
|
{
|
|
bool has_rights_id = false;
|
|
|
|
for(i = 0; i < 0x10; i++)
|
|
{
|
|
if (dec_nca_header.rights_id[i] != 0)
|
|
{
|
|
has_rights_id = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (has_rights_id)
|
|
{
|
|
if (curRomFsType == ROMFS_TYPE_PATCH)
|
|
{
|
|
// Retrieve the ticket from the HFS0 partition in the gamecard
|
|
if (!retrieveTitleKeyFromGameCardTicket(&rights_info, decrypted_nca_keys)) goto out;
|
|
} else {
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: Rights ID field in %s NCA header not empty!", __func__, (curRomFsType == ROMFS_TYPE_ADDON ? "Data" : "Program"));
|
|
goto out;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (curRomFsType != ROMFS_TYPE_PATCH)
|
|
{
|
|
// Read directory and file tables from the RomFS section
|
|
ret = parseRomFsEntryFromNca(&ncmStorage, &ncaId, &dec_nca_header, decrypted_nca_keys);
|
|
if (ret == 0)
|
|
{
|
|
romFsContext.storageId = curStorageId;
|
|
romFsContext.idOffset = titleContentInfos[contentIndex].id_offset;
|
|
}
|
|
} else {
|
|
// Look for the base application title index
|
|
u32 appIndex;
|
|
|
|
for(i = 0; i < titleAppCount; i++)
|
|
{
|
|
if (checkIfPatchOrAddOnBelongsToBaseApplication(titleIndex, i, false))
|
|
{
|
|
appIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (i >= titleAppCount)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to find base application title index for the selected update!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
// Read directory and file tables from the RomFS section in the Program NCA from the base application
|
|
// We'll proceed even if the Program NCA from the base application doesn't hold a RomFS section (ret == -2)
|
|
ret = readNcaRomFsSection(appIndex, ROMFS_TYPE_APP, (int)titleContentInfos[contentIndex].id_offset);
|
|
if (ret == -1) goto out;
|
|
|
|
// Remove missing base RomFS error message if needed
|
|
if (ret == -2) uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), BG_COLOR_RGB);
|
|
|
|
// Update BKTR context to use the base RomFS if available
|
|
bktrContext.use_base_romfs = (ret == 0);
|
|
|
|
// Read BKTR entry data in the Program NCA from the update
|
|
ret = (parseBktrEntryFromNca(&ncmStorage, &ncaId, &dec_nca_header, decrypted_nca_keys) ? 0 : -1);
|
|
if (ret == 0)
|
|
{
|
|
bktrContext.storageId = curStorageId;
|
|
bktrContext.idOffset = titleContentInfos[contentIndex].id_offset;
|
|
}
|
|
}
|
|
|
|
out:
|
|
if (ret != 0)
|
|
{
|
|
ncmContentStorageClose(&ncmStorage);
|
|
if (curStorageId == NcmStorageId_GameCard) closeGameCardStoragePartition();
|
|
}
|
|
|
|
if (titleContentInfos) free(titleContentInfos);
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool getExeFsFileList()
|
|
{
|
|
if (!exeFsContext.exefs_header.file_cnt || !exeFsContext.exefs_entries || !exeFsContext.exefs_str_table)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to retrieve ExeFS section filelist!", __func__);
|
|
return false;
|
|
}
|
|
|
|
u32 i;
|
|
char curName[NAME_BUF_LEN] = {'\0'};
|
|
|
|
freeHfs0ExeFsEntriesSizes();
|
|
|
|
hfs0ExeFsEntriesSizes = calloc(exeFsContext.exefs_header.file_cnt, sizeof(browser_entry_size_info));
|
|
if (!hfs0ExeFsEntriesSizes)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to allocate memory for ExeFS entries size info!", __func__);
|
|
return false;
|
|
}
|
|
|
|
if (!allocateFilenameBuffer(exeFsContext.exefs_header.file_cnt))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to allocate memory for the filename buffer!", __func__);
|
|
return false;
|
|
}
|
|
|
|
for(i = 0; i < exeFsContext.exefs_header.file_cnt; i++)
|
|
{
|
|
char *cur_filename = (exeFsContext.exefs_str_table + exeFsContext.exefs_entries[i].filename_offset);
|
|
|
|
snprintf(curName, MAX_CHARACTERS(curName), cur_filename);
|
|
|
|
// Fix entry name length
|
|
truncateBrowserEntryName(curName);
|
|
|
|
if (!addStringToFilenameBuffer(curName))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to allocate memory for filename entry in filename buffer!", __func__);
|
|
freeHfs0ExeFsEntriesSizes();
|
|
return false;
|
|
}
|
|
|
|
// Save entry size
|
|
hfs0ExeFsEntriesSizes[i].size = exeFsContext.exefs_entries[i].file_size;
|
|
convertSize(hfs0ExeFsEntriesSizes[i].size, hfs0ExeFsEntriesSizes[i].sizeStr, MAX_CHARACTERS(hfs0ExeFsEntriesSizes[i].sizeStr));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool getRomFsParentDir(u32 dir_offset, bool usePatch, u32 *out)
|
|
{
|
|
if ((!usePatch && (!romFsContext.romfs_dirtable_size || dir_offset > romFsContext.romfs_dirtable_size || !romFsContext.romfs_dir_entries)) || (usePatch && (!bktrContext.romfs_dirtable_size || dir_offset > bktrContext.romfs_dirtable_size || !bktrContext.romfs_dir_entries)))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to retrieve parent RomFS section directory!", __func__);
|
|
return false;
|
|
}
|
|
|
|
romfs_dir *entry = (!usePatch ? (romfs_dir*)((u8*)romFsContext.romfs_dir_entries + dir_offset) : (romfs_dir*)((u8*)bktrContext.romfs_dir_entries + dir_offset));
|
|
|
|
*out = entry->parent;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool generateCurrentRomFsPath(u32 dir_offset, bool usePatch)
|
|
{
|
|
if ((!usePatch && (!romFsContext.romfs_dirtable_size || dir_offset > romFsContext.romfs_dirtable_size || !romFsContext.romfs_dir_entries)) || (usePatch && (!bktrContext.romfs_dirtable_size || dir_offset > bktrContext.romfs_dirtable_size || !bktrContext.romfs_dir_entries)))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to generate current RomFS section path!", __func__);
|
|
return false;
|
|
}
|
|
|
|
// Generate current path if we're not dealing with the root directory
|
|
if (dir_offset)
|
|
{
|
|
romfs_dir *entry = (!usePatch ? (romfs_dir*)((u8*)romFsContext.romfs_dir_entries + dir_offset) : (romfs_dir*)((u8*)bktrContext.romfs_dir_entries + dir_offset));
|
|
|
|
if (!entry->nameLen)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: directory entry without name in RomFS section!", __func__);
|
|
return false;
|
|
}
|
|
|
|
// Check if we're not a root dir child
|
|
if (entry->parent)
|
|
{
|
|
if (!generateCurrentRomFsPath(entry->parent, usePatch)) return false;
|
|
}
|
|
|
|
// Concatenate entry name
|
|
strcat(curRomFsPath, "/");
|
|
strncat(curRomFsPath, (char*)entry->name, entry->nameLen);
|
|
} else {
|
|
strcat(curRomFsPath, "/");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool getRomFsFileList(u32 dir_offset, bool usePatch)
|
|
{
|
|
u64 entryOffset = 0;
|
|
u32 dirEntryCnt = 1; // Always add the parent directory entry ("..")
|
|
u32 fileEntryCnt = 0;
|
|
u32 totalEntryCnt = 0;
|
|
u32 i = 1;
|
|
u32 romFsParentDir = 0;
|
|
|
|
u64 dirTableSize;
|
|
u64 fileTableSize;
|
|
|
|
freeRomFsBrowserEntries();
|
|
|
|
memset(curRomFsPath, 0, NAME_BUF_LEN);
|
|
|
|
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)))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to retrieve RomFS section filelist!", __func__);
|
|
return false;
|
|
}
|
|
|
|
if (!getRomFsParentDir(dir_offset, usePatch, &romFsParentDir)) return false;
|
|
|
|
if (!generateCurrentRomFsPath(dir_offset, usePatch)) return false;
|
|
|
|
dirTableSize = (!usePatch ? romFsContext.romfs_dirtable_size : bktrContext.romfs_dirtable_size);
|
|
fileTableSize = (!usePatch ? romFsContext.romfs_filetable_size : bktrContext.romfs_filetable_size);
|
|
|
|
// First count the directory entries
|
|
entryOffset = ROMFS_NONAME_DIRENTRY_SIZE; // Always skip the first entry (root directory)
|
|
|
|
while(entryOffset < dirTableSize)
|
|
{
|
|
romfs_dir *entry = (!usePatch ? (romfs_dir*)((u8*)romFsContext.romfs_dir_entries + entryOffset) : (romfs_dir*)((u8*)bktrContext.romfs_dir_entries + entryOffset));
|
|
|
|
if (!entry->nameLen)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: directory entry without name in RomFS section!", __func__);
|
|
return false;
|
|
}
|
|
|
|
// Only add entries inside the directory we're looking in
|
|
if (entry->parent == dir_offset) dirEntryCnt++;
|
|
|
|
entryOffset += round_up(ROMFS_NONAME_DIRENTRY_SIZE + entry->nameLen, 4);
|
|
}
|
|
|
|
// Now count the file entries
|
|
entryOffset = 0;
|
|
|
|
while(entryOffset < fileTableSize)
|
|
{
|
|
romfs_file *entry = (!usePatch ? (romfs_file*)((u8*)romFsContext.romfs_file_entries + entryOffset) : (romfs_file*)((u8*)bktrContext.romfs_file_entries + entryOffset));
|
|
|
|
if (!entry->nameLen)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: file entry without name in RomFS section!", __func__);
|
|
return false;
|
|
}
|
|
|
|
// Only add entries inside the directory we're looking in
|
|
if (entry->parent == dir_offset) fileEntryCnt++;
|
|
|
|
entryOffset += round_up(ROMFS_NONAME_FILEENTRY_SIZE + entry->nameLen, 4);
|
|
}
|
|
|
|
totalEntryCnt = (dirEntryCnt + fileEntryCnt);
|
|
|
|
char curName[NAME_BUF_LEN] = {'\0'};
|
|
|
|
// Silently return true if we're dealing with an empty directory
|
|
if (!totalEntryCnt) goto out;
|
|
|
|
// Allocate memory for our entries
|
|
romFsBrowserEntries = calloc(totalEntryCnt, sizeof(romfs_browser_entry));
|
|
if (!romFsBrowserEntries)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to allocate memory for file/dir attributes in RomFS section!", __func__);
|
|
return false;
|
|
}
|
|
|
|
if (!allocateFilenameBuffer(totalEntryCnt))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to allocate memory for the filename buffer!", __func__);
|
|
freeRomFsBrowserEntries();
|
|
return false;
|
|
}
|
|
|
|
if (!addStringToFilenameBuffer(".."))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to allocate memory for parent dir entry in filename buffer!", __func__);
|
|
freeRomFsBrowserEntries();
|
|
return false;
|
|
}
|
|
|
|
// Add parent directory entry ("..")
|
|
romFsBrowserEntries[0].type = ROMFS_ENTRY_DIR;
|
|
romFsBrowserEntries[0].offset = romFsParentDir;
|
|
|
|
// First add the directory entries
|
|
if ((!romFsParentDir && dirEntryCnt > 1) || (romFsParentDir && dirEntryCnt > 0))
|
|
{
|
|
entryOffset = ROMFS_NONAME_DIRENTRY_SIZE; // Always skip the first entry (root directory)
|
|
|
|
while(entryOffset < dirTableSize)
|
|
{
|
|
romfs_dir *entry = (!usePatch ? (romfs_dir*)((u8*)romFsContext.romfs_dir_entries + entryOffset) : (romfs_dir*)((u8*)bktrContext.romfs_dir_entries + entryOffset));
|
|
|
|
// Only add entries inside the directory we're looking in
|
|
if (entry->parent == dir_offset)
|
|
{
|
|
romFsBrowserEntries[i].type = ROMFS_ENTRY_DIR;
|
|
romFsBrowserEntries[i].offset = entryOffset;
|
|
|
|
snprintf(curName, entry->nameLen + 1, (char*)entry->name);
|
|
|
|
// Fix entry name length
|
|
truncateBrowserEntryName(curName);
|
|
|
|
if (!addStringToFilenameBuffer(curName))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to allocate memory for filename entry in filename buffer!", __func__);
|
|
freeRomFsBrowserEntries();
|
|
return false;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
entryOffset += round_up(ROMFS_NONAME_DIRENTRY_SIZE + entry->nameLen, 4);
|
|
}
|
|
}
|
|
|
|
// Now add the file entries
|
|
if (fileEntryCnt > 0)
|
|
{
|
|
entryOffset = 0;
|
|
|
|
while(entryOffset < fileTableSize)
|
|
{
|
|
romfs_file *entry = (!usePatch ? (romfs_file*)((u8*)romFsContext.romfs_file_entries + entryOffset) : (romfs_file*)((u8*)bktrContext.romfs_file_entries + entryOffset));
|
|
|
|
// Only add entries inside the directory we're looking in
|
|
if (entry->parent == dir_offset)
|
|
{
|
|
romFsBrowserEntries[i].type = ROMFS_ENTRY_FILE;
|
|
romFsBrowserEntries[i].offset = entryOffset;
|
|
romFsBrowserEntries[i].sizeInfo.size = entry->dataSize;
|
|
convertSize(entry->dataSize, romFsBrowserEntries[i].sizeInfo.sizeStr, MAX_CHARACTERS(romFsBrowserEntries[i].sizeInfo.sizeStr));
|
|
|
|
snprintf(curName, entry->nameLen + 1, (char*)entry->name);
|
|
|
|
// Fix entry name length
|
|
truncateBrowserEntryName(curName);
|
|
|
|
if (!addStringToFilenameBuffer(curName))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to allocate memory for filename entry in filename buffer!", __func__);
|
|
freeRomFsBrowserEntries();
|
|
return false;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
entryOffset += round_up(ROMFS_NONAME_FILEENTRY_SIZE + entry->nameLen, 4);
|
|
}
|
|
}
|
|
|
|
out:
|
|
// Update current RomFS directory offset
|
|
curRomFsDirOffset = dir_offset;
|
|
|
|
return true;
|
|
}
|
|
|
|
void printProgressBar(progress_ctx_t *progressCtx, bool calcData, u64 chunkSize)
|
|
{
|
|
if (!progressCtx) return;
|
|
|
|
if (calcData)
|
|
{
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx->now));
|
|
|
|
// Workaround to properly calculate speed for sequential dumps
|
|
u64 speedCurOffset = (progressCtx->seqDumpCurOffset ? progressCtx->seqDumpCurOffset : progressCtx->curOffset);
|
|
|
|
progressCtx->lastSpeed = (((double)(speedCurOffset + chunkSize) / (double)MiB) / (double)(progressCtx->now - progressCtx->start));
|
|
progressCtx->averageSpeed = ((SMOOTHING_FACTOR * progressCtx->lastSpeed) + ((1 - SMOOTHING_FACTOR) * progressCtx->averageSpeed));
|
|
if (!isnormal(progressCtx->averageSpeed)) progressCtx->averageSpeed = SMOOTHING_FACTOR; // Very low values
|
|
|
|
progressCtx->remainingTime = (u64)(((double)(progressCtx->totalSize - (progressCtx->curOffset + chunkSize)) / (double)MiB) / progressCtx->averageSpeed);
|
|
|
|
progressCtx->progress = (u8)(((progressCtx->curOffset + chunkSize) * 100) / progressCtx->totalSize);
|
|
}
|
|
|
|
formatETAString(progressCtx->remainingTime, progressCtx->etaInfo, MAX_CHARACTERS(progressCtx->etaInfo));
|
|
|
|
convertSize(progressCtx->curOffset + chunkSize, progressCtx->curOffsetStr, MAX_CHARACTERS(progressCtx->curOffsetStr));
|
|
|
|
uiFill(0, (progressCtx->line_offset * LINE_HEIGHT) + 8, FB_WIDTH / 4, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
uiDrawString(font_height * 2, STRING_Y_POS(progressCtx->line_offset), FONT_COLOR_RGB, "%.2lf MiB/s [ETA: %s]", progressCtx->averageSpeed, progressCtx->etaInfo);
|
|
|
|
if (progressCtx->totalSize && (progressCtx->curOffset + chunkSize) < progressCtx->totalSize)
|
|
{
|
|
uiFill(FB_WIDTH / 4, (progressCtx->line_offset * LINE_HEIGHT) + 10, FB_WIDTH / 2, LINE_HEIGHT, EMPTY_BAR_COLOR_RGB);
|
|
uiFill(FB_WIDTH / 4, (progressCtx->line_offset * LINE_HEIGHT) + 10, (((progressCtx->curOffset + chunkSize) * (u64)(FB_WIDTH / 2)) / progressCtx->totalSize), LINE_HEIGHT, FONT_COLOR_SUCCESS_RGB);
|
|
} else {
|
|
uiFill(FB_WIDTH / 4, (progressCtx->line_offset * LINE_HEIGHT) + 10, FB_WIDTH / 2, LINE_HEIGHT, FONT_COLOR_SUCCESS_RGB);
|
|
}
|
|
|
|
uiFill(FB_WIDTH - (FB_WIDTH / 4), (progressCtx->line_offset * LINE_HEIGHT) + 8, FB_WIDTH / 4, LINE_HEIGHT * 2, BG_COLOR_RGB);
|
|
uiDrawString(FB_WIDTH - (FB_WIDTH / 4) + (font_height * 2), STRING_Y_POS(progressCtx->line_offset), FONT_COLOR_RGB, "%u%% [%s / %s]", progressCtx->progress, progressCtx->curOffsetStr, progressCtx->totalSizeStr);
|
|
|
|
uiRefreshDisplay();
|
|
uiUpdateStatusMsg();
|
|
}
|
|
|
|
void setProgressBarError(progress_ctx_t *progressCtx)
|
|
{
|
|
if (!progressCtx) return;
|
|
|
|
if (progressCtx->totalSize && progressCtx->curOffset < progressCtx->totalSize)
|
|
{
|
|
uiFill(FB_WIDTH / 4, (progressCtx->line_offset * LINE_HEIGHT) + 10, FB_WIDTH / 2, LINE_HEIGHT, EMPTY_BAR_COLOR_RGB);
|
|
uiFill(FB_WIDTH / 4, (progressCtx->line_offset * LINE_HEIGHT) + 10, ((progressCtx->curOffset * (u64)(FB_WIDTH / 2)) / progressCtx->totalSize), LINE_HEIGHT, FONT_COLOR_ERROR_RGB);
|
|
} else {
|
|
uiFill(FB_WIDTH / 4, (progressCtx->line_offset * LINE_HEIGHT) + 10, FB_WIDTH / 2, LINE_HEIGHT, FONT_COLOR_ERROR_RGB);
|
|
}
|
|
}
|
|
|
|
bool cancelProcessCheck(progress_ctx_t *progressCtx)
|
|
{
|
|
if (!progressCtx) return false;
|
|
|
|
hidScanInput();
|
|
|
|
progressCtx->cancelBtnState = (hidKeysAllHeld(CONTROLLER_P1_AUTO) & KEY_B);
|
|
|
|
if (progressCtx->cancelBtnState && progressCtx->cancelBtnState != progressCtx->cancelBtnStatePrev)
|
|
{
|
|
// Cancel button has just been pressed
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx->cancelStartTmr));
|
|
} else
|
|
if (progressCtx->cancelBtnState && progressCtx->cancelBtnState == progressCtx->cancelBtnStatePrev && progressCtx->cancelStartTmr)
|
|
{
|
|
// If the cancel button has been held up to this point, check if at least CANCEL_BTN_SEC_HOLD seconds have passed
|
|
// Only perform this check if cancelStartTmr has already been set to a value greater than zero
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx->cancelEndTmr));
|
|
|
|
if ((progressCtx->cancelEndTmr - progressCtx->cancelStartTmr) >= CANCEL_BTN_SEC_HOLD) return true;
|
|
} else {
|
|
progressCtx->cancelStartTmr = progressCtx->cancelEndTmr = 0;
|
|
}
|
|
|
|
progressCtx->cancelBtnStatePrev = progressCtx->cancelBtnState;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool yesNoPrompt(const char *message)
|
|
{
|
|
if (message && strlen(message))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, message);
|
|
breaks++;
|
|
}
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "[ %s ] Yes | [ %s ] No", NINTENDO_FONT_A, NINTENDO_FONT_B);
|
|
breaks += 2;
|
|
|
|
bool ret = false;
|
|
|
|
while(true)
|
|
{
|
|
uiUpdateStatusMsg();
|
|
uiRefreshDisplay();
|
|
|
|
hidScanInput();
|
|
|
|
u64 keysDown = hidKeysAllDown(CONTROLLER_P1_AUTO);
|
|
|
|
if (keysDown & KEY_A)
|
|
{
|
|
ret = true;
|
|
break;
|
|
} else
|
|
if (keysDown & KEY_B)
|
|
{
|
|
ret = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool checkIfDumpedXciContainsCertificate(const char *xciPath)
|
|
{
|
|
if (!xciPath || !strlen(xciPath)) return false;
|
|
|
|
FILE *xciFile = NULL;
|
|
u64 xciSize = 0;
|
|
|
|
size_t read_bytes;
|
|
|
|
u8 xci_cert[CERT_SIZE];
|
|
|
|
u8 xci_cert_wiped[CERT_SIZE];
|
|
memset(xci_cert_wiped, 0xFF, CERT_SIZE);
|
|
|
|
xciFile = fopen(xciPath, "rb");
|
|
if (!xciFile) return false;
|
|
|
|
fseek(xciFile, 0, SEEK_END);
|
|
xciSize = ftell(xciFile);
|
|
rewind(xciFile);
|
|
|
|
if (xciSize < (size_t)(CERT_OFFSET + CERT_SIZE))
|
|
{
|
|
fclose(xciFile);
|
|
return false;
|
|
}
|
|
|
|
fseek(xciFile, CERT_OFFSET, SEEK_SET);
|
|
|
|
read_bytes = fread(xci_cert, 1, CERT_SIZE, xciFile);
|
|
|
|
fclose(xciFile);
|
|
|
|
if (read_bytes != (size_t)CERT_SIZE) return false;
|
|
|
|
if (memcmp(xci_cert, xci_cert_wiped, CERT_SIZE) != 0) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool checkIfDumpedNspContainsConsoleData(const char *nspPath)
|
|
{
|
|
if (!nspPath || !strlen(nspPath)) return false;
|
|
|
|
FILE *nspFile = NULL;
|
|
u64 nspSize = 0;
|
|
|
|
size_t read_bytes;
|
|
pfs0_header nspHeader;
|
|
pfs0_file_entry *nspEntries = NULL;
|
|
char *nspStrTable = NULL;
|
|
|
|
u32 i;
|
|
bool foundTik = false;
|
|
u64 tikOffset = 0, tikSize = 0;
|
|
rsa2048_sha256_ticket tikData;
|
|
|
|
const u8 titlekey_block_0x190_empty_hash[0x20] = {
|
|
0x2D, 0xFB, 0xA6, 0x33, 0x81, 0x70, 0x46, 0xC7, 0xF5, 0x59, 0xED, 0x4B, 0x93, 0x07, 0x60, 0x48,
|
|
0x43, 0x5F, 0x7E, 0x1A, 0x90, 0xF1, 0x4E, 0xB8, 0x03, 0x5C, 0x04, 0xB9, 0xEB, 0xAE, 0x25, 0x37
|
|
};
|
|
|
|
u8 titlekey_block_0x190_hash[0x20];
|
|
|
|
nspFile = fopen(nspPath, "rb");
|
|
if (!nspFile) return false;
|
|
|
|
fseek(nspFile, 0, SEEK_END);
|
|
nspSize = ftell(nspFile);
|
|
rewind(nspFile);
|
|
|
|
if (nspSize < sizeof(pfs0_header))
|
|
{
|
|
fclose(nspFile);
|
|
return false;
|
|
}
|
|
|
|
read_bytes = fread(&nspHeader, 1, sizeof(pfs0_header), nspFile);
|
|
|
|
if (read_bytes != sizeof(pfs0_header) || __builtin_bswap32(nspHeader.magic) != PFS0_MAGIC || nspSize < (sizeof(pfs0_header) + (sizeof(pfs0_file_entry) * (u64)nspHeader.file_cnt) + (u64)nspHeader.str_table_size))
|
|
{
|
|
fclose(nspFile);
|
|
return false;
|
|
}
|
|
|
|
nspEntries = calloc((u64)nspHeader.file_cnt, sizeof(pfs0_file_entry));
|
|
if (!nspEntries)
|
|
{
|
|
fclose(nspFile);
|
|
return false;
|
|
}
|
|
|
|
read_bytes = fread(nspEntries, 1, sizeof(pfs0_file_entry) * (u64)nspHeader.file_cnt, nspFile);
|
|
|
|
if (read_bytes != (sizeof(pfs0_file_entry) * (u64)nspHeader.file_cnt))
|
|
{
|
|
free(nspEntries);
|
|
fclose(nspFile);
|
|
return false;
|
|
}
|
|
|
|
nspStrTable = calloc((u64)nspHeader.str_table_size, sizeof(char));
|
|
if (!nspStrTable)
|
|
{
|
|
free(nspEntries);
|
|
fclose(nspFile);
|
|
return false;
|
|
}
|
|
|
|
read_bytes = fread(nspStrTable, 1, (u64)nspHeader.str_table_size, nspFile);
|
|
|
|
if (read_bytes != (u64)nspHeader.str_table_size)
|
|
{
|
|
free(nspStrTable);
|
|
free(nspEntries);
|
|
fclose(nspFile);
|
|
return false;
|
|
}
|
|
|
|
for(i = 0; i < nspHeader.file_cnt; i++)
|
|
{
|
|
char *curFilename = (nspStrTable + nspEntries[i].filename_offset);
|
|
|
|
if (!strncasecmp(curFilename + strlen(curFilename) - 4, ".tik", 4))
|
|
{
|
|
tikOffset = (sizeof(pfs0_header) + (sizeof(pfs0_file_entry) * (u64)nspHeader.file_cnt) + (u64)nspHeader.str_table_size + nspEntries[i].file_offset);
|
|
tikSize = nspEntries[i].file_size;
|
|
foundTik = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
free(nspStrTable);
|
|
free(nspEntries);
|
|
|
|
if (!foundTik || tikSize != ETICKET_TIK_FILE_SIZE || nspSize < (tikOffset + tikSize))
|
|
{
|
|
fclose(nspFile);
|
|
return false;
|
|
}
|
|
|
|
fseek(nspFile, tikOffset, SEEK_SET);
|
|
|
|
read_bytes = fread(&tikData, 1, ETICKET_TIK_FILE_SIZE, nspFile);
|
|
|
|
fclose(nspFile);
|
|
|
|
if (read_bytes != ETICKET_TIK_FILE_SIZE) return false;
|
|
|
|
sha256CalculateHash(titlekey_block_0x190_hash, tikData.titlekey_block + 0x10, 0xF0);
|
|
|
|
if (strncmp(tikData.sig_issuer, "Root-CA00000003-XS00000020", 26) != 0 || memcmp(titlekey_block_0x190_hash, titlekey_block_0x190_empty_hash, 0x20) != 0 || tikData.titlekey_type != ETICKET_TITLEKEY_COMMON || tikData.ticket_id != 0 || tikData.device_id != 0 || tikData.account_id != 0) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void removeDirectoryWithVerbose(const char *path, const char *msg)
|
|
{
|
|
if (!path || !strlen(path) || !msg || !strlen(msg)) return;
|
|
|
|
int initial_breaks = breaks;
|
|
|
|
breaks += 2;
|
|
|
|
if (yesNoPrompt("Do you wish to delete the data dumped up to this point? This may take a while."))
|
|
{
|
|
breaks = initial_breaks;
|
|
uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), BG_COLOR_RGB);
|
|
|
|
breaks += 2;
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, msg);
|
|
uiRefreshDisplay();
|
|
|
|
fsdevDeleteDirectoryRecursively(path);
|
|
}
|
|
|
|
breaks = initial_breaks;
|
|
uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), BG_COLOR_RGB);
|
|
uiRefreshDisplay();
|
|
}
|
|
|
|
static bool parseNSWDBRelease(xmlDocPtr doc, xmlNodePtr cur, u32 crc)
|
|
{
|
|
if (!doc || !cur) return false;
|
|
|
|
xmlChar *key = NULL;
|
|
xmlNodePtr node = cur;
|
|
|
|
u32 xmlCrc = 0;
|
|
char xmlReleaseName[256] = {'\0'};
|
|
|
|
bool found = false;
|
|
|
|
while(node)
|
|
{
|
|
if ((!xmlStrcmp(node->name, (const xmlChar*)NSWDB_XML_CHILD_IMGCRC)))
|
|
{
|
|
key = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
|
|
if (key) xmlCrc = strtoul((const char*)key, NULL, 16);
|
|
} else
|
|
if ((!xmlStrcmp(node->name, (const xmlChar*)NSWDB_XML_CHILD_RELEASENAME)))
|
|
{
|
|
key = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
|
|
if (key) snprintf(xmlReleaseName, MAX_CHARACTERS(xmlReleaseName), "%s", (const char*)key);
|
|
}
|
|
|
|
if (key)
|
|
{
|
|
xmlFree(key);
|
|
key = NULL;
|
|
}
|
|
|
|
node = node->next;
|
|
}
|
|
|
|
if (strlen(xmlReleaseName) && xmlCrc == crc)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Found matching Scene release: \"%s\" (CRC32: %08X). This is likely a good dump!", xmlReleaseName, xmlCrc);
|
|
found = true;
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
static xmlXPathObjectPtr getXPathNodeSet(xmlDocPtr doc, char *xpathExpr)
|
|
{
|
|
if (!doc || !xpathExpr || !strlen(xpathExpr)) return NULL;
|
|
|
|
xmlXPathContextPtr context = NULL;
|
|
xmlXPathObjectPtr result = NULL;
|
|
|
|
context = xmlXPathNewContext(doc);
|
|
if (!context) return NULL;
|
|
|
|
result = xmlXPathEvalExpression((xmlChar*)xpathExpr, context);
|
|
|
|
xmlXPathFreeContext(context);
|
|
|
|
if (!result) return NULL;
|
|
|
|
if (xmlXPathNodeSetIsEmpty(result->nodesetval))
|
|
{
|
|
xmlXPathFreeObject(result);
|
|
return NULL;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void gameCardDumpNSWDBCheck(u32 crc)
|
|
{
|
|
if (menuType != MENUTYPE_GAMECARD || !titleAppCount || !baseAppEntries || !gameCardInfo.hfs0PartitionCnt) return;
|
|
|
|
u32 i, j;
|
|
xmlDocPtr doc = NULL;
|
|
bool found = false;
|
|
|
|
doc = xmlParseFile(NSWDB_XML_PATH);
|
|
if (!doc)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open and/or parse \"%s\"!", __func__, NSWDB_XML_PATH);
|
|
return;
|
|
}
|
|
|
|
for(i = 0; i < titleAppCount; i++)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "//%s/%s[.//%s[contains(.,'%016lX')]]", NSWDB_XML_ROOT, NSWDB_XML_CHILD, NSWDB_XML_CHILD_TITLEID, baseAppEntries[i].titleId);
|
|
|
|
xmlXPathObjectPtr nodeSet = getXPathNodeSet(doc, strbuf);
|
|
if (!nodeSet) continue;
|
|
|
|
for(j = 0; j < (u32)nodeSet->nodesetval->nodeNr; j++)
|
|
{
|
|
xmlNodePtr node = nodeSet->nodesetval->nodeTab[j]->xmlChildrenNode;
|
|
found = parseNSWDBRelease(doc, node, crc);
|
|
if (found) break;
|
|
}
|
|
|
|
xmlXPathFreeObject(nodeSet);
|
|
|
|
if (found) break;
|
|
}
|
|
|
|
xmlFreeDoc(doc);
|
|
|
|
if (!found) uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "No match found in NSWDB.COM XML database! This could either be a bad dump or an undumped gamecard.");
|
|
}
|
|
|
|
static Result networkInit()
|
|
{
|
|
if (initNet) return 0;
|
|
|
|
Result result = socketInitializeDefault();
|
|
if (R_SUCCEEDED(result))
|
|
{
|
|
curl_global_init(CURL_GLOBAL_ALL);
|
|
initNet = true;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static void networkExit()
|
|
{
|
|
if (!initNet) return;
|
|
|
|
curl_global_cleanup();
|
|
socketExit();
|
|
initNet = false;
|
|
}
|
|
|
|
static size_t writeCurlFile(char *buffer, size_t size, size_t number_of_items, void *input_stream)
|
|
{
|
|
size_t total_size = (size * number_of_items);
|
|
if (fwrite(buffer, 1, total_size, input_stream) != total_size) return 0;
|
|
return total_size;
|
|
}
|
|
|
|
static size_t writeCurlBuffer(char *buffer, size_t size, size_t number_of_items, void *input_stream)
|
|
{
|
|
(void) input_stream;
|
|
const size_t bsz = (size * number_of_items);
|
|
|
|
if (result_sz == 0 || !result_buf)
|
|
{
|
|
result_sz = 0x1000;
|
|
result_buf = malloc(result_sz);
|
|
if (!result_buf) return 0;
|
|
}
|
|
|
|
bool need_realloc = false;
|
|
|
|
while((result_written + bsz) > result_sz)
|
|
{
|
|
result_sz <<= 1;
|
|
need_realloc = true;
|
|
}
|
|
|
|
if (need_realloc)
|
|
{
|
|
char *new_buf = realloc(result_buf, result_sz);
|
|
if (!new_buf) return 0;
|
|
result_buf = new_buf;
|
|
}
|
|
|
|
memcpy(result_buf + result_written, buffer, bsz);
|
|
result_written += bsz;
|
|
return bsz;
|
|
}
|
|
|
|
static bool performCurlRequest(CURL *curl, const char *url, FILE *filePtr, bool forceHttps, bool verbose)
|
|
{
|
|
if (!curl || !url || !strlen(url))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to perform CURL request!", __func__);
|
|
return false;
|
|
}
|
|
|
|
curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 102400L);
|
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, HTTP_USER_AGENT);
|
|
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_NOBODY, 0L);
|
|
curl_easy_setopt(curl, CURLOPT_HEADER, 0L);
|
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
|
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 50L);
|
|
if (forceHttps) curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS);
|
|
|
|
if (filePtr)
|
|
{
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCurlFile);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, filePtr);
|
|
} else {
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCurlBuffer);
|
|
}
|
|
|
|
CURLcode res;
|
|
long http_code = 0;
|
|
double size = 0.0;
|
|
bool success = false;
|
|
|
|
res = curl_easy_perform(curl);
|
|
|
|
result_sz = result_written = 0;
|
|
|
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
|
curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD, &size);
|
|
|
|
if (res == CURLE_OK && http_code >= 200 && http_code <= 299 && size > 0)
|
|
{
|
|
if (verbose) uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Successfully downloaded %.0lf bytes!", size);
|
|
success = true;
|
|
} else {
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: CURL request failed for \"%s\" endpoint!\nHTTP status code: %ld", __func__, url, http_code);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
void noIntroDumpCheck(bool isDigital, u32 crc)
|
|
{
|
|
Result result;
|
|
CURL *curl = NULL;
|
|
char noIntroUrl[128] = {'\0'};
|
|
|
|
// Build URL
|
|
// f = "cart" (XCI) or "dlc" (NSP)
|
|
// c = search by code (Title ID or serial)
|
|
// crc = search by CRC32 checksum
|
|
snprintf(noIntroUrl, MAX_CHARACTERS(noIntroUrl), "%s?f=%s&crc=%08X", NOINTRO_DOM_CHECK_URL, (isDigital ? "dlc" : "cart"), crc);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Performing CRC32 checksum lookup against No-Intro, please wait...");
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
result = networkInit();
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to initialize socket! (%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
|
|
curl = curl_easy_init();
|
|
if (!curl)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to initialize CURL context!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if (!performCurlRequest(curl, noIntroUrl, NULL, true, false)) goto out;
|
|
|
|
if (!strlen(result_buf) || !strncmp(result_buf, "unknown crc32", 13))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "No match found in No-Intro database! This could either be a bad dump or an undumped %s.", (isDigital ? "digital title" : "gamecard"));
|
|
goto out;
|
|
}
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Found matching No-Intro database entry: \"%s\". This is likely a good dump!", result_buf);
|
|
|
|
out:
|
|
if (result_buf)
|
|
{
|
|
free(result_buf);
|
|
result_buf = NULL;
|
|
}
|
|
|
|
if (curl) curl_easy_cleanup(curl);
|
|
|
|
if (R_SUCCEEDED(result)) networkExit();
|
|
}
|
|
|
|
void updateNSWDBXml()
|
|
{
|
|
Result result;
|
|
CURL *curl = NULL;
|
|
bool success = false;
|
|
FILE *nswdbXml = NULL;
|
|
|
|
result = networkInit();
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to initialize socket! (%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
|
|
char xmlPath[256] = {'\0'};
|
|
snprintf(xmlPath, MAX_CHARACTERS(xmlPath), "%s.tmp", NSWDB_XML_PATH);
|
|
|
|
curl = curl_easy_init();
|
|
if (!curl)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to initialize CURL context!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
nswdbXml = fopen(xmlPath, "wb");
|
|
if (!nswdbXml)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open \"%s\" in write mode!", __func__, NSWDB_XML_URL);
|
|
goto out;
|
|
}
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Downloading XML database from \"%s\", please wait...", NSWDB_XML_URL);
|
|
breaks++;
|
|
|
|
appletModeOperationWarning();
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
changeHomeButtonBlockStatus(true);
|
|
|
|
success = performCurlRequest(curl, NSWDB_XML_URL, nswdbXml, false, true);
|
|
|
|
changeHomeButtonBlockStatus(false);
|
|
|
|
out:
|
|
if (nswdbXml) fclose(nswdbXml);
|
|
|
|
if (success)
|
|
{
|
|
remove(NSWDB_XML_PATH);
|
|
rename(xmlPath, NSWDB_XML_PATH);
|
|
} else {
|
|
remove(xmlPath);
|
|
}
|
|
|
|
if (curl) curl_easy_cleanup(curl);
|
|
|
|
if (R_SUCCEEDED(result)) networkExit();
|
|
|
|
breaks += 2;
|
|
}
|
|
|
|
static int versionNumCmp(char *ver1, char *ver2)
|
|
{
|
|
int i, curPart, res;
|
|
char *token = NULL;
|
|
|
|
// Define a struct for comparison purposes
|
|
typedef struct {
|
|
int major;
|
|
int minor;
|
|
int build;
|
|
} version_t;
|
|
|
|
version_t versionNum1, versionNum2;
|
|
memset(&versionNum1, 0, sizeof(version_t));
|
|
memset(&versionNum2, 0, sizeof(version_t));
|
|
|
|
// Create copies of the version strings to avoid modifications by strtok()
|
|
char ver1tok[64] = {'\0'};
|
|
snprintf(ver1tok, 63, ver1);
|
|
|
|
char ver2tok[64] = {'\0'};
|
|
snprintf(ver2tok, 63, ver2);
|
|
|
|
// Parse version string 1
|
|
i = 0;
|
|
token = strtok(ver1tok, ".");
|
|
while(token != NULL && i < 3)
|
|
{
|
|
curPart = atoi(token);
|
|
|
|
switch(i)
|
|
{
|
|
case 0:
|
|
versionNum1.major = curPart;
|
|
break;
|
|
case 1:
|
|
versionNum1.minor = curPart;
|
|
break;
|
|
case 2:
|
|
versionNum1.build = curPart;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
token = strtok(NULL, ".");
|
|
|
|
i++;
|
|
}
|
|
|
|
// Parse version string 2
|
|
i = 0;
|
|
token = strtok(ver2tok, ".");
|
|
while(token != NULL && i < 3)
|
|
{
|
|
curPart = atoi(token);
|
|
|
|
switch(i)
|
|
{
|
|
case 0:
|
|
versionNum2.major = curPart;
|
|
break;
|
|
case 1:
|
|
versionNum2.minor = curPart;
|
|
break;
|
|
case 2:
|
|
versionNum2.build = curPart;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
token = strtok(NULL, ".");
|
|
|
|
i++;
|
|
}
|
|
|
|
// Compare version_t structs
|
|
if (versionNum1.major == versionNum2.major)
|
|
{
|
|
if (versionNum1.minor == versionNum2.minor)
|
|
{
|
|
if (versionNum1.build == versionNum2.build)
|
|
{
|
|
res = 0;
|
|
} else
|
|
if (versionNum1.build < versionNum2.build)
|
|
{
|
|
res = -1;
|
|
} else {
|
|
res = 1;
|
|
}
|
|
} else
|
|
if (versionNum1.minor < versionNum2.minor)
|
|
{
|
|
res = -1;
|
|
} else {
|
|
res = 1;
|
|
}
|
|
} else
|
|
if (versionNum1.major < versionNum2.major)
|
|
{
|
|
res = -1;
|
|
} else {
|
|
res = 1;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static struct json_object *retrieveJsonObjMemberByNameAndType(struct json_object *jobj, char *memberName, json_type memberType)
|
|
{
|
|
if (!jobj || !memberName || !strlen(memberName))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to retrieve member by name and type from JSON object!", __func__);
|
|
return NULL;
|
|
}
|
|
|
|
struct json_object *memberObj = NULL;
|
|
json_type memberObjType;
|
|
|
|
if (!json_object_object_get_ex(jobj, memberName, &memberObj))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to retrieve member \"%s\" from JSON object!", __func__, memberName);
|
|
return NULL;
|
|
}
|
|
|
|
memberObjType = json_object_get_type(memberObj);
|
|
if (memberObjType != memberType)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid type for member \"%s\" in JSON object! (got \"%s\", expected \"%s\")", __func__, memberName, json_type_to_name(memberObjType), json_type_to_name(memberType));
|
|
return NULL;
|
|
}
|
|
|
|
return memberObj;
|
|
}
|
|
|
|
static const char *retrieveJsonObjStrMemberContentsByName(struct json_object *jobj, char *memberName)
|
|
{
|
|
if (!jobj || !memberName || !strlen(memberName))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to retrieve string member contents by name from JSON object!", __func__);
|
|
return NULL;
|
|
}
|
|
|
|
struct json_object *memberObj = retrieveJsonObjMemberByNameAndType(jobj, memberName, json_type_string);
|
|
if (!memberObj) return NULL;
|
|
|
|
const char *memberObjStr = json_object_get_string(memberObj);
|
|
if (!memberObjStr || !strlen(memberObjStr))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: string member \"%s\" from JSON object is empty!", __func__, memberName);
|
|
return NULL;
|
|
}
|
|
|
|
return memberObjStr;
|
|
}
|
|
|
|
static struct json_object *retrieveJsonObjArrayMemberByName(struct json_object *jobj, char *memberName, size_t *outputArrayLength)
|
|
{
|
|
if (!jobj || !memberName || !strlen(memberName) || !outputArrayLength)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to retrieve array member by name from JSON object!", __func__);
|
|
return NULL;
|
|
}
|
|
|
|
struct json_object *memberObj = retrieveJsonObjMemberByNameAndType(jobj, memberName, json_type_array);
|
|
if (memberObj) *outputArrayLength = json_object_array_length(memberObj);
|
|
|
|
return memberObj;
|
|
}
|
|
|
|
static struct json_object *retrieveJsonObjArrayElementByIndex(struct json_object *jobj, size_t idx)
|
|
{
|
|
if (!jobj || json_object_get_type(jobj) != json_type_array)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to retrieve element by index from JSON array object!", __func__);
|
|
return NULL;
|
|
}
|
|
|
|
struct json_object *memberObjArrayElement = json_object_array_get_idx(jobj, idx);
|
|
if (!memberObjArrayElement) uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to retrieve element at index %lu from JSON array object!", __func__, idx);
|
|
|
|
return memberObjArrayElement;
|
|
}
|
|
|
|
bool updateApplication()
|
|
{
|
|
if (envIsNso())
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to update application. Not running as a NRO.", __func__);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
Result result;
|
|
CURL *curl = NULL;
|
|
FILE *nxDumpToolNro = NULL;
|
|
|
|
char releaseTag[32] = {'\0'};
|
|
bool success = false;
|
|
|
|
size_t i, assetsCnt = 0;
|
|
struct json_object *jobj = NULL, *assets = NULL;
|
|
const char *releaseNameObjStr = NULL, *dlUrlObjStr = NULL;
|
|
|
|
char nroPath[NAME_BUF_LEN] = {'\0'};
|
|
snprintf(nroPath, MAX_CHARACTERS(nroPath), "%s.tmp", (appLaunchPath ? appLaunchPath : NRO_PATH));
|
|
|
|
result = networkInit();
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to initialize socket! (%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
|
|
curl = curl_easy_init();
|
|
if (!curl)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to initialize CURL context!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Requesting latest release information from \"%s\"...", GITHUB_API_URL);
|
|
breaks++;
|
|
|
|
uiRefreshDisplay();
|
|
|
|
if (!performCurlRequest(curl, GITHUB_API_URL, NULL, true, false)) goto out;
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Parsing response JSON data...");
|
|
breaks++;
|
|
|
|
uiRefreshDisplay();
|
|
|
|
jobj = json_tokener_parse(result_buf);
|
|
if (!jobj)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to parse JSON response!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
releaseNameObjStr = retrieveJsonObjStrMemberContentsByName(jobj, GITHUB_API_JSON_RELEASE_NAME);
|
|
if (!releaseNameObjStr) goto out;
|
|
|
|
snprintf(releaseTag, MAX_CHARACTERS(releaseTag), releaseNameObjStr);
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Latest release: %s.", releaseTag);
|
|
breaks++;
|
|
|
|
uiRefreshDisplay();
|
|
|
|
// Remove the first character from the release name if it's v/V/r/R
|
|
if (releaseTag[0] == 'v' || releaseTag[0] == 'V' || releaseTag[0] == 'r' || releaseTag[0] == 'R')
|
|
{
|
|
u32 releaseTagLen = strlen(releaseTag);
|
|
memmove(releaseTag, releaseTag + 1, releaseTagLen - 1);
|
|
releaseTag[releaseTagLen - 1] = '\0';
|
|
}
|
|
|
|
// Compare versions
|
|
if (versionNumCmp(releaseTag, APP_VERSION) <= 0)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "You already have the latest version!");
|
|
breaks += 2;
|
|
|
|
// Ask the user if they want to perform a forced update
|
|
int cur_breaks = breaks;
|
|
|
|
if (yesNoPrompt("Do you want to perform a forced update?"))
|
|
{
|
|
// 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();
|
|
} else {
|
|
breaks -= 2;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
assets = retrieveJsonObjArrayMemberByName(jobj, GITHUB_API_JSON_ASSETS, &assetsCnt);
|
|
if (!assets) goto out;
|
|
|
|
// Cycle through the assets to find the right download URL
|
|
for(i = 0; i < assetsCnt; i++)
|
|
{
|
|
struct json_object *assetElement = retrieveJsonObjArrayElementByIndex(assets, i);
|
|
if (!assetElement) break;
|
|
|
|
const char *assetName = retrieveJsonObjStrMemberContentsByName(assetElement, GITHUB_API_JSON_ASSETS_NAME);
|
|
if (!assetName) break;
|
|
|
|
if (!strncmp(assetName, NRO_NAME, strlen(assetName)))
|
|
{
|
|
// Found it
|
|
dlUrlObjStr = retrieveJsonObjStrMemberContentsByName(assetElement, GITHUB_API_JSON_ASSETS_DL_URL);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!dlUrlObjStr)
|
|
{
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to locate NRO download URL!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Download URL: \"%s\".", dlUrlObjStr);
|
|
breaks++;
|
|
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Please wait...");
|
|
breaks++;
|
|
|
|
appletModeOperationWarning();
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
changeHomeButtonBlockStatus(true);
|
|
|
|
nxDumpToolNro = fopen(nroPath, "wb");
|
|
if (!nxDumpToolNro)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open \"%s\" in write mode!", __func__, nroPath);
|
|
goto out;
|
|
}
|
|
|
|
curl_easy_reset(curl);
|
|
|
|
success = performCurlRequest(curl, dlUrlObjStr, nxDumpToolNro, true, true);
|
|
if (!success) goto out;
|
|
|
|
breaks++;
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Please restart the application to reflect the changes.");
|
|
|
|
out:
|
|
if (nxDumpToolNro) fclose(nxDumpToolNro);
|
|
|
|
if (strlen(nroPath))
|
|
{
|
|
if (success)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), nroPath);
|
|
nroPath[strlen(nroPath) - 4] = '\0';
|
|
|
|
remove(nroPath);
|
|
rename(strbuf, nroPath);
|
|
} else {
|
|
remove(nroPath);
|
|
}
|
|
}
|
|
|
|
if (jobj) json_object_put(jobj);
|
|
|
|
if (result_buf)
|
|
{
|
|
free(result_buf);
|
|
result_buf = NULL;
|
|
}
|
|
|
|
if (curl) curl_easy_cleanup(curl);
|
|
|
|
if (R_SUCCEEDED(result)) networkExit();
|
|
|
|
breaks += 2;
|
|
|
|
changeHomeButtonBlockStatus(false);
|
|
|
|
return success;
|
|
}
|