mirror of
https://github.com/DarkMatterCore/nxdumptool.git
synced 2024-11-22 18:26:39 +00:00
4763 lines
162 KiB
C
4763 lines
162 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 PadState g_padState = {0};
|
|
|
|
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;
|
|
|
|
static const u32 g_smAtmosphereHasService = 65100;
|
|
static const SplConfigItem SplConfigItem_ExosphereApiVersion = (SplConfigItem)65000;
|
|
static const u32 g_atmosphereTipcVersion = MAKEHOSVERSION(0, 19, 0);
|
|
|
|
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.batchDumpCfg.isFat32 = 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 bool isGameCardInserted()
|
|
{
|
|
bool inserted = false;
|
|
fsDeviceOperatorIsGameCardInserted(&(gameCardInfo.fsOperatorInstance), &inserted);
|
|
return inserted;
|
|
}
|
|
|
|
static void changeAtomicBool(volatile bool *ptr, bool value)
|
|
{
|
|
if (!ptr) return;
|
|
|
|
if (value)
|
|
{
|
|
__atomic_test_and_set(ptr, __ATOMIC_SEQ_CST);
|
|
} else {
|
|
__atomic_clear(ptr, __ATOMIC_SEQ_CST);
|
|
}
|
|
}
|
|
|
|
static void *fsGameCardDetectionThreadFunc(void *arg)
|
|
{
|
|
(void)arg;
|
|
|
|
Result result = 0;
|
|
int idx = 0;
|
|
|
|
Waiter gameCardEventWaiter = waiterForEvent(&(gameCardInfo.fsGameCardKernelEvent));
|
|
Waiter exitEventWaiter = waiterForUEvent(&exitEvent);
|
|
|
|
changeAtomicBool(&gameCardInfoLoaded, false);
|
|
|
|
/* Retrieve initial gamecard status */
|
|
bool curGcStatus = isGameCardInserted();
|
|
changeAtomicBool(&(gameCardInfo.isInserted), curGcStatus);
|
|
|
|
while(true)
|
|
{
|
|
// Wait until an event is triggered
|
|
result = waitMulti(&idx, -1, gameCardEventWaiter, exitEventWaiter);
|
|
if (R_FAILED(result)) continue;
|
|
|
|
// Exit event triggered
|
|
if (idx == 1) break;
|
|
|
|
// Retrieve current gamecard status
|
|
// Only proceed if we're dealing with a status change
|
|
curGcStatus = isGameCardInserted();
|
|
changeAtomicBool(&(gameCardInfo.isInserted), curGcStatus);
|
|
if (!curGcStatus && gameCardInfoLoaded) changeAtomicBool(&gameCardInfoLoaded, false);
|
|
}
|
|
|
|
waitMulti(&idx, 0, gameCardEventWaiter, exitEventWaiter);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool createGameCardDetectionThread()
|
|
{
|
|
int ret1 = 0, ret2 = 0;
|
|
pthread_attr_t attr;
|
|
|
|
ret1 = pthread_attr_init(&attr);
|
|
if (ret1 != 0)
|
|
{
|
|
uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "%s: failed to initialize thread attributes! (%d)", __func__, ret1);
|
|
return false;
|
|
}
|
|
|
|
ret1 = pthread_create(&gameCardDetectionThread, &attr, &fsGameCardDetectionThreadFunc, NULL);
|
|
if (ret1 != 0) uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "%s: failed to create thread! (%d)", __func__, ret1);
|
|
|
|
ret2 = pthread_attr_destroy(&attr);
|
|
if (ret2 != 0) uiDrawString(STRING_X_POS, (ret1 == 0 ? 8 : STRING_Y_POS(1)), FONT_COLOR_ERROR_RGB, "%s: failed to destroy thread attributes! (%d)", __func__, ret2);
|
|
|
|
if (ret1 != 0 || ret2 != 0) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void closeGameCardHandle()
|
|
{
|
|
svcCloseHandle(gameCardInfo.fsGameCardHandle.value);
|
|
gameCardInfo.fsGameCardHandle.value = 0;
|
|
}
|
|
|
|
void closeGameCardStoragePartition()
|
|
{
|
|
if (!gameCardInfo.curIStorageIndex || gameCardInfo.curIStorageIndex >= ISTORAGE_PARTITION_INVALID) return;
|
|
|
|
fsStorageClose(&(gameCardInfo.fsGameCardStorage));
|
|
memset(&(gameCardInfo.fsGameCardStorage), 0, sizeof(FsStorage));
|
|
|
|
closeGameCardHandle();
|
|
|
|
gameCardInfo.curIStorageIndex = ISTORAGE_PARTITION_NONE;
|
|
}
|
|
|
|
Result openGameCardStoragePartition(openIStoragePartition partitionIndex)
|
|
{
|
|
// Check if the provided IStorage index is valid
|
|
if (!partitionIndex || partitionIndex >= ISTORAGE_PARTITION_INVALID) return MAKERESULT(Module_Libnx, LibnxError_IoError);
|
|
|
|
// Safety check: check if we have already opened an IStorage instance
|
|
if (gameCardInfo.curIStorageIndex && gameCardInfo.curIStorageIndex < ISTORAGE_PARTITION_INVALID)
|
|
{
|
|
// If the opened IStorage instance is the same as the requested one, just return right away
|
|
if (gameCardInfo.curIStorageIndex == partitionIndex) return 0;
|
|
|
|
// Otherwise, close the current IStorage instance
|
|
closeGameCardStoragePartition();
|
|
}
|
|
|
|
u8 i;
|
|
u32 idx = (u32)(partitionIndex - 1);
|
|
Result res1 = 0, res2 = 0, out = 0;
|
|
|
|
// 10 tries
|
|
for(i = 0; i < 10; i++)
|
|
{
|
|
// First try to retrieve the IStorage partition handle using the current gamecard handle
|
|
res1 = fsOpenGameCardStorage(&(gameCardInfo.fsGameCardStorage), &(gameCardInfo.fsGameCardHandle), idx);
|
|
if (R_SUCCEEDED(res1)) break;
|
|
|
|
// If the previous call failed, we may have an invalid handle, so let's close the current one and try to retrieve a new one
|
|
closeGameCardHandle();
|
|
res2 = fsDeviceOperatorGetGameCardHandle(&(gameCardInfo.fsOperatorInstance), &(gameCardInfo.fsGameCardHandle));
|
|
}
|
|
|
|
if (R_SUCCEEDED(res1) && R_SUCCEEDED(res2))
|
|
{
|
|
// Update current IStorage index
|
|
gameCardInfo.curIStorageIndex = partitionIndex;
|
|
} else {
|
|
// res2 takes precedence over res1
|
|
out = (R_FAILED(res2) ? res2 : res1);
|
|
|
|
// Close leftover gamecard handle
|
|
closeGameCardHandle();
|
|
}
|
|
|
|
// If everything worked properly, a functional gamecard handle + IStorage handle are guaranteed up to this point
|
|
return out;
|
|
}
|
|
|
|
Result readGameCardStoragePartition(u64 off, void *buf, size_t len)
|
|
{
|
|
if (!gameCardInfo.curIStorageIndex || gameCardInfo.curIStorageIndex >= ISTORAGE_PARTITION_INVALID || !buf || !len) return MAKERESULT(Module_Libnx, LibnxError_IoError);
|
|
|
|
// Optimization for reads that are already aligned to MEDIA_UNIT_SIZE bytes
|
|
if (!(off % MEDIA_UNIT_SIZE) && !(len % MEDIA_UNIT_SIZE)) return fsStorageRead(&(gameCardInfo.fsGameCardStorage), off, buf, len);
|
|
|
|
Result result;
|
|
u8 *outBuf = (u8*)buf;
|
|
|
|
u64 block_start_offset = (off - (off % MEDIA_UNIT_SIZE));
|
|
u64 block_end_offset = (u64)round_up(off + len, MEDIA_UNIT_SIZE);
|
|
u64 block_size = (block_end_offset - block_start_offset);
|
|
|
|
u64 block_size_used = (block_size > GAMECARD_READ_BUFFER_SIZE ? GAMECARD_READ_BUFFER_SIZE : block_size);
|
|
u64 output_block_size = (block_size > GAMECARD_READ_BUFFER_SIZE ? (GAMECARD_READ_BUFFER_SIZE - (off - block_start_offset)) : len);
|
|
|
|
result = fsStorageRead(&(gameCardInfo.fsGameCardStorage), block_start_offset, gcReadBuf, block_size_used);
|
|
if (R_FAILED(result)) return result;
|
|
|
|
memcpy(outBuf, gcReadBuf + (off - block_start_offset), output_block_size);
|
|
|
|
if (block_size > GAMECARD_READ_BUFFER_SIZE) return readGameCardStoragePartition(off + output_block_size, outBuf + output_block_size, len - output_block_size);
|
|
|
|
return result;
|
|
}
|
|
|
|
static Result getGameCardStoragePartitionSize(u64 *out)
|
|
{
|
|
if (!gameCardInfo.curIStorageIndex || gameCardInfo.curIStorageIndex >= ISTORAGE_PARTITION_INVALID || !out) return MAKERESULT(Module_Libnx, LibnxError_IoError);
|
|
|
|
return fsStorageGetSize(&(gameCardInfo.fsGameCardStorage), (s64*)out);
|
|
}
|
|
|
|
bool mountSysEmmcPartition()
|
|
{
|
|
Result result = 0;
|
|
FRESULT fr = FR_OK;
|
|
|
|
result = fsOpenBisStorage(&fatFsStorage, FsBisPartitionId_System);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "%s: failed to open BIS System partition! (0x%08X)", __func__, result);
|
|
return false;
|
|
}
|
|
|
|
fatFsObj = calloc(1, sizeof(FATFS));
|
|
if (!fatFsObj)
|
|
{
|
|
uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "%s: failed to allocate memory for FatFs object!", __func__);
|
|
return false;
|
|
}
|
|
|
|
fr = f_mount(fatFsObj, BIS_MOUNT_NAME, 1);
|
|
if (fr != FR_OK)
|
|
{
|
|
uiDrawString(STRING_DEFAULT_POS, FONT_COLOR_ERROR_RGB, "%s: failed to mount BIS System partition! (%u)", __func__, fr);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void unmountSysEmmcPartition()
|
|
{
|
|
if (fatFsObj)
|
|
{
|
|
f_unmount(BIS_MOUNT_NAME);
|
|
free(fatFsObj);
|
|
fatFsObj = NULL;
|
|
}
|
|
|
|
if (serviceIsActive(&(fatFsStorage.s)))
|
|
{
|
|
fsStorageClose(&fatFsStorage);
|
|
memset(&fatFsStorage, 0, sizeof(FsStorage));
|
|
}
|
|
}
|
|
|
|
static bool getExosphereApiVersion(u32 *out)
|
|
{
|
|
if (!out) return false;
|
|
|
|
Result rc = 0;
|
|
u64 version = 0;
|
|
|
|
rc = splGetConfig(SplConfigItem_ExosphereApiVersion, &version);
|
|
if (R_FAILED(rc)) return false;
|
|
|
|
*out = (u32)((version >> 40) & 0xFFFFFF);
|
|
|
|
return true;
|
|
}
|
|
|
|
static Result smAtmosphereHasService(bool *out, SmServiceName name)
|
|
{
|
|
u8 tmp = 0;
|
|
Result rc = 0;
|
|
u32 version = 0;
|
|
|
|
if (hosversionAtLeast(12, 0, 0) || (getExosphereApiVersion(&version) && version >= g_atmosphereTipcVersion))
|
|
{
|
|
rc = tipcDispatchInOut(smGetServiceSessionTipc(), g_smAtmosphereHasService, name, tmp);
|
|
} else {
|
|
rc = serviceDispatchInOut(smGetServiceSession(), g_smAtmosphereHasService, name, tmp);
|
|
}
|
|
|
|
if (R_SUCCEEDED(rc) && out) *out = tmp;
|
|
|
|
return rc;
|
|
}
|
|
|
|
static bool isServiceRunning(const char *name)
|
|
{
|
|
if (!name || !*name) return false;
|
|
|
|
bool out = false;
|
|
SmServiceName srv_name = smEncodeName(name);
|
|
|
|
Result rc = smAtmosphereHasService(&out, srv_name);
|
|
return (R_SUCCEEDED(rc) && out);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
void delay(u8 seconds)
|
|
{
|
|
if (!seconds) return;
|
|
|
|
u64 nanoseconds = (seconds * (u64)1000000000);
|
|
svcSleepThread(nanoseconds);
|
|
|
|
uiRefreshDisplay();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
static bool getSdCardFreeSpace(u64 *out)
|
|
{
|
|
Result result;
|
|
FsFileSystem *sdfs = NULL;
|
|
u64 size = 0;
|
|
|
|
sdfs = fsdevGetDeviceFileSystem("sdmc:");
|
|
if (!sdfs)
|
|
{
|
|
uiStatusMsg("%s: fsdevGetDefaultFileSystem failed!", __func__);
|
|
return false;
|
|
}
|
|
|
|
result = fsFsGetFreeSpace(sdfs, "/", (s64*)&size);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiStatusMsg("%s: fsFsGetFreeSpace failed! (0x%08X)", __func__, result);
|
|
return false;
|
|
}
|
|
|
|
*out = size;
|
|
|
|
return true;
|
|
}
|
|
|
|
void convertSize(u64 size, char *out, size_t outSize)
|
|
{
|
|
if (!out || !outSize) return;
|
|
|
|
double bytes = (double)size;
|
|
|
|
if (bytes < 1000.0)
|
|
{
|
|
snprintf(out, outSize, "%.0lf B", bytes);
|
|
} else
|
|
if (bytes < (10.0 * KiB))
|
|
{
|
|
snprintf(out, outSize, "%.2lf KiB", floor((bytes * 100.0) / KiB) / 100.0);
|
|
} else
|
|
if (bytes < (100.0 * KiB))
|
|
{
|
|
snprintf(out, outSize, "%.1lf KiB", floor((bytes * 10.0) / KiB) / 10.0);
|
|
} else
|
|
if (bytes < (1000.0 * KiB))
|
|
{
|
|
snprintf(out, outSize, "%.0lf KiB", floor(bytes / KiB));
|
|
} else
|
|
if (bytes < (10.0 * MiB))
|
|
{
|
|
snprintf(out, outSize, "%.2lf MiB", floor((bytes * 100.0) / MiB) / 100.0);
|
|
} else
|
|
if (bytes < (100.0 * MiB))
|
|
{
|
|
snprintf(out, outSize, "%.1lf MiB", floor((bytes * 10.0) / MiB) / 10.0);
|
|
} else
|
|
if (bytes < (1000.0 * MiB))
|
|
{
|
|
snprintf(out, outSize, "%.0lf MiB", floor(bytes / MiB));
|
|
} else
|
|
if (bytes < (10.0 * GiB))
|
|
{
|
|
snprintf(out, outSize, "%.2lf GiB", floor((bytes * 100.0) / GiB) / 100.0);
|
|
} else
|
|
if (bytes < (100.0 * GiB))
|
|
{
|
|
snprintf(out, outSize, "%.1lf GiB", floor((bytes * 10.0) / GiB) / 10.0);
|
|
} else {
|
|
snprintf(out, outSize, "%.0lf GiB", floor(bytes / GiB));
|
|
}
|
|
}
|
|
|
|
void updateFreeSpace()
|
|
{
|
|
getSdCardFreeSpace(&freeSpace);
|
|
convertSize(freeSpace, freeSpaceStr, MAX_CHARACTERS(freeSpaceStr));
|
|
}
|
|
|
|
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 initExeFsContext()
|
|
{
|
|
memset(&exeFsContext, 0, sizeof(exefs_ctx_t));
|
|
}
|
|
|
|
void freeExeFsContext()
|
|
{
|
|
if (exeFsContext.storageId == NcmStorageId_GameCard) closeGameCardStoragePartition();
|
|
exeFsContext.storageId = NcmStorageId_None;
|
|
|
|
// Remember to close this NCM service resource
|
|
ncmContentStorageClose(&(exeFsContext.ncmStorage));
|
|
memset(&(exeFsContext.ncmStorage), 0, sizeof(NcmContentStorage));
|
|
|
|
if (exeFsContext.exefs_entries != NULL)
|
|
{
|
|
free(exeFsContext.exefs_entries);
|
|
exeFsContext.exefs_entries = NULL;
|
|
}
|
|
|
|
if (exeFsContext.exefs_str_table != NULL)
|
|
{
|
|
free(exeFsContext.exefs_str_table);
|
|
exeFsContext.exefs_str_table = NULL;
|
|
}
|
|
}
|
|
|
|
void initRomFsContext()
|
|
{
|
|
memset(&romFsContext, 0, sizeof(romfs_ctx_t));
|
|
}
|
|
|
|
void freeRomFsContext()
|
|
{
|
|
if (romFsContext.storageId == NcmStorageId_GameCard) closeGameCardStoragePartition();
|
|
romFsContext.storageId = NcmStorageId_None;
|
|
|
|
// Remember to close this NCM service resource
|
|
ncmContentStorageClose(&(romFsContext.ncmStorage));
|
|
memset(&(romFsContext.ncmStorage), 0, sizeof(NcmContentStorage));
|
|
|
|
if (romFsContext.romfs_dir_entries != NULL)
|
|
{
|
|
free(romFsContext.romfs_dir_entries);
|
|
romFsContext.romfs_dir_entries = NULL;
|
|
}
|
|
|
|
if (romFsContext.romfs_file_entries != NULL)
|
|
{
|
|
free(romFsContext.romfs_file_entries);
|
|
romFsContext.romfs_file_entries = NULL;
|
|
}
|
|
}
|
|
|
|
void initBktrContext()
|
|
{
|
|
memset(&bktrContext, 0, sizeof(bktr_ctx_t));
|
|
}
|
|
|
|
void freeBktrContext()
|
|
{
|
|
if (bktrContext.storageId == NcmStorageId_GameCard) closeGameCardStoragePartition();
|
|
bktrContext.storageId = NcmStorageId_None;
|
|
|
|
// Remember to close this NCM service resource
|
|
ncmContentStorageClose(&(bktrContext.ncmStorage));
|
|
memset(&(bktrContext.ncmStorage), 0, sizeof(NcmContentStorage));
|
|
|
|
if (bktrContext.relocation_block != NULL)
|
|
{
|
|
free(bktrContext.relocation_block);
|
|
bktrContext.relocation_block = NULL;
|
|
}
|
|
|
|
if (bktrContext.subsection_block != NULL)
|
|
{
|
|
free(bktrContext.subsection_block);
|
|
bktrContext.subsection_block = NULL;
|
|
}
|
|
|
|
if (bktrContext.romfs_dir_entries != NULL)
|
|
{
|
|
free(bktrContext.romfs_dir_entries);
|
|
bktrContext.romfs_dir_entries = NULL;
|
|
}
|
|
|
|
if (bktrContext.romfs_file_entries != NULL)
|
|
{
|
|
free(bktrContext.romfs_file_entries);
|
|
bktrContext.romfs_file_entries = NULL;
|
|
}
|
|
|
|
bktrContext.use_base_romfs = false;
|
|
}
|
|
|
|
static void freeGameCardInfo()
|
|
{
|
|
u32 i;
|
|
|
|
memset(&(gameCardInfo.header), 0, sizeof(gamecard_header_t));
|
|
|
|
if (gameCardInfo.rootHfs0Header)
|
|
{
|
|
free(gameCardInfo.rootHfs0Header);
|
|
gameCardInfo.rootHfs0Header = NULL;
|
|
}
|
|
|
|
if (gameCardInfo.hfs0Partitions)
|
|
{
|
|
for(i = 0; i < gameCardInfo.hfs0PartitionCnt; i++)
|
|
{
|
|
if (gameCardInfo.hfs0Partitions[i].header) free(gameCardInfo.hfs0Partitions[i].header);
|
|
}
|
|
|
|
free(gameCardInfo.hfs0Partitions);
|
|
gameCardInfo.hfs0Partitions = NULL;
|
|
}
|
|
|
|
gameCardInfo.hfs0PartitionCnt = 0;
|
|
|
|
gameCardInfo.size = 0;
|
|
memset(gameCardInfo.sizeStr, 0, sizeof(gameCardInfo.sizeStr));
|
|
|
|
gameCardInfo.trimmedSize = 0;
|
|
memset(gameCardInfo.trimmedSizeStr, 0, sizeof(gameCardInfo.trimmedSizeStr));
|
|
|
|
for(i = 0; i < ISTORAGE_PARTITION_CNT; i++) gameCardInfo.IStoragePartitionSizes[i] = 0;
|
|
|
|
gameCardInfo.updateTitleId = 0;
|
|
gameCardInfo.updateVersion = 0;
|
|
memset(gameCardInfo.updateVersionStr, 0, sizeof(gameCardInfo.updateVersionStr));
|
|
|
|
closeGameCardStoragePartition();
|
|
}
|
|
|
|
static void freeOrphanPatchOrAddOnList()
|
|
{
|
|
if (orphanEntries != NULL)
|
|
{
|
|
free(orphanEntries);
|
|
orphanEntries = NULL;
|
|
}
|
|
|
|
orphanEntriesCnt = 0;
|
|
}
|
|
|
|
static void freeTitleInfo()
|
|
{
|
|
u32 i;
|
|
|
|
if (baseAppEntries && titleAppCount)
|
|
{
|
|
for(i = 0; i < titleAppCount; i++)
|
|
{
|
|
if (baseAppEntries[i].icon) free(baseAppEntries[i].icon);
|
|
}
|
|
}
|
|
|
|
if (baseAppEntries)
|
|
{
|
|
free(baseAppEntries);
|
|
baseAppEntries = NULL;
|
|
}
|
|
|
|
if (patchEntries)
|
|
{
|
|
free(patchEntries);
|
|
patchEntries = NULL;
|
|
}
|
|
|
|
if (addOnEntries)
|
|
{
|
|
free(addOnEntries);
|
|
addOnEntries = NULL;
|
|
}
|
|
|
|
titleAppCount = 0;
|
|
titlePatchCount = 0;
|
|
titleAddOnCount = 0;
|
|
|
|
sdCardTitleAppCount = 0;
|
|
sdCardTitlePatchCount = 0;
|
|
sdCardTitleAddOnCount = 0;
|
|
|
|
emmcTitleAppCount = 0;
|
|
emmcTitlePatchCount = 0;
|
|
emmcTitleAddOnCount = 0;
|
|
|
|
gameCardSdCardEmmcPatchCount = 0;
|
|
gameCardSdCardEmmcAddOnCount = 0;
|
|
|
|
freeOrphanPatchOrAddOnList();
|
|
}
|
|
|
|
void freeRomFsBrowserEntries()
|
|
{
|
|
if (romFsBrowserEntries != NULL)
|
|
{
|
|
free(romFsBrowserEntries);
|
|
romFsBrowserEntries = NULL;
|
|
}
|
|
}
|
|
|
|
void freeHfs0ExeFsEntriesSizes()
|
|
{
|
|
if (hfs0ExeFsEntriesSizes)
|
|
{
|
|
free(hfs0ExeFsEntriesSizes);
|
|
hfs0ExeFsEntriesSizes = NULL;
|
|
}
|
|
}
|
|
|
|
static void freeGlobalData()
|
|
{
|
|
freeGameCardInfo();
|
|
|
|
freeTitleInfo();
|
|
|
|
freeExeFsContext();
|
|
|
|
freeRomFsContext();
|
|
|
|
freeBktrContext();
|
|
|
|
freeRomFsBrowserEntries();
|
|
|
|
freeHfs0ExeFsEntriesSizes();
|
|
|
|
freeFilenameBuffer();
|
|
}
|
|
|
|
void scanPads(void)
|
|
{
|
|
padUpdate(&g_padState);
|
|
}
|
|
|
|
u64 getButtonsDown(void)
|
|
{
|
|
return padGetButtonsDown(&g_padState);
|
|
}
|
|
|
|
u64 getButtonsHeld(void)
|
|
{
|
|
return padGetButtons(&g_padState);
|
|
}
|
|
|
|
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");
|
|
consoleUpdate(NULL);
|
|
|
|
/* Don't consider stick movement as button inputs. */
|
|
u64 flag = ~(HidNpadButton_StickLLeft | HidNpadButton_StickLRight | HidNpadButton_StickLUp | HidNpadButton_StickLDown | HidNpadButton_StickRLeft | HidNpadButton_StickRRight | \
|
|
HidNpadButton_StickRUp | HidNpadButton_StickRDown);
|
|
|
|
while(appletMainLoop())
|
|
{
|
|
scanPads();
|
|
if (getButtonsDown() && flag) break;
|
|
}
|
|
|
|
consoleExit(NULL);
|
|
}
|
|
|
|
static bool initServices()
|
|
{
|
|
Result result;
|
|
|
|
/* Initialize ncm service */
|
|
result = ncmInitialize();
|
|
if (R_FAILED(result))
|
|
{
|
|
consoleErrorScreen("%s: failed to initialize ncm service! (0x%08X)", __func__, result);
|
|
return false;
|
|
}
|
|
|
|
initNcm = true;
|
|
|
|
/* Initialize ns service */
|
|
result = nsInitialize();
|
|
if (R_FAILED(result))
|
|
{
|
|
consoleErrorScreen("%s: failed to initialize ns service! (0x%08X)", __func__, result);
|
|
return false;
|
|
}
|
|
|
|
initNs = true;
|
|
|
|
/* Initialize csrng service */
|
|
result = csrngInitialize();
|
|
if (R_FAILED(result))
|
|
{
|
|
consoleErrorScreen("%s: failed to initialize csrng service! (0x%08X)", __func__, result);
|
|
return false;
|
|
}
|
|
|
|
initCsrng = true;
|
|
|
|
/* Initialize spl service */
|
|
result = splInitialize();
|
|
if (R_FAILED(result))
|
|
{
|
|
consoleErrorScreen("%s: failed to initialize spl service! (0x%08X)", __func__, result);
|
|
return false;
|
|
}
|
|
|
|
initSpl = true;
|
|
|
|
/* Initialize pm:dmnt service */
|
|
result = pmdmntInitialize();
|
|
if (R_FAILED(result))
|
|
{
|
|
consoleErrorScreen("%s: failed to initialize pm:dmnt service! (0x%08X)", __func__, result);
|
|
return false;
|
|
}
|
|
|
|
initPmdmnt = true;
|
|
|
|
/* Initialize pl service */
|
|
result = plInitialize(PlServiceType_User);
|
|
if (R_FAILED(result))
|
|
{
|
|
consoleErrorScreen("%s: failed to initialize pl service! (0x%08X)", __func__, result);
|
|
return false;
|
|
}
|
|
|
|
initPl = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void deinitServices()
|
|
{
|
|
/* Denitialize pl service */
|
|
if (initPl) plExit();
|
|
|
|
/* Denitialize pm:dmnt service */
|
|
if (initPmdmnt) pmdmntExit();
|
|
|
|
/* Denitialize spl service */
|
|
if (initSpl) splExit();
|
|
|
|
/* Denitialize csrng service */
|
|
if (initCsrng) csrngExit();
|
|
|
|
/* Denitialize ns service */
|
|
if (initNs) nsExit();
|
|
|
|
/* Denitialize ncm service */
|
|
if (initNcm) ncmExit();
|
|
}
|
|
|
|
bool initApplicationResources(int argc, char **argv)
|
|
{
|
|
Result result = 0;
|
|
bool success = false;
|
|
|
|
/* Configure input. */
|
|
/* Up to 8 different, full controller inputs. */
|
|
/* Individual Joy-Cons not supported. */
|
|
padConfigureInput(8, HidNpadStyleSet_NpadFullCtrl);
|
|
padInitializeWithMask(&g_padState, 0x1000000FFUL);
|
|
|
|
/* 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_FastLoad);
|
|
|
|
/* 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_Normal);
|
|
|
|
/* Enable screen dimming and auto sleep */
|
|
appletSetMediaPlaybackState(false);
|
|
|
|
/* Deinitialize UI */
|
|
uiDeinit();
|
|
|
|
/* Deinitialize services */
|
|
deinitServices();
|
|
}
|
|
|
|
bool appletModeCheck()
|
|
{
|
|
return (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication);
|
|
}
|
|
|
|
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 changeHomeButtonBlockStatus(bool block)
|
|
{
|
|
// Only change HOME button blocking status if we're running as a regular application or a system application, and if it's current blocking status is different than the requested one
|
|
if (appletModeCheck() || block == homeBtnBlocked) return;
|
|
|
|
if (block)
|
|
{
|
|
appletBeginBlockingHomeButtonShortAndLongPressed(0);
|
|
} else {
|
|
appletEndBlockingHomeButtonShortAndLongPressed();
|
|
}
|
|
|
|
homeBtnBlocked = block;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
static void convertTitleVersionToDotNotation(u32 titleVersion, u8 *outMajor, u8 *outMinor, u8 *outMicro, u16 *outBugfix)
|
|
{
|
|
if (!outMajor || !outMinor || !outMicro || !outBugfix) return;
|
|
|
|
*outMajor = (u8)((titleVersion >> 26) & 0x3F);
|
|
*outMinor = (u8)((titleVersion >> 20) & 0x3F);
|
|
*outMicro = (u8)((titleVersion >> 16) & 0xF);
|
|
*outBugfix = (u16)titleVersion;
|
|
}
|
|
|
|
static void generateVersionDottedStr(u32 titleVersion, char *outBuf, size_t outBufSize)
|
|
{
|
|
if (!outBuf || !outBufSize) return;
|
|
|
|
u8 major = 0, minor = 0, micro = 0;
|
|
u16 bugfix = 0;
|
|
|
|
convertTitleVersionToDotNotation(titleVersion, &major, &minor, µ, &bugfix);
|
|
snprintf(outBuf, outBufSize, "%u (%u.%u.%u.%u)", titleVersion, major, minor, micro, bugfix);
|
|
}
|
|
|
|
static bool listTitlesByType(NcmContentMetaDatabase *ncmDb, NcmContentMetaType metaType)
|
|
{
|
|
if (!ncmDb || (metaType != NcmContentMetaType_Application && metaType != NcmContentMetaType_Patch && metaType != NcmContentMetaType_AddOnContent))
|
|
{
|
|
uiStatusMsg("%s: invalid parameters to list titles by type from storage!", __func__);
|
|
return false;
|
|
}
|
|
|
|
bool success = false, proceed = true, memError = false;
|
|
|
|
Result result;
|
|
|
|
NcmApplicationContentMetaKey *titleList = NULL, *titleListTmp = NULL;
|
|
size_t titleListSize = sizeof(NcmApplicationContentMetaKey);
|
|
|
|
u32 i, written = 0, total = 0;
|
|
|
|
base_app_ctx_t *tmpAppEntries = NULL;
|
|
patch_addon_ctx_t *tmpPatchAddOnEntries = NULL;
|
|
|
|
titleList = calloc(1, titleListSize);
|
|
if (!titleList)
|
|
{
|
|
uiStatusMsg("%s: unable to allocate memory for the ApplicationContentMetaKey struct! (meta type: 0x%02X).", __func__, (u8)metaType);
|
|
goto out;
|
|
}
|
|
|
|
result = ncmContentMetaDatabaseListApplication(ncmDb, (s32*)&total, (s32*)&written, titleList, 1, metaType);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiStatusMsg("%s: ncmContentMetaDatabaseListApplication failed! (0x%08X) (meta type: 0x%02X).", __func__, result, (u8)metaType);
|
|
goto out;
|
|
}
|
|
|
|
if (!written || !total)
|
|
{
|
|
// There are no titles that match the provided filter in the opened storage device
|
|
success = true;
|
|
goto out;
|
|
}
|
|
|
|
if (total > written)
|
|
{
|
|
titleListSize *= total;
|
|
titleListTmp = realloc(titleList, titleListSize);
|
|
if (titleListTmp)
|
|
{
|
|
titleList = titleListTmp;
|
|
memset(titleList, 0, titleListSize);
|
|
|
|
result = ncmContentMetaDatabaseListApplication(ncmDb, (s32*)&total, (s32*)&written, titleList, (s32)total, metaType);
|
|
if (R_SUCCEEDED(result))
|
|
{
|
|
if (written != total)
|
|
{
|
|
uiStatusMsg("%s: title count mismatch in ncmContentMetaDatabaseListApplication! (%u != %u) (meta type: 0x%02X).", __func__, written, total, (u8)metaType);
|
|
proceed = false;
|
|
}
|
|
} else {
|
|
uiStatusMsg("%s: ncmContentMetaDatabaseListApplication failed! (0x%08X) (meta type: 0x%02X).", __func__, result, (u8)metaType);
|
|
proceed = false;
|
|
}
|
|
} else {
|
|
uiStatusMsg("%s: error reallocating output buffer for ncmContentMetaDatabaseListApplication! (%u %s) (meta type: 0x%02X).", __func__, total, (total == 1 ? "entry" : "entries"), (u8)metaType);
|
|
proceed = false;
|
|
}
|
|
}
|
|
|
|
if (!proceed) goto out;
|
|
|
|
if (metaType == NcmContentMetaType_Application)
|
|
{
|
|
// If ptr == NULL, realloc will essentially act as a malloc
|
|
tmpAppEntries = realloc(baseAppEntries, (titleAppCount + total) * sizeof(base_app_ctx_t));
|
|
if (tmpAppEntries)
|
|
{
|
|
baseAppEntries = tmpAppEntries;
|
|
tmpAppEntries = NULL;
|
|
|
|
memset(baseAppEntries + titleAppCount, 0, total * sizeof(base_app_ctx_t));
|
|
|
|
for(i = 0; i < total; i++)
|
|
{
|
|
baseAppEntries[titleAppCount + i].titleId = titleList[i].key.id;
|
|
baseAppEntries[titleAppCount + i].version = titleList[i].key.version;
|
|
baseAppEntries[titleAppCount + i].ncmIndex = i;
|
|
generateVersionDottedStr(titleList[i].key.version, baseAppEntries[titleAppCount + i].versionStr, VERSION_STR_LEN);
|
|
}
|
|
|
|
titleAppCount += total;
|
|
|
|
success = true;
|
|
} else {
|
|
memError = true;
|
|
}
|
|
} else
|
|
if (metaType == NcmContentMetaType_Patch)
|
|
{
|
|
// If ptr == NULL, realloc will essentially act as a malloc
|
|
tmpPatchAddOnEntries = realloc(patchEntries, (titlePatchCount + total) * sizeof(patch_addon_ctx_t));
|
|
if (tmpPatchAddOnEntries)
|
|
{
|
|
patchEntries = tmpPatchAddOnEntries;
|
|
tmpPatchAddOnEntries = NULL;
|
|
|
|
memset(patchEntries + titlePatchCount, 0, total * sizeof(patch_addon_ctx_t));
|
|
|
|
for(i = 0; i < total; i++)
|
|
{
|
|
patchEntries[titlePatchCount + i].titleId = titleList[i].key.id;
|
|
patchEntries[titlePatchCount + i].version = titleList[i].key.version;
|
|
patchEntries[titlePatchCount + i].ncmIndex = i;
|
|
generateVersionDottedStr(titleList[i].key.version, patchEntries[titlePatchCount + i].versionStr, VERSION_STR_LEN);
|
|
}
|
|
|
|
titlePatchCount += total;
|
|
|
|
success = true;
|
|
} else {
|
|
memError = true;
|
|
}
|
|
} else
|
|
if (metaType == NcmContentMetaType_AddOnContent)
|
|
{
|
|
// If ptr == NULL, realloc will essentially act as a malloc
|
|
tmpPatchAddOnEntries = realloc(addOnEntries, (titleAddOnCount + total) * sizeof(patch_addon_ctx_t));
|
|
if (tmpPatchAddOnEntries)
|
|
{
|
|
addOnEntries = tmpPatchAddOnEntries;
|
|
tmpPatchAddOnEntries = NULL;
|
|
|
|
memset(addOnEntries + titleAddOnCount, 0, total * sizeof(patch_addon_ctx_t));
|
|
|
|
for(i = 0; i < total; i++)
|
|
{
|
|
addOnEntries[titleAddOnCount + i].titleId = titleList[i].key.id;
|
|
addOnEntries[titleAddOnCount + i].version = titleList[i].key.version;
|
|
addOnEntries[titleAddOnCount + i].ncmIndex = i;
|
|
generateVersionDottedStr(titleList[i].key.version, addOnEntries[titleAddOnCount + i].versionStr, VERSION_STR_LEN);
|
|
}
|
|
|
|
titleAddOnCount += total;
|
|
|
|
success = true;
|
|
} else {
|
|
memError = true;
|
|
}
|
|
}
|
|
|
|
out:
|
|
if (memError) uiStatusMsg("%s: failed to reallocate entry buffer! (meta type: 0x%02X).", __func__, (u8)metaType);
|
|
|
|
if (titleList) free(titleList);
|
|
|
|
return success;
|
|
}
|
|
|
|
static bool getTitleIDAndVersionList(NcmStorageId storageId, bool loadBaseApps, bool loadPatches, bool loadAddOns)
|
|
{
|
|
if ((storageId != NcmStorageId_GameCard && storageId != NcmStorageId_SdCard && storageId != NcmStorageId_BuiltInUser) || (!loadBaseApps && !loadPatches && !loadAddOns))
|
|
{
|
|
uiStatusMsg("%s: invalid parameters to retrieve Title ID + version list!", __func__);
|
|
return false;
|
|
}
|
|
|
|
/* Check if the SD card is really mounted */
|
|
if (storageId == NcmStorageId_SdCard && fsdevGetDeviceFileSystem("sdmc:") == NULL) return true;
|
|
|
|
bool listApp = false, listPatch = false, listAddOn = false, success = false;
|
|
|
|
Result result;
|
|
NcmContentMetaDatabase ncmDb;
|
|
|
|
u32 i;
|
|
u32 curAppCount = titleAppCount, curPatchCount = titlePatchCount, curAddOnCount = titleAddOnCount;
|
|
|
|
result = ncmOpenContentMetaDatabase(&ncmDb, storageId);
|
|
if (R_FAILED(result))
|
|
{
|
|
if (storageId == NcmStorageId_SdCard && result == 0x21005)
|
|
{
|
|
// If the SD card is mounted, but is isn't currently used by HOS because of some weird reason, just filter this particular error and continue
|
|
// This can occur when using the "Nintendo" directory from a different console, or when the "sdmc:/Nintendo/Contents/private" file is corrupted
|
|
return true;
|
|
} else {
|
|
uiStatusMsg("%s: ncmOpenContentMetaDatabase failed for storage ID %u! (0x%08X)", __func__, result, storageId);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (loadBaseApps)
|
|
{
|
|
listApp = listTitlesByType(&ncmDb, NcmContentMetaType_Application);
|
|
if (listApp && titleAppCount > curAppCount)
|
|
{
|
|
for(i = curAppCount; i < titleAppCount; i++) baseAppEntries[i].storageId = storageId;
|
|
}
|
|
}
|
|
|
|
if (loadPatches)
|
|
{
|
|
listPatch = listTitlesByType(&ncmDb, NcmContentMetaType_Patch);
|
|
if (listPatch && titlePatchCount > curPatchCount)
|
|
{
|
|
for(i = curPatchCount; i < titlePatchCount; i++) patchEntries[i].storageId = storageId;
|
|
}
|
|
}
|
|
|
|
if (loadAddOns)
|
|
{
|
|
listAddOn = listTitlesByType(&ncmDb, NcmContentMetaType_AddOnContent);
|
|
if (listAddOn && titleAddOnCount > curAddOnCount)
|
|
{
|
|
for(i = curAddOnCount; i < titleAddOnCount; i++) addOnEntries[i].storageId = storageId;
|
|
}
|
|
}
|
|
|
|
success = (listApp || listPatch || listAddOn);
|
|
|
|
ncmContentMetaDatabaseClose(&ncmDb);
|
|
|
|
return success;
|
|
}
|
|
|
|
bool loadTitlesFromSdCardAndEmmc(NcmContentMetaType metaType)
|
|
{
|
|
if (menuType != MENUTYPE_GAMECARD || (metaType != NcmContentMetaType_Patch && metaType != NcmContentMetaType_AddOnContent)) return false;
|
|
|
|
if ((metaType == NcmContentMetaType_Patch && gameCardSdCardEmmcPatchCount) || (metaType == NcmContentMetaType_AddOnContent && gameCardSdCardEmmcAddOnCount)) return true;
|
|
|
|
u8 i;
|
|
u32 curPatchCount = titlePatchCount, curAddOnCount = titleAddOnCount;
|
|
|
|
for(i = 0; i < 2; i++)
|
|
{
|
|
NcmStorageId curStorageId = (i == 0 ? NcmStorageId_SdCard : NcmStorageId_BuiltInUser);
|
|
|
|
if (!getTitleIDAndVersionList(curStorageId, false, (metaType == NcmContentMetaType_Patch), (metaType == NcmContentMetaType_AddOnContent))) continue;
|
|
|
|
if (metaType == NcmContentMetaType_Patch)
|
|
{
|
|
if (titlePatchCount > curPatchCount)
|
|
{
|
|
u32 newPatchCount = (titlePatchCount - curPatchCount);
|
|
|
|
gameCardSdCardEmmcPatchCount = newPatchCount;
|
|
|
|
if (curStorageId == NcmStorageId_SdCard)
|
|
{
|
|
sdCardTitlePatchCount = newPatchCount;
|
|
} else {
|
|
emmcTitlePatchCount = newPatchCount;
|
|
}
|
|
}
|
|
} else
|
|
if (metaType == NcmContentMetaType_AddOnContent)
|
|
{
|
|
if (titleAddOnCount > curAddOnCount)
|
|
{
|
|
u32 newAddOnCount = (titleAddOnCount - curAddOnCount);
|
|
|
|
gameCardSdCardEmmcAddOnCount = newAddOnCount;
|
|
|
|
if (curStorageId == NcmStorageId_SdCard)
|
|
{
|
|
sdCardTitleAddOnCount = newAddOnCount;
|
|
} else {
|
|
emmcTitleAddOnCount = newAddOnCount;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((metaType == NcmContentMetaType_Patch && gameCardSdCardEmmcPatchCount) || (metaType == NcmContentMetaType_AddOnContent && gameCardSdCardEmmcAddOnCount)) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void freeTitlesFromSdCardAndEmmc(NcmContentMetaType metaType)
|
|
{
|
|
if (menuType != MENUTYPE_GAMECARD || (metaType != NcmContentMetaType_Patch && metaType != NcmContentMetaType_AddOnContent) || (metaType == NcmContentMetaType_Patch && (!titlePatchCount || !gameCardSdCardEmmcPatchCount)) || (metaType == NcmContentMetaType_AddOnContent && (!titleAddOnCount || !gameCardSdCardEmmcAddOnCount))) return;
|
|
|
|
patch_addon_ctx_t *tmpPatchAddOnEntries = NULL;
|
|
|
|
if (metaType == NcmContentMetaType_Patch)
|
|
{
|
|
if ((titlePatchCount - gameCardSdCardEmmcPatchCount) > 0)
|
|
{
|
|
tmpPatchAddOnEntries = realloc(patchEntries, (titlePatchCount - gameCardSdCardEmmcPatchCount) * sizeof(patch_addon_ctx_t));
|
|
if (tmpPatchAddOnEntries != NULL)
|
|
{
|
|
patchEntries = tmpPatchAddOnEntries;
|
|
tmpPatchAddOnEntries = NULL;
|
|
}
|
|
} else {
|
|
free(patchEntries);
|
|
patchEntries = NULL;
|
|
}
|
|
|
|
titlePatchCount -= gameCardSdCardEmmcPatchCount;
|
|
|
|
gameCardSdCardEmmcPatchCount = 0;
|
|
sdCardTitlePatchCount = 0;
|
|
emmcTitlePatchCount = 0;
|
|
} else {
|
|
if ((titleAddOnCount - gameCardSdCardEmmcAddOnCount) > 0)
|
|
{
|
|
tmpPatchAddOnEntries = realloc(addOnEntries, (titleAddOnCount - gameCardSdCardEmmcAddOnCount) * sizeof(patch_addon_ctx_t));
|
|
if (tmpPatchAddOnEntries != NULL)
|
|
{
|
|
addOnEntries = tmpPatchAddOnEntries;
|
|
tmpPatchAddOnEntries = NULL;
|
|
}
|
|
} else {
|
|
free(addOnEntries);
|
|
addOnEntries = NULL;
|
|
}
|
|
|
|
titleAddOnCount -= gameCardSdCardEmmcAddOnCount;
|
|
|
|
gameCardSdCardEmmcAddOnCount = 0;
|
|
sdCardTitleAddOnCount = 0;
|
|
emmcTitleAddOnCount = 0;
|
|
}
|
|
}
|
|
|
|
static bool getCachedBaseApplicationNacpMetadata(u64 titleID, char *nameBuf, size_t nameBufSize, char *authorBuf, size_t authorBufSize, u8 **iconBuf)
|
|
{
|
|
// At least the name must be retrieved
|
|
if (!nameBuf || !nameBufSize || (authorBuf && !authorBufSize))
|
|
{
|
|
uiStatusMsg("%s: invalid parameters to retrieve Control.nacp!", __func__);
|
|
return false;
|
|
}
|
|
|
|
bool getNameAndAuthor = false, getIcon = false, success = false;
|
|
Result result;
|
|
size_t outsize = 0;
|
|
NsApplicationControlData *buf = NULL;
|
|
NacpLanguageEntry *langentry = NULL;
|
|
|
|
buf = calloc(1, sizeof(NsApplicationControlData));
|
|
if (buf)
|
|
{
|
|
result = nsGetApplicationControlData(NsApplicationControlSource_Storage, titleID, buf, sizeof(NsApplicationControlData), &outsize);
|
|
if (R_SUCCEEDED(result))
|
|
{
|
|
if (outsize >= sizeof(buf->nacp))
|
|
{
|
|
result = nacpGetLanguageEntry(&buf->nacp, &langentry);
|
|
if (R_SUCCEEDED(result))
|
|
{
|
|
snprintf(nameBuf, nameBufSize, langentry->name);
|
|
if (authorBuf && authorBufSize) snprintf(authorBuf, authorBufSize, langentry->author);
|
|
getNameAndAuthor = true;
|
|
} else {
|
|
uiStatusMsg("%s: GetLanguageEntry failed! (0x%08X)", __func__, result);
|
|
}
|
|
|
|
if (iconBuf != NULL)
|
|
{
|
|
getIcon = uiLoadJpgFromMem(buf->icon, sizeof(buf->icon), NACP_ICON_SQUARE_DIMENSION, NACP_ICON_SQUARE_DIMENSION, NACP_ICON_DOWNSCALED, NACP_ICON_DOWNSCALED, iconBuf);
|
|
if (!getIcon) uiStatusMsg(strbuf);
|
|
}
|
|
|
|
success = (iconBuf != NULL ? (getNameAndAuthor && getIcon) : getNameAndAuthor);
|
|
} else {
|
|
uiStatusMsg("%s: Control.nacp buffer size (%u bytes) is too small! Expected: %u bytes", __func__, outsize, sizeof(buf->nacp));
|
|
}
|
|
} else {
|
|
uiStatusMsg("%s: GetApplicationControlData failed! (0x%08X)", __func__, result);
|
|
}
|
|
|
|
free(buf);
|
|
} else {
|
|
uiStatusMsg("%s: unable to allocate memory for the ns service operations!", __func__);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
void removeIllegalCharacters(char *name)
|
|
{
|
|
if (!name || !strlen(name)) return;
|
|
|
|
u32 i, len = strlen(name);
|
|
for (i = 0; i < len; i++)
|
|
{
|
|
if (memchr("?[]/\\=+<>:;\",*|^", name[i], sizeof("?[]/\\=+<>:;\",*|^") - 1) || name[i] < 0x20 || name[i] > 0x7E) name[i] = '_';
|
|
}
|
|
}
|
|
|
|
void strtrim(char *str)
|
|
{
|
|
if (!str || !strlen(str)) return;
|
|
|
|
char *start = str;
|
|
char *end = (start + strlen(str));
|
|
|
|
while(--end >= start)
|
|
{
|
|
if (!isspace((unsigned char)*end)) break;
|
|
}
|
|
|
|
*(++end) = '\0';
|
|
|
|
while(isspace((unsigned char)*start)) start++;
|
|
|
|
if (start != str) memmove(str, start, end - start + 1);
|
|
}
|
|
|
|
bool retrieveGameCardInfo()
|
|
{
|
|
Result result;
|
|
bool success = false;
|
|
|
|
u32 i;
|
|
hfs0_header header;
|
|
hfs0_file_entry entry;
|
|
|
|
u8 major = 0, minor = 0, micro = 0;
|
|
u16 bugfix = 0;
|
|
|
|
// Open normal IStorage partition
|
|
result = openGameCardStoragePartition(ISTORAGE_PARTITION_NORMAL);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiStatusMsg("%s: failed to open normal IStorage partition! (0x%08X)", __func__, result);
|
|
return false;
|
|
}
|
|
|
|
// Retrieve normal IStorage partition size
|
|
result = getGameCardStoragePartitionSize(&(gameCardInfo.IStoragePartitionSizes[0]));
|
|
if (R_FAILED(result))
|
|
{
|
|
uiStatusMsg("%s: failed to retrieve size for normal IStorage partition! (0x%08X)", __func__, result);
|
|
return false;
|
|
}
|
|
|
|
// Read gamecard header
|
|
result = readGameCardStoragePartition(0, &(gameCardInfo.header), sizeof(gamecard_header_t));
|
|
if (R_FAILED(result))
|
|
{
|
|
uiStatusMsg("%s: failed to read %lu bytes long gamecard header! (0x%08X)", __func__, sizeof(gamecard_header_t), result);
|
|
goto out;
|
|
}
|
|
|
|
if (__builtin_bswap32(gameCardInfo.header.magic) != GAMECARD_HEADER_MAGIC)
|
|
{
|
|
uiStatusMsg("%s: invalid gamecard header magic word! (0x%08X)", __func__, __builtin_bswap32(gameCardInfo.header.magic));
|
|
goto out;
|
|
}
|
|
|
|
switch(gameCardInfo.header.size)
|
|
{
|
|
case 0xFA: // 1 GiB
|
|
gameCardInfo.size = GAMECARD_SIZE_1GiB;
|
|
break;
|
|
case 0xF8: // 2 GiB
|
|
gameCardInfo.size = GAMECARD_SIZE_2GiB;
|
|
break;
|
|
case 0xF0: // 4 GiB
|
|
gameCardInfo.size = GAMECARD_SIZE_4GiB;
|
|
break;
|
|
case 0xE0: // 8 GiB
|
|
gameCardInfo.size = GAMECARD_SIZE_8GiB;
|
|
break;
|
|
case 0xE1: // 16 GiB
|
|
gameCardInfo.size = GAMECARD_SIZE_16GiB;
|
|
break;
|
|
case 0xE2: // 32 GiB
|
|
gameCardInfo.size = GAMECARD_SIZE_32GiB;
|
|
break;
|
|
default:
|
|
uiStatusMsg("%s: invalid gamecard size value! (0x%02X)", __func__, gameCardInfo.header.size);
|
|
goto out;
|
|
}
|
|
|
|
convertSize(gameCardInfo.size, gameCardInfo.sizeStr, MAX_CHARACTERS(gameCardInfo.sizeStr));
|
|
|
|
gameCardInfo.trimmedSize = (sizeof(gamecard_header_t) + (gameCardInfo.header.validDataEndAddr * MEDIA_UNIT_SIZE));
|
|
convertSize(gameCardInfo.trimmedSize, gameCardInfo.trimmedSizeStr, MAX_CHARACTERS(gameCardInfo.trimmedSizeStr));
|
|
|
|
gameCardInfo.rootHfs0Header = calloc(1, gameCardInfo.header.rootHfs0HeaderSize);
|
|
if (!gameCardInfo.rootHfs0Header)
|
|
{
|
|
uiStatusMsg("%s: unable to allocate memory for the root HFS0 header!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
result = readGameCardStoragePartition(gameCardInfo.header.rootHfs0HeaderOffset, gameCardInfo.rootHfs0Header, gameCardInfo.header.rootHfs0HeaderSize);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiStatusMsg("%s: failed to read %lu bytes long root HFS0 header! (0x%08X)", __func__, gameCardInfo.header.rootHfs0HeaderSize, result);
|
|
goto out;
|
|
}
|
|
|
|
memcpy(&header, gameCardInfo.rootHfs0Header, sizeof(hfs0_header));
|
|
|
|
if (__builtin_bswap32(header.magic) != HFS0_MAGIC)
|
|
{
|
|
uiStatusMsg("%s: invalid magic word in root HFS0 header! (0x%08X)", __func__, __builtin_bswap32(header.magic));
|
|
goto out;
|
|
}
|
|
|
|
if (!header.file_cnt)
|
|
{
|
|
uiStatusMsg("%s: invalid file count in root HFS0 header!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if (!header.str_table_size)
|
|
{
|
|
uiStatusMsg("%s: invalid string table size in root HFS0 header!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
gameCardInfo.hfs0PartitionCnt = header.file_cnt;
|
|
|
|
// Retrieve partition data
|
|
gameCardInfo.hfs0Partitions = calloc(gameCardInfo.hfs0PartitionCnt, sizeof(hfs0_partition_info));
|
|
if (!gameCardInfo.hfs0Partitions)
|
|
{
|
|
uiStatusMsg("%s: unable to allocate memory for HFS0 partition headers!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
for(i = 0; i < gameCardInfo.hfs0PartitionCnt; i++)
|
|
{
|
|
memcpy(&entry, gameCardInfo.rootHfs0Header + sizeof(hfs0_header) + (i * sizeof(hfs0_file_entry)), sizeof(hfs0_file_entry));
|
|
|
|
if (!entry.file_size)
|
|
{
|
|
uiStatusMsg("%s: invalid size for %s HFS0 partition!", __func__, GAMECARD_PARTITION_NAME(gameCardInfo.hfs0PartitionCnt, i));
|
|
goto out;
|
|
}
|
|
|
|
gameCardInfo.hfs0Partitions[i].size = entry.file_size;
|
|
|
|
// Check if we're dealing with the secure HFS0 partition
|
|
if (i == (gameCardInfo.hfs0PartitionCnt - 1))
|
|
{
|
|
// The partition offset must be zero, because the secure HFS0 partition is stored at the start of the secure IStorage partition
|
|
gameCardInfo.hfs0Partitions[i].offset = 0;
|
|
|
|
// Open secure IStorage partition
|
|
result = openGameCardStoragePartition(ISTORAGE_PARTITION_SECURE);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiStatusMsg("%s: failed to open secure IStorage partition! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
|
|
if (strncmp(cfwDirStr, CFW_PATH_SXOS, strlen(CFW_PATH_SXOS)) != 0)
|
|
{
|
|
// Retrieve secure IStorage partition size
|
|
result = getGameCardStoragePartitionSize(&(gameCardInfo.IStoragePartitionSizes[1]));
|
|
if (R_FAILED(result))
|
|
{
|
|
uiStatusMsg("%s: failed to retrieve size for secure IStorage partition! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
} else {
|
|
// Total size for the secure IStorage partition is maxed out under SX OS, so let's try to calculate it manually
|
|
gameCardInfo.IStoragePartitionSizes[1] = ((gameCardInfo.size - ((gameCardInfo.size / GAMECARD_ECC_BLOCK_SIZE) * GAMECARD_ECC_DATA_SIZE)) - gameCardInfo.IStoragePartitionSizes[0]);
|
|
}
|
|
} else {
|
|
// The partition offset is relative to the start of the normal IStorage partition (true gamecard image start)
|
|
gameCardInfo.hfs0Partitions[i].offset = (gameCardInfo.header.rootHfs0HeaderOffset + gameCardInfo.header.rootHfs0HeaderSize + entry.file_offset);
|
|
}
|
|
|
|
// Partially read the current HFS0 partition header
|
|
result = readGameCardStoragePartition(gameCardInfo.hfs0Partitions[i].offset, &header, sizeof(hfs0_header));
|
|
if (R_FAILED(result))
|
|
{
|
|
uiStatusMsg("%s: failed to read %lu bytes long chunk from %s HFS0 partition! (0x%08X)", __func__, sizeof(hfs0_header), GAMECARD_PARTITION_NAME(gameCardInfo.hfs0PartitionCnt, i), result);
|
|
goto out;
|
|
}
|
|
|
|
// Check the HFS0 magic word
|
|
if (__builtin_bswap32(header.magic) != HFS0_MAGIC)
|
|
{
|
|
uiStatusMsg("%s: invalid magic word in %s HFS0 partition header! (0x%08X)", __func__, GAMECARD_PARTITION_NAME(gameCardInfo.hfs0PartitionCnt, i), __builtin_bswap32(header.magic));
|
|
goto out;
|
|
}
|
|
|
|
if (!header.str_table_size)
|
|
{
|
|
uiStatusMsg("%s: invalid string table size in %s HFS0 partition header!", __func__, GAMECARD_PARTITION_NAME(gameCardInfo.hfs0PartitionCnt, i));
|
|
goto out;
|
|
}
|
|
|
|
// Calculate the size for the HFS0 partition header and round it to a MEDIA_UNIT_SIZE bytes boundary
|
|
gameCardInfo.hfs0Partitions[i].header_size = (sizeof(hfs0_header) + (header.file_cnt * sizeof(hfs0_file_entry)) + header.str_table_size);
|
|
gameCardInfo.hfs0Partitions[i].header_size = round_up(gameCardInfo.hfs0Partitions[i].header_size, MEDIA_UNIT_SIZE);
|
|
|
|
gameCardInfo.hfs0Partitions[i].file_cnt = header.file_cnt;
|
|
gameCardInfo.hfs0Partitions[i].str_table_size = header.str_table_size;
|
|
|
|
gameCardInfo.hfs0Partitions[i].header = calloc(1, gameCardInfo.hfs0Partitions[i].header_size);
|
|
if (!gameCardInfo.hfs0Partitions[i].header)
|
|
{
|
|
uiStatusMsg("%s: unable to allocate memory for %s HFS0 partition header!", __func__, GAMECARD_PARTITION_NAME(gameCardInfo.hfs0PartitionCnt, i));
|
|
goto out;
|
|
}
|
|
|
|
// Finally, read the full HFS0 partition header
|
|
result = readGameCardStoragePartition(gameCardInfo.hfs0Partitions[i].offset, gameCardInfo.hfs0Partitions[i].header, gameCardInfo.hfs0Partitions[i].header_size);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiStatusMsg("%s: failed to read %lu bytes long %s HFS0 partition header! (0x%08X)", __func__, gameCardInfo.hfs0Partitions[i].header_size, GAMECARD_PARTITION_NAME(gameCardInfo.hfs0PartitionCnt, i), result);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
// Get bundled FW version update
|
|
result = fsDeviceOperatorUpdatePartitionInfo(&(gameCardInfo.fsOperatorInstance), &(gameCardInfo.fsGameCardHandle), &(gameCardInfo.updateVersion), &(gameCardInfo.updateTitleId));
|
|
if (R_SUCCEEDED(result))
|
|
{
|
|
if (gameCardInfo.updateTitleId == GAMECARD_UPDATE_TITLEID)
|
|
{
|
|
convertTitleVersionToDotNotation(gameCardInfo.updateVersion, &major, &minor, µ, &bugfix);
|
|
snprintf(gameCardInfo.updateVersionStr, MAX_CHARACTERS(gameCardInfo.updateVersionStr), "%u.%u.%u (v%u)", major, minor, micro, gameCardInfo.updateVersion);
|
|
} else {
|
|
uiStatusMsg("%s: update Title ID mismatch! (%016lX != %016lX)", __func__, gameCardInfo.updateTitleId, GAMECARD_UPDATE_TITLEID);
|
|
}
|
|
} else {
|
|
uiStatusMsg("%s: UpdatePartitionInfo failed! (0x%08X)", __func__, result);
|
|
}
|
|
|
|
success = true;
|
|
|
|
out:
|
|
if (success)
|
|
{
|
|
closeGameCardStoragePartition();
|
|
} else {
|
|
freeGameCardInfo();
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
u64 calculateSizeFromContentRecords(NcmStorageId curStorageId, NcmContentMetaType metaType, u32 ncmTitleCount, u32 ncmTitleIndex)
|
|
{
|
|
if ((curStorageId != NcmStorageId_GameCard && curStorageId != NcmStorageId_SdCard && curStorageId != NcmStorageId_BuiltInUser) || (metaType != NcmContentMetaType_Application && metaType != NcmContentMetaType_Patch && metaType != NcmContentMetaType_AddOnContent) || ncmTitleIndex >= ncmTitleCount) return 0;
|
|
|
|
NcmContentInfo *titleContentInfos = NULL;
|
|
u32 i, titleContentInfoCnt = 0;
|
|
u64 tmp = 0, outSize = 0;
|
|
|
|
if (!retrieveContentInfosFromTitle(curStorageId, metaType, ncmTitleCount, ncmTitleIndex, &titleContentInfos, &titleContentInfoCnt)) return 0;
|
|
|
|
for(i = 0; i < titleContentInfoCnt; i++)
|
|
{
|
|
if (titleContentInfos[i].content_type >= NcmContentType_DeltaFragment) continue;
|
|
|
|
convertNcaSizeToU64(titleContentInfos[i].size, &tmp);
|
|
outSize += tmp;
|
|
}
|
|
|
|
free(titleContentInfos);
|
|
|
|
return outSize;
|
|
}
|
|
|
|
int baseAppCmp(const void *a, const void *b)
|
|
{
|
|
base_app_ctx_t *baseApp1 = (base_app_ctx_t*)a;
|
|
base_app_ctx_t *baseApp2 = (base_app_ctx_t*)b;
|
|
|
|
return strcasecmp(baseApp1->name, baseApp2->name);
|
|
}
|
|
|
|
int orphanEntryCmp(const void *a, const void *b)
|
|
{
|
|
orphan_patch_addon_entry *orphanEntry1 = (orphan_patch_addon_entry*)a;
|
|
orphan_patch_addon_entry *orphanEntry2 = (orphan_patch_addon_entry*)b;
|
|
|
|
return strcasecmp(orphanEntry1->orphanListStr, orphanEntry2->orphanListStr);
|
|
}
|
|
|
|
void loadTitleInfo()
|
|
{
|
|
if (menuType == MENUTYPE_MAIN)
|
|
{
|
|
freeGlobalData();
|
|
changeAtomicBool(&gameCardInfoLoaded, false);
|
|
sdCardAndEmmcTitleInfoLoaded = false;
|
|
return;
|
|
}
|
|
|
|
bool proceed = false;
|
|
|
|
if (menuType == MENUTYPE_GAMECARD)
|
|
{
|
|
if (gameCardInfo.isInserted && gameCardInfoLoaded) return;
|
|
|
|
freeGlobalData();
|
|
|
|
if (!gameCardInfo.isInserted) return;
|
|
|
|
/* Don't access the gamecard immediately to avoid conflicts with the fsp-srv, ncm and ns services */
|
|
uiPleaseWait(GAMECARD_WAIT_TIME);
|
|
|
|
proceed = retrieveGameCardInfo();
|
|
changeAtomicBool(&gameCardInfoLoaded, true);
|
|
|
|
if (proceed) proceed = getTitleIDAndVersionList(NcmStorageId_GameCard, true, true, true);
|
|
} else
|
|
if (menuType == MENUTYPE_SDCARD_EMMC)
|
|
{
|
|
if (sdCardAndEmmcTitleInfoLoaded) return;
|
|
|
|
uiPleaseWait(0);
|
|
|
|
freeTitleInfo();
|
|
|
|
if (getTitleIDAndVersionList(NcmStorageId_SdCard, true, true, true))
|
|
{
|
|
sdCardTitleAppCount = titleAppCount;
|
|
sdCardTitlePatchCount = titlePatchCount;
|
|
sdCardTitleAddOnCount = titleAddOnCount;
|
|
|
|
if (getTitleIDAndVersionList(NcmStorageId_BuiltInUser, true, true, true))
|
|
{
|
|
emmcTitleAppCount = (titleAppCount - sdCardTitleAppCount);
|
|
emmcTitlePatchCount = (titlePatchCount - sdCardTitlePatchCount);
|
|
emmcTitleAddOnCount = (titleAddOnCount - sdCardTitleAddOnCount);
|
|
|
|
proceed = true;
|
|
}
|
|
}
|
|
|
|
sdCardAndEmmcTitleInfoLoaded = true;
|
|
}
|
|
|
|
if (proceed)
|
|
{
|
|
u32 i, ncmTitleCount;
|
|
|
|
for(i = 0; i < titleAppCount; i++)
|
|
{
|
|
// Retrieve base application name, author and icon
|
|
if (getCachedBaseApplicationNacpMetadata(baseAppEntries[i].titleId, baseAppEntries[i].name, MAX_CHARACTERS(baseAppEntries[i].name), baseAppEntries[i].author, MAX_CHARACTERS(baseAppEntries[i].author), &(baseAppEntries[i].icon)))
|
|
{
|
|
strtrim(baseAppEntries[i].name);
|
|
strtrim(baseAppEntries[i].author);
|
|
snprintf(baseAppEntries[i].fixedName, MAX_CHARACTERS(baseAppEntries[i].fixedName), baseAppEntries[i].name);
|
|
removeIllegalCharacters(baseAppEntries[i].fixedName);
|
|
}
|
|
|
|
// Retrieve base application content size
|
|
ncmTitleCount = (baseAppEntries[i].storageId == NcmStorageId_GameCard ? titleAppCount : (baseAppEntries[i].storageId == NcmStorageId_SdCard ? sdCardTitleAppCount : emmcTitleAppCount));
|
|
baseAppEntries[i].contentSize = calculateSizeFromContentRecords(baseAppEntries[i].storageId, NcmContentMetaType_Application, ncmTitleCount, baseAppEntries[i].ncmIndex);
|
|
convertSize(baseAppEntries[i].contentSize, baseAppEntries[i].contentSizeStr, MAX_CHARACTERS(baseAppEntries[i].contentSizeStr));
|
|
}
|
|
|
|
// Sort base applications by name
|
|
if (titleAppCount) qsort(baseAppEntries, titleAppCount, sizeof(base_app_ctx_t), baseAppCmp);
|
|
|
|
for(i = 0; i < titlePatchCount; i++)
|
|
{
|
|
// Retrieve patch content size
|
|
ncmTitleCount = (patchEntries[i].storageId == NcmStorageId_GameCard ? titlePatchCount : (patchEntries[i].storageId == NcmStorageId_SdCard ? sdCardTitlePatchCount : emmcTitlePatchCount));
|
|
patchEntries[i].contentSize = calculateSizeFromContentRecords(patchEntries[i].storageId, NcmContentMetaType_Patch, ncmTitleCount, patchEntries[i].ncmIndex);
|
|
convertSize(patchEntries[i].contentSize, patchEntries[i].contentSizeStr, MAX_CHARACTERS(patchEntries[i].contentSizeStr));
|
|
}
|
|
|
|
for(i = 0; i < titleAddOnCount; i++)
|
|
{
|
|
// Retrieve add-on content size
|
|
ncmTitleCount = (addOnEntries[i].storageId == NcmStorageId_GameCard ? titleAddOnCount : (addOnEntries[i].storageId == NcmStorageId_SdCard ? sdCardTitleAddOnCount : emmcTitleAddOnCount));
|
|
addOnEntries[i].contentSize = calculateSizeFromContentRecords(addOnEntries[i].storageId, NcmContentMetaType_AddOnContent, ncmTitleCount, addOnEntries[i].ncmIndex);
|
|
convertSize(addOnEntries[i].contentSize, addOnEntries[i].contentSizeStr, MAX_CHARACTERS(addOnEntries[i].contentSizeStr));
|
|
}
|
|
|
|
// Generate orphan content list
|
|
// If orphanEntries == NULL or if orphanEntriesCnt == 0, both variables will be regenerated
|
|
// Otherwise, this will only fill filenameBuffer
|
|
if (menuType == MENUTYPE_SDCARD_EMMC) generateOrphanPatchOrAddOnList();
|
|
}
|
|
|
|
uiPrintHeadline();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Used to retrieve data from files in the HFS0 Secure partition
|
|
// An IStorage instance must have been opened beforehand
|
|
bool readFileFromSecureHfs0PartitionByName(const char *filename, u64 offset, void *outBuf, size_t bufSize)
|
|
{
|
|
if (!gameCardInfo.hfs0PartitionCnt || !gameCardInfo.hfs0Partitions || !gameCardInfo.hfs0Partitions[gameCardInfo.hfs0PartitionCnt - 1].header || !gameCardInfo.hfs0Partitions[gameCardInfo.hfs0PartitionCnt - 1].header_size || !gameCardInfo.hfs0Partitions[gameCardInfo.hfs0PartitionCnt - 1].file_cnt || !gameCardInfo.hfs0Partitions[gameCardInfo.hfs0PartitionCnt - 1].str_table_size || !filename || !strlen(filename) || !outBuf || !bufSize)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to read file from Secure HFS0 partition!", __func__);
|
|
return false;
|
|
}
|
|
|
|
u32 i;
|
|
Result result;
|
|
hfs0_file_entry entry;
|
|
|
|
bool proceed = true, found = false;
|
|
|
|
u32 partition = (gameCardInfo.hfs0PartitionCnt - 1); // Select the Secure HFS0 partition
|
|
|
|
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);
|
|
|
|
if (strncasecmp(cur_filename, filename, strlen(filename)) != 0) continue;
|
|
|
|
found = true;
|
|
|
|
if (!entry.file_size || (offset + bufSize) > entry.file_size)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid file size for \"%s\"!", __func__, filename);
|
|
proceed = false;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (!proceed || !found)
|
|
{
|
|
if (proceed && !found) uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to find file \"%s\" in Secure HFS0 partition!", __func__, filename);
|
|
return false;
|
|
}
|
|
|
|
result = readGameCardStoragePartition(gameCardInfo.hfs0Partitions[partition].header_size + entry.file_offset + offset, outBuf, bufSize);
|
|
if (R_FAILED(result))
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to read file \"%s\"! (0x%08X)", __func__, filename, result);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool calculateExeFsExtractedDataSize(u64 *out)
|
|
{
|
|
if (!exeFsContext.exefs_header.file_cnt || !exeFsContext.exefs_entries || !out)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to calculate extracted data size for the ExeFS section!", __func__);
|
|
return false;
|
|
}
|
|
|
|
u32 i;
|
|
u64 totalSize = 0;
|
|
|
|
for(i = 0; i < exeFsContext.exefs_header.file_cnt; i++) totalSize += exeFsContext.exefs_entries[i].file_size;
|
|
|
|
*out = totalSize;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool calculateRomFsFullExtractedSize(bool usePatch, u64 *out)
|
|
{
|
|
if ((!usePatch && (!romFsContext.romfs_filetable_size || !romFsContext.romfs_file_entries)) || (usePatch && (!bktrContext.romfs_filetable_size || !bktrContext.romfs_file_entries)) || !out)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to calculate extracted data size for the RomFS section!", __func__);
|
|
return false;
|
|
}
|
|
|
|
u64 offset = 0;
|
|
u64 totalSize = 0;
|
|
|
|
u64 filetableSize = (!usePatch ? romFsContext.romfs_filetable_size : bktrContext.romfs_filetable_size);
|
|
romfs_file *fileEntries = (!usePatch ? romFsContext.romfs_file_entries : bktrContext.romfs_file_entries);
|
|
|
|
while(offset < filetableSize)
|
|
{
|
|
romfs_file *entry = (romfs_file*)((u8*)fileEntries + offset);
|
|
|
|
totalSize += entry->dataSize;
|
|
|
|
offset += round_up(ROMFS_NONAME_FILEENTRY_SIZE + entry->nameLen, 4);
|
|
}
|
|
|
|
*out = totalSize;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool calculateRomFsExtractedDirSize(u32 dir_offset, bool usePatch, u64 *out)
|
|
{
|
|
if ((!usePatch && (!romFsContext.romfs_dirtable_size || !romFsContext.romfs_dir_entries || !romFsContext.romfs_filetable_size || !romFsContext.romfs_file_entries || dir_offset > romFsContext.romfs_dirtable_size)) || (usePatch && (!bktrContext.romfs_dirtable_size || !bktrContext.romfs_dir_entries || !bktrContext.romfs_filetable_size || !bktrContext.romfs_file_entries || dir_offset > bktrContext.romfs_dirtable_size)) || !out)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to calculate extracted size for the current RomFS directory!", __func__);
|
|
return false;
|
|
}
|
|
|
|
u64 totalSize = 0, childDirSize = 0;
|
|
romfs_file *fileEntry = NULL;
|
|
romfs_dir *childDirEntry = NULL;
|
|
romfs_dir *dirEntry = (!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 (!dirEntry->nameLen && dir_offset > 0)
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: directory entry without name in RomFS section!", __func__);
|
|
return false;
|
|
}
|
|
|
|
if (dirEntry->childFile != ROMFS_ENTRY_EMPTY)
|
|
{
|
|
fileEntry = (!usePatch ? (romfs_file*)((u8*)romFsContext.romfs_file_entries + dirEntry->childFile) : (romfs_file*)((u8*)bktrContext.romfs_file_entries + dirEntry->childFile));
|
|
|
|
totalSize += fileEntry->dataSize;
|
|
|
|
while(fileEntry->sibling != ROMFS_ENTRY_EMPTY)
|
|
{
|
|
fileEntry = (!usePatch ? (romfs_file*)((u8*)romFsContext.romfs_file_entries + fileEntry->sibling) : (romfs_file*)((u8*)bktrContext.romfs_file_entries + fileEntry->sibling));
|
|
|
|
totalSize += fileEntry->dataSize;
|
|
}
|
|
}
|
|
|
|
if (dirEntry->childDir != ROMFS_ENTRY_EMPTY)
|
|
{
|
|
if (!calculateRomFsExtractedDirSize(dirEntry->childDir, usePatch, &childDirSize)) return false;
|
|
|
|
totalSize += childDirSize;
|
|
|
|
childDirEntry = (!usePatch ? (romfs_dir*)((u8*)romFsContext.romfs_dir_entries + dirEntry->childDir) : (romfs_dir*)((u8*)bktrContext.romfs_dir_entries + dirEntry->childDir));
|
|
|
|
while(childDirEntry->sibling != ROMFS_ENTRY_EMPTY)
|
|
{
|
|
if (!calculateRomFsExtractedDirSize(childDirEntry->sibling, usePatch, &childDirSize)) return false;
|
|
|
|
totalSize += childDirSize;
|
|
|
|
childDirEntry = (!usePatch ? (romfs_dir*)((u8*)romFsContext.romfs_dir_entries + childDirEntry->sibling) : (romfs_dir*)((u8*)bktrContext.romfs_dir_entries + childDirEntry->sibling));
|
|
}
|
|
}
|
|
|
|
*out = totalSize;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool retrieveContentInfosFromTitle(NcmStorageId storageId, NcmContentMetaType metaType, u32 titleCount, u32 titleIndex, NcmContentInfo **outContentInfos, u32 *outContentInfoCnt)
|
|
{
|
|
Result result;
|
|
|
|
NcmContentMetaDatabase ncmDb;
|
|
memset(&ncmDb, 0, sizeof(NcmContentMetaDatabase));
|
|
|
|
NcmContentMetaHeader cnmtHeader;
|
|
memset(&cnmtHeader, 0, sizeof(NcmContentMetaHeader));
|
|
u64 cnmtHeaderReadSize = 0;
|
|
|
|
NcmApplicationContentMetaKey *titleList = NULL;
|
|
size_t titleListSize = (sizeof(NcmApplicationContentMetaKey) * titleCount);
|
|
|
|
NcmContentInfo *titleContentInfos = NULL;
|
|
u32 titleContentInfoCnt = 0;
|
|
|
|
u32 written = 0, total = 0;
|
|
|
|
bool success = false;
|
|
|
|
if (storageId != NcmStorageId_GameCard && storageId != NcmStorageId_SdCard && storageId != NcmStorageId_BuiltInUser)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: invalid title storage ID!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if (metaType != NcmContentMetaType_Application && metaType != NcmContentMetaType_Patch && metaType != NcmContentMetaType_AddOnContent)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: invalid title meta type!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if (!titleCount)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: invalid title type count!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if (titleIndex >= titleCount)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: invalid title index!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if (!outContentInfos || !outContentInfoCnt)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: invalid output parameters!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
titleList = calloc(1, titleListSize);
|
|
if (!titleList)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: unable to allocate memory for the ApplicationContentMetaKey struct!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
result = ncmOpenContentMetaDatabase(&ncmDb, storageId);
|
|
if (R_FAILED(result))
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: ncmOpenContentMetaDatabase failed! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
|
|
result = ncmContentMetaDatabaseListApplication(&ncmDb, (s32*)&total, (s32*)&written, titleList, (s32)titleCount, metaType);
|
|
if (R_FAILED(result))
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: ncmContentMetaDatabaseListApplication failed! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
|
|
if (!written || !total)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: ncmContentMetaDatabaseListApplication wrote no entries to output buffer!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
if (written != total)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: title count mismatch in ncmContentMetaDatabaseListApplication! (%u != %u)", __func__, written, total);
|
|
goto out;
|
|
}
|
|
|
|
if (titleIndex >= total)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: provided title index exceeds title count from ncmContentMetaDatabaseListApplication!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
result = ncmContentMetaDatabaseGet(&ncmDb, &(titleList[titleIndex].key), &cnmtHeaderReadSize, &cnmtHeader, sizeof(NcmContentMetaHeader));
|
|
if (R_FAILED(result))
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: ncmContentMetaDatabaseGet failed! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
|
|
titleContentInfoCnt = (u32)(cnmtHeader.content_count);
|
|
|
|
titleContentInfos = calloc(titleContentInfoCnt, sizeof(NcmContentInfo));
|
|
if (!titleContentInfos)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: unable to allocate memory for the title content information struct!", __func__);
|
|
goto out;
|
|
}
|
|
|
|
written = 0;
|
|
|
|
result = ncmContentMetaDatabaseListContentInfo(&ncmDb, (s32*)&written, titleContentInfos, (s32)titleContentInfoCnt, &(titleList[titleIndex].key), 0);
|
|
if (R_FAILED(result))
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: ncmContentMetaDatabaseListContentInfo failed! (0x%08X)", __func__, result);
|
|
goto out;
|
|
}
|
|
|
|
if (written != titleContentInfoCnt)
|
|
{
|
|
snprintf(strbuf, MAX_CHARACTERS(strbuf), "%s: title content count mismatch in ncmContentMetaDatabaseListContentInfo! (%u != %u)", __func__, written, titleContentInfoCnt);
|
|
goto out;
|
|
}
|
|
|
|
success = true;
|
|
|
|
// Update output parameters
|
|
*outContentInfos = titleContentInfos;
|
|
*outContentInfoCnt = titleContentInfoCnt;
|
|
|
|
out:
|
|
if (!success && titleContentInfos) free(titleContentInfos);
|
|
|
|
ncmContentMetaDatabaseClose(&ncmDb);
|
|
|
|
if (titleList) free(titleList);
|
|
|
|
return success;
|
|
}
|
|
|
|
void removeConsoleDataFromTicket(title_rights_ctx *rights_info)
|
|
{
|
|
if (!rights_info || !rights_info->has_rights_id || !rights_info->retrieved_tik || rights_info->missing_tik || rights_info->tik_data.titlekey_type != ETICKET_TITLEKEY_PERSONALIZED) return;
|
|
|
|
memset(rights_info->tik_data.signature, 0xFF, 0x100);
|
|
|
|
memset(rights_info->tik_data.sig_issuer, 0, 0x40);
|
|
sprintf(rights_info->tik_data.sig_issuer, "Root-CA00000003-XS00000020");
|
|
|
|
memset(rights_info->tik_data.titlekey_block, 0, 0x100);
|
|
memcpy(rights_info->tik_data.titlekey_block, rights_info->enc_titlekey, 0x10);
|
|
|
|
rights_info->tik_data.titlekey_type = ETICKET_TITLEKEY_COMMON;
|
|
rights_info->tik_data.property_mask &= ~(BIT(5) | BIT(4));
|
|
rights_info->tik_data.ticket_id = 0;
|
|
rights_info->tik_data.device_id = 0;
|
|
rights_info->tik_data.account_id = 0;
|
|
|
|
rights_info->tik_data.sect_total_size = 0;
|
|
rights_info->tik_data.sect_hdr_offset = (u32)sizeof(rsa2048_sha256_ticket);
|
|
rights_info->tik_data.sect_hdr_count = 0;
|
|
rights_info->tik_data.sect_hdr_entry_size = 0;
|
|
}
|
|
|
|
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. Press B to go back.", 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();
|
|
|
|
scanPads();
|
|
|
|
keysDown = getButtonsDown();
|
|
keysHeld = getButtonsHeld();
|
|
|
|
if (keysDown || keysHeld) break;
|
|
}
|
|
|
|
if (keysDown & HidNpadButton_A)
|
|
{
|
|
idx = (int)indexes[selectedContent];
|
|
break;
|
|
}
|
|
|
|
if ((keysDown & HidNpadButton_Up) || (keysDown & HidNpadButton_StickLUp) || (keysHeld & HidNpadButton_StickRUp))
|
|
{
|
|
if (selectedContent > 0) selectedContent--;
|
|
}
|
|
|
|
if ((keysDown & HidNpadButton_Down) || (keysDown & HidNpadButton_StickLDown) || (keysHeld & HidNpadButton_StickRDown))
|
|
{
|
|
if (selectedContent < (cnt - 1)) selectedContent++;
|
|
}
|
|
|
|
if (keysDown & HidNpadButton_B)
|
|
{
|
|
breaks = initial_breaks;
|
|
uiFill(0, 8 + (breaks * LINE_HEIGHT), FB_WIDTH, FB_HEIGHT - (8 + (breaks * LINE_HEIGHT)), BG_COLOR_RGB);
|
|
uiRefreshDisplay();
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
// Read BKTR entry data in the Program NCA from the update
|
|
ret = (parseBktrEntryFromNca(&ncmStorage, &ncaId, &dec_nca_header, decrypted_nca_keys, ret == 0) ? 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;
|
|
}
|
|
|
|
char *generateGameCardDumpName(bool useBrackets)
|
|
{
|
|
if (menuType != MENUTYPE_GAMECARD || !titleAppCount || !baseAppEntries) return NULL;
|
|
|
|
u32 i, j;
|
|
|
|
char tmp[NAME_BUF_LEN / 2] = {'\0'};
|
|
char *fullname = NULL;
|
|
char *fullnameTmp = NULL;
|
|
|
|
size_t strsize = NAME_BUF_LEN;
|
|
|
|
fullname = calloc(strsize + 1, sizeof(char));
|
|
if (!fullname) return NULL;
|
|
|
|
for(i = 0; i < titleAppCount; i++)
|
|
{
|
|
u32 highestVersion = baseAppEntries[i].version;
|
|
|
|
// Check if our current gamecard has any bundled updates for this application. If so, use the highest update version available
|
|
if (titlePatchCount && patchEntries)
|
|
{
|
|
for(j = 0; j < titlePatchCount; j++)
|
|
{
|
|
if (checkIfPatchOrAddOnBelongsToBaseApplication(j, i, false) && patchEntries[j].version > highestVersion) highestVersion = patchEntries[j].version;
|
|
}
|
|
}
|
|
|
|
if (useBrackets)
|
|
{
|
|
snprintf(tmp, MAX_CHARACTERS(tmp), "%s [%016lX][v%u]", baseAppEntries[i].fixedName, baseAppEntries[i].titleId, highestVersion);
|
|
} else {
|
|
snprintf(tmp, MAX_CHARACTERS(tmp), "%s v%u (%016lX)", baseAppEntries[i].fixedName, highestVersion, baseAppEntries[i].titleId);
|
|
}
|
|
|
|
if ((strlen(fullname) + strlen(tmp) + 4) > strsize)
|
|
{
|
|
size_t fullname_len = strlen(fullname);
|
|
|
|
strsize = (fullname_len + strlen(tmp) + 4);
|
|
|
|
fullnameTmp = realloc(fullname, strsize);
|
|
if (fullnameTmp)
|
|
{
|
|
fullname = fullnameTmp;
|
|
fullnameTmp = NULL;
|
|
memset(fullname + fullname_len, 0, strlen(tmp) + 4);
|
|
} else {
|
|
free(fullname);
|
|
fullname = NULL;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (i > 0) strcat(fullname, " + ");
|
|
|
|
strcat(fullname, tmp);
|
|
}
|
|
|
|
return fullname;
|
|
}
|
|
|
|
char *generateNSPDumpName(nspDumpType selectedNspDumpType, u32 titleIndex, bool useBrackets)
|
|
{
|
|
if ((selectedNspDumpType == DUMP_APP_NSP && (!titleAppCount || !baseAppEntries || titleIndex >= titleAppCount)) || (selectedNspDumpType == DUMP_PATCH_NSP && (!titlePatchCount || !patchEntries || titleIndex >= titlePatchCount)) || (selectedNspDumpType == DUMP_ADDON_NSP && (!titleAddOnCount || !addOnEntries || titleIndex >= titleAddOnCount))) return NULL;
|
|
|
|
u32 i;
|
|
size_t strsize = NAME_BUF_LEN;
|
|
|
|
patch_addon_ctx_t *ptr = NULL;
|
|
|
|
char *fullname = calloc(strsize + 1, sizeof(char));
|
|
if (!fullname) return NULL;
|
|
|
|
switch(selectedNspDumpType)
|
|
{
|
|
case DUMP_APP_NSP:
|
|
if (useBrackets)
|
|
{
|
|
snprintf(fullname, strsize, "%s [%016lX][v%u][BASE]", baseAppEntries[titleIndex].fixedName, baseAppEntries[titleIndex].titleId, baseAppEntries[titleIndex].version);
|
|
} else {
|
|
snprintf(fullname, strsize, "%s v%u (%016lX) (BASE)", baseAppEntries[titleIndex].fixedName, baseAppEntries[titleIndex].version, baseAppEntries[titleIndex].titleId);
|
|
}
|
|
break;
|
|
case DUMP_PATCH_NSP:
|
|
case DUMP_ADDON_NSP:
|
|
ptr = (selectedNspDumpType == DUMP_PATCH_NSP ? &(patchEntries[titleIndex]) : &(addOnEntries[titleIndex]));
|
|
|
|
// Look for the parent base application name
|
|
if (titleAppCount && baseAppEntries)
|
|
{
|
|
for(i = 0; i < titleAppCount; i++)
|
|
{
|
|
if (checkIfPatchOrAddOnBelongsToBaseApplication(titleIndex, i, (selectedNspDumpType == DUMP_ADDON_NSP)))
|
|
{
|
|
if (useBrackets)
|
|
{
|
|
snprintf(fullname, strsize, "%s [%016lX][v%u][%s]", baseAppEntries[i].fixedName, ptr->titleId, ptr->version, (selectedNspDumpType == DUMP_PATCH_NSP ? "UPD" : "DLC"));
|
|
} else {
|
|
snprintf(fullname, strsize, "%s v%u (%016lX) (%s)", baseAppEntries[i].fixedName, ptr->version, ptr->titleId, (selectedNspDumpType == DUMP_PATCH_NSP ? "UPD" : "DLC"));
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!strlen(fullname))
|
|
{
|
|
// Look for the parent base application name in orphan entries
|
|
if (orphanEntries && orphanEntriesCnt)
|
|
{
|
|
for(i = 0; i < orphanEntriesCnt; i++)
|
|
{
|
|
if (orphanEntries[i].index == titleIndex && strlen(orphanEntries[i].fixedName) && ((selectedNspDumpType == DUMP_PATCH_NSP && orphanEntries[i].type == ORPHAN_ENTRY_TYPE_PATCH) || (selectedNspDumpType == DUMP_ADDON_NSP && orphanEntries[i].type == ORPHAN_ENTRY_TYPE_ADDON)))
|
|
{
|
|
if (useBrackets)
|
|
{
|
|
snprintf(fullname, strsize, "%s [%016lX][v%u][%s]", orphanEntries[i].fixedName, ptr->titleId, ptr->version, (selectedNspDumpType == DUMP_PATCH_NSP ? "UPD" : "DLC"));
|
|
} else {
|
|
snprintf(fullname, strsize, "%s v%u (%016lX) (%s)", orphanEntries[i].fixedName, ptr->version, ptr->titleId, (selectedNspDumpType == DUMP_PATCH_NSP ? "UPD" : "DLC"));
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!strlen(fullname))
|
|
{
|
|
// Nothing worked, just print the Title ID + version
|
|
if (useBrackets)
|
|
{
|
|
snprintf(fullname, strsize, "[%016lX][v%u][%s]", ptr->titleId, ptr->version, (selectedNspDumpType == DUMP_PATCH_NSP ? "UPD" : "DLC"));
|
|
} else {
|
|
snprintf(fullname, strsize, "%016lX v%u (%s)", ptr->titleId, ptr->version, (selectedNspDumpType == DUMP_PATCH_NSP ? "UPD" : "DLC"));
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
default:
|
|
free(fullname);
|
|
fullname = NULL;
|
|
break;
|
|
}
|
|
|
|
return fullname;
|
|
}
|
|
|
|
void retrieveDescriptionForPatchOrAddOn(u32 titleIndex, bool addOn, bool addAppName, const char *prefix, char *outBuf, size_t outBufSize)
|
|
{
|
|
if ((!addOn && (!titlePatchCount || !patchEntries || titleIndex >= titlePatchCount)) || (addOn && (!titleAddOnCount || !addOnEntries || titleIndex >= titleAddOnCount)) || !outBuf || !outBufSize) return;
|
|
|
|
u32 i;
|
|
bool addPrefix = (prefix && strlen(prefix));
|
|
patch_addon_ctx_t *ptr = (!addOn ? &(patchEntries[titleIndex]) : &(addOnEntries[titleIndex]));
|
|
|
|
// Check if we need to add the base application name
|
|
if (!addAppName || !titleAppCount || !baseAppEntries)
|
|
{
|
|
if (addPrefix)
|
|
{
|
|
snprintf(outBuf, outBufSize, "%s%016lX v%s", prefix, ptr->titleId, ptr->versionStr);
|
|
} else {
|
|
snprintf(outBuf, outBufSize, "%016lX v%s", ptr->titleId, ptr->versionStr);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Look for the parent base application name
|
|
for(i = 0; i < titleAppCount; i++)
|
|
{
|
|
if (checkIfPatchOrAddOnBelongsToBaseApplication(titleIndex, i, addOn))
|
|
{
|
|
if (addPrefix)
|
|
{
|
|
snprintf(outBuf, outBufSize, "%s%s | %016lX v%s", prefix, baseAppEntries[i].name, ptr->titleId, ptr->versionStr);
|
|
} else {
|
|
snprintf(outBuf, outBufSize, "%s | %016lX v%s", baseAppEntries[i].name, ptr->titleId, ptr->versionStr);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Look for the parent base application name in orphan entries
|
|
if (orphanEntries != NULL && orphanEntriesCnt)
|
|
{
|
|
for(i = 0; i < orphanEntriesCnt; i++)
|
|
{
|
|
if (orphanEntries[i].index == titleIndex && strlen(orphanEntries[i].name) && ((!addOn && orphanEntries[i].type == ORPHAN_ENTRY_TYPE_PATCH) || (addOn && orphanEntries[i].type == ORPHAN_ENTRY_TYPE_ADDON)))
|
|
{
|
|
if (addPrefix)
|
|
{
|
|
snprintf(outBuf, outBufSize, "%s%s | %016lX v%s", prefix, orphanEntries[i].name, ptr->titleId, ptr->versionStr);
|
|
} else {
|
|
snprintf(outBuf, outBufSize, "%s | %016lX v%s", orphanEntries[i].name, ptr->titleId, ptr->versionStr);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Nothing worked, just print the Title ID + version
|
|
if (addPrefix)
|
|
{
|
|
snprintf(outBuf, outBufSize, "%s%016lX v%s", prefix, ptr->titleId, ptr->versionStr);
|
|
} else {
|
|
snprintf(outBuf, outBufSize, "%016lX v%s", ptr->titleId, ptr->versionStr);
|
|
}
|
|
}
|
|
|
|
u32 calculateOrphanPatchOrAddOnCount(bool addOn)
|
|
{
|
|
if ((!addOn && (!titlePatchCount || !patchEntries)) || (addOn && (!titleAddOnCount || !addOnEntries))) return 0;
|
|
|
|
if ((!titleAppCount || !baseAppEntries) && ((!addOn && titlePatchCount && patchEntries) || (addOn && titleAddOnCount && addOnEntries))) return (!addOn ? titlePatchCount : titleAddOnCount);
|
|
|
|
u32 i, j;
|
|
u32 titleCount = (!addOn ? titlePatchCount : titleAddOnCount);
|
|
u32 orphanCnt = 0;
|
|
|
|
for(i = 0; i < titleCount; i++)
|
|
{
|
|
bool foundMatch = false;
|
|
|
|
for(j = 0; j < titleAppCount; j++)
|
|
{
|
|
if ((!addOn && patchEntries[i].titleId == (baseAppEntries[j].titleId | APPLICATION_PATCH_BITMASK)) || (addOn && (addOnEntries[i].titleId & APPLICATION_ADDON_BITMASK) == (baseAppEntries[j].titleId & APPLICATION_ADDON_BITMASK)))
|
|
{
|
|
foundMatch = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (foundMatch) continue;
|
|
|
|
orphanCnt++;
|
|
}
|
|
|
|
return orphanCnt;
|
|
}
|
|
|
|
void generateOrphanPatchOrAddOnList()
|
|
{
|
|
Result result;
|
|
u32 nsAppRecordCnt = 0;
|
|
|
|
bool foundMatch;
|
|
u32 i, j, k;
|
|
|
|
u32 orphanEntryIndex = 0;
|
|
u32 orphanPatchCount = calculateOrphanPatchOrAddOnCount(false);
|
|
u32 orphanAddOnCount = calculateOrphanPatchOrAddOnCount(true);
|
|
|
|
if (!orphanPatchCount && !orphanAddOnCount) return;
|
|
|
|
if (orphanEntries && orphanEntriesCnt && orphanEntriesCnt == (orphanPatchCount + orphanAddOnCount)) goto out;
|
|
|
|
freeOrphanPatchOrAddOnList();
|
|
|
|
// Retrieve all cached Application IDs (assuming no one has more than 2048 cached base applications...)
|
|
NsApplicationRecord *appRecords = calloc(2048, sizeof(NsApplicationRecord));
|
|
if (!appRecords) return;
|
|
|
|
result = nsListApplicationRecord(appRecords, 2048, 0, (s32*)&nsAppRecordCnt);
|
|
if (R_FAILED(result))
|
|
{
|
|
free(appRecords);
|
|
return;
|
|
}
|
|
|
|
// Allocate memory for our orphan entries
|
|
orphanEntries = calloc(orphanPatchCount + orphanAddOnCount, sizeof(orphan_patch_addon_entry));
|
|
if (!orphanEntries)
|
|
{
|
|
free(appRecords);
|
|
return;
|
|
}
|
|
|
|
// Save orphan patch & add-on data
|
|
for(i = 0; i < 2; i++)
|
|
{
|
|
u32 titleCount = (i == 0 ? titlePatchCount : titleAddOnCount);
|
|
|
|
for(j = 0; j < titleCount; j++)
|
|
{
|
|
foundMatch = false;
|
|
|
|
if (titleAppCount && baseAppEntries)
|
|
{
|
|
for(k = 0; k < titleAppCount; k++)
|
|
{
|
|
if (checkIfPatchOrAddOnBelongsToBaseApplication(j, k, (i == 1)))
|
|
{
|
|
foundMatch = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (foundMatch) continue;
|
|
|
|
patch_addon_ctx_t *ptr = (i == 0 ? &(patchEntries[j]) : &(addOnEntries[j]));
|
|
|
|
// Look for a matching Application ID in our NS records
|
|
for(k = 0; k < nsAppRecordCnt; k++)
|
|
{
|
|
if ((i == 0 && ptr->titleId == (appRecords[k].application_id | APPLICATION_PATCH_BITMASK)) || (i == 1 && (ptr->titleId & APPLICATION_ADDON_BITMASK) == (appRecords[k].application_id & APPLICATION_ADDON_BITMASK)))
|
|
{
|
|
if (getCachedBaseApplicationNacpMetadata(appRecords[k].application_id, orphanEntries[orphanEntryIndex].name, MAX_CHARACTERS(orphanEntries[orphanEntryIndex].name), NULL, 0, NULL))
|
|
{
|
|
strtrim(orphanEntries[orphanEntryIndex].name);
|
|
snprintf(orphanEntries[orphanEntryIndex].fixedName, MAX_CHARACTERS(orphanEntries[orphanEntryIndex].fixedName), orphanEntries[orphanEntryIndex].name);
|
|
removeIllegalCharacters(orphanEntries[orphanEntryIndex].fixedName);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (strlen(orphanEntries[orphanEntryIndex].name))
|
|
{
|
|
snprintf(orphanEntries[orphanEntryIndex].orphanListStr, MAX_CHARACTERS(orphanEntries[orphanEntryIndex].orphanListStr), "%s v%u (%016lX) (%s)", orphanEntries[orphanEntryIndex].name, ptr->version, ptr->titleId, (i == 0 ? "Update" : "DLC"));
|
|
} else {
|
|
snprintf(orphanEntries[orphanEntryIndex].orphanListStr, MAX_CHARACTERS(orphanEntries[orphanEntryIndex].orphanListStr), "%016lX v%u (%s)", ptr->titleId, ptr->version, (i == 0 ? "Update" : "DLC"));
|
|
}
|
|
|
|
orphanEntries[orphanEntryIndex].index = j;
|
|
orphanEntries[orphanEntryIndex].type = (i == 0 ? ORPHAN_ENTRY_TYPE_PATCH : ORPHAN_ENTRY_TYPE_ADDON);
|
|
|
|
orphanEntryIndex++;
|
|
}
|
|
}
|
|
|
|
orphanEntriesCnt = (orphanPatchCount + orphanAddOnCount);
|
|
|
|
free(appRecords);
|
|
|
|
// Sort orphan titles by name
|
|
qsort(orphanEntries, orphanEntriesCnt, sizeof(orphan_patch_addon_entry), orphanEntryCmp);
|
|
|
|
out:
|
|
if (!allocateFilenameBuffer(orphanEntriesCnt)) return;
|
|
|
|
for(i = 0; i < orphanEntriesCnt; i++)
|
|
{
|
|
if (!addStringToFilenameBuffer(orphanEntries[i].orphanListStr)) return;
|
|
}
|
|
}
|
|
|
|
bool checkIfBaseApplicationHasPatchOrAddOn(u32 appIndex, bool addOn)
|
|
{
|
|
if (!titleAppCount || !baseAppEntries || appIndex >= titleAppCount || (!addOn && (!titlePatchCount || !patchEntries)) || (addOn && (!titleAddOnCount || !addOnEntries))) return false;
|
|
|
|
u32 i;
|
|
u32 count = (!addOn ? titlePatchCount : titleAddOnCount);
|
|
|
|
for(i = 0; i < count; i++)
|
|
{
|
|
if ((!addOn && (baseAppEntries[appIndex].titleId | APPLICATION_PATCH_BITMASK) == patchEntries[i].titleId) || (addOn && (baseAppEntries[appIndex].titleId & APPLICATION_ADDON_BITMASK) == (addOnEntries[i].titleId & APPLICATION_ADDON_BITMASK))) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool checkIfPatchOrAddOnBelongsToBaseApplication(u32 titleIndex, u32 appIndex, bool addOn)
|
|
{
|
|
if (!titleAppCount || !baseAppEntries || appIndex >= titleAppCount || (!addOn && (!titlePatchCount || !patchEntries || titleIndex >= titlePatchCount)) || (addOn && (!titleAddOnCount || !addOnEntries || titleIndex >= titleAddOnCount))) return false;
|
|
|
|
if ((!addOn && patchEntries[titleIndex].titleId == (baseAppEntries[appIndex].titleId | APPLICATION_PATCH_BITMASK)) || (addOn && (addOnEntries[titleIndex].titleId & APPLICATION_ADDON_BITMASK) == (baseAppEntries[appIndex].titleId & APPLICATION_ADDON_BITMASK))) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
u32 retrieveFirstPatchOrAddOnIndexFromBaseApplication(u32 appIndex, bool addOn)
|
|
{
|
|
if (!titleAppCount || !baseAppEntries || appIndex >= titleAppCount || (!addOn && (!titlePatchCount || !patchEntries)) || (addOn && (!titleAddOnCount || !addOnEntries))) return 0;
|
|
|
|
u32 titleIndex;
|
|
u32 count = (!addOn ? titlePatchCount : titleAddOnCount);
|
|
|
|
for(titleIndex = 0; titleIndex < count; titleIndex++)
|
|
{
|
|
if (checkIfPatchOrAddOnBelongsToBaseApplication(titleIndex, appIndex, addOn)) return titleIndex;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
u32 retrievePreviousPatchOrAddOnIndexFromBaseApplication(u32 startTitleIndex, u32 appIndex, bool addOn)
|
|
{
|
|
u32 count = (!addOn ? titlePatchCount : titleAddOnCount);
|
|
u32 retTitleIndex = startTitleIndex;
|
|
u32 curTitleIndex = 0;
|
|
|
|
if (!startTitleIndex || startTitleIndex >= count || !titleAppCount || !baseAppEntries || appIndex >= titleAppCount || (!addOn && (!titlePatchCount || !patchEntries)) || (addOn && (!titleAddOnCount || !addOnEntries))) return retTitleIndex;
|
|
|
|
for(curTitleIndex = startTitleIndex; curTitleIndex > 0; curTitleIndex--)
|
|
{
|
|
if (checkIfPatchOrAddOnBelongsToBaseApplication(curTitleIndex - 1, appIndex, addOn))
|
|
{
|
|
retTitleIndex = (curTitleIndex - 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return retTitleIndex;
|
|
}
|
|
|
|
u32 retrieveNextPatchOrAddOnIndexFromBaseApplication(u32 startTitleIndex, u32 appIndex, bool addOn)
|
|
{
|
|
u32 count = (!addOn ? titlePatchCount : titleAddOnCount);
|
|
u32 retTitleIndex = startTitleIndex;
|
|
u32 curTitleIndex = 0;
|
|
|
|
if (startTitleIndex >= count || !titleAppCount || !baseAppEntries || appIndex >= titleAppCount || (!addOn && (!titlePatchCount || !patchEntries)) || (addOn && (!titleAddOnCount || !addOnEntries))) return retTitleIndex;
|
|
|
|
for(curTitleIndex = (startTitleIndex + 1); curTitleIndex < count; curTitleIndex++)
|
|
{
|
|
if (checkIfPatchOrAddOnBelongsToBaseApplication(curTitleIndex, appIndex, addOn))
|
|
{
|
|
retTitleIndex = curTitleIndex;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return retTitleIndex;
|
|
}
|
|
|
|
u32 retrieveLastPatchOrAddOnIndexFromBaseApplication(u32 appIndex, bool addOn)
|
|
{
|
|
if (!titleAppCount || !baseAppEntries || appIndex >= titleAppCount || (!addOn && (!titlePatchCount || !patchEntries)) || (addOn && (!titleAddOnCount || !addOnEntries))) return 0;
|
|
|
|
u32 titleIndex;
|
|
u32 count = (!addOn ? titlePatchCount : titleAddOnCount);
|
|
|
|
for(titleIndex = count; titleIndex > 0; titleIndex--)
|
|
{
|
|
if (checkIfPatchOrAddOnBelongsToBaseApplication(titleIndex - 1, appIndex, addOn)) return (titleIndex - 1);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void waitForButtonPress()
|
|
{
|
|
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Press any button to continue");
|
|
|
|
/* Don't consider stick movement as button inputs. */
|
|
u64 flag = ~(HidNpadButton_StickLLeft | HidNpadButton_StickLRight | HidNpadButton_StickLUp | HidNpadButton_StickLDown | HidNpadButton_StickRLeft | HidNpadButton_StickRRight | \
|
|
HidNpadButton_StickRUp | HidNpadButton_StickRDown);
|
|
|
|
while(true)
|
|
{
|
|
uiUpdateStatusMsg();
|
|
uiRefreshDisplay();
|
|
|
|
scanPads();
|
|
if (getButtonsDown() & flag) break;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
scanPads();
|
|
|
|
progressCtx->cancelBtnState = (getButtonsHeld() & HidNpadButton_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;
|
|
}
|
|
|
|
void convertDataToHexString(const u8 *data, const u32 dataSize, char *outBuf, const u32 outBufSize)
|
|
{
|
|
if (!data || !dataSize || !outBuf || !outBufSize || outBufSize < ((dataSize * 2) + 1)) return;
|
|
|
|
u32 i;
|
|
char tmp[3] = {'\0'};
|
|
|
|
memset(outBuf, 0, outBufSize);
|
|
|
|
for(i = 0; i < dataSize; i++)
|
|
{
|
|
sprintf(tmp, "%02x", data[i]);
|
|
strcat(outBuf, tmp);
|
|
}
|
|
}
|
|
|
|
bool checkIfFileExists(const char *path)
|
|
{
|
|
if (!path || !strlen(path)) return false;
|
|
|
|
FILE *chkfile = fopen(path, "rb");
|
|
if (chkfile)
|
|
{
|
|
fclose(chkfile);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool yesNoPrompt(const char *message)
|
|
{
|
|
if (message && strlen(message))
|
|
{
|
|
uiDrawString(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();
|
|
|
|
scanPads();
|
|
|
|
u64 keysDown = getButtonsDown();
|
|
|
|
if (keysDown & HidNpadButton_A)
|
|
{
|
|
ret = true;
|
|
break;
|
|
} else
|
|
if (keysDown & HidNpadButton_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;
|
|
}
|