mirror of
https://github.com/DarkMatterCore/nxdumptool.git
synced 2024-11-23 02:36:41 +00:00
3391 lines
128 KiB
C
3391 lines
128 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 "dumper.h"
|
|
#include "fs_ext.h"
|
|
#include "ui.h"
|
|
#include "util.h"
|
|
|
|
/* Extern variables */
|
|
|
|
extern int breaks;
|
|
extern int font_height;
|
|
|
|
extern int cursor;
|
|
extern int scroll;
|
|
|
|
extern curMenuType menuType;
|
|
|
|
/* Constants */
|
|
|
|
const char *nswReleasesXmlUrl = "http://nswdb.com/xml.php";
|
|
const char *nswReleasesXmlTmpPath = OUTPUT_DUMP_BASE_PATH "NSWreleases.xml.tmp";
|
|
const char *nswReleasesXmlPath = OUTPUT_DUMP_BASE_PATH "NSWreleases.xml";
|
|
const char *nswReleasesRootElement = "releases";
|
|
const char *nswReleasesChildren = "release";
|
|
const char *nswReleasesChildrenImageSize = "imagesize";
|
|
const char *nswReleasesChildrenTitleID = "titleid";
|
|
const char *nswReleasesChildrenImgCrc = "imgcrc";
|
|
const char *nswReleasesChildrenReleaseName = "releasename";
|
|
|
|
const char *githubReleasesApiUrl = "https://api.github.com/repos/DarkMatterCore/nxdumptool/releases/latest";
|
|
const char *nxDumpToolPath = "sdmc:/switch/nxdumptool.nro";
|
|
const char *userAgent = "nxdumptool/" APP_VERSION " (Nintendo Switch)";
|
|
|
|
/* Statically allocated variables */
|
|
|
|
static char *result_buf = NULL;
|
|
static size_t result_sz = 0;
|
|
static size_t result_written = 0;
|
|
|
|
char *filenameBuffer = NULL;
|
|
char *filenames[FILENAME_MAX_CNT];
|
|
int filenamesCount = 0;
|
|
|
|
FsDeviceOperator fsOperatorInstance;
|
|
FsEventNotifier fsGameCardEventNotifier;
|
|
Handle fsGameCardEventHandle;
|
|
Event fsGameCardKernelEvent;
|
|
UEvent exitEvent;
|
|
|
|
AppletType programAppletType;
|
|
|
|
bool gameCardInserted;
|
|
|
|
u64 gameCardSize = 0, trimmedCardSize = 0;
|
|
char gameCardSizeStr[32] = {'\0'}, trimmedCardSizeStr[32] = {'\0'};
|
|
|
|
u64 gameCardUpdateTitleID = 0;
|
|
u32 gameCardUpdateVersion = 0;
|
|
char gameCardUpdateVersionStr[128] = {'\0'};
|
|
|
|
u8 *hfs0_header = NULL;
|
|
u64 hfs0_offset = 0, hfs0_size = 0;
|
|
u32 hfs0_partition_cnt = 0;
|
|
|
|
u8 *partitionHfs0Header = NULL;
|
|
u64 partitionHfs0HeaderOffset = 0, partitionHfs0HeaderSize = 0;
|
|
u32 partitionHfs0FileCount = 0, partitionHfs0StrTableSize = 0;
|
|
|
|
u32 titleAppCount = 0;
|
|
u64 *titleAppTitleID = NULL;
|
|
u32 *titleAppVersion = NULL;
|
|
FsStorageId *titleAppStorageId = NULL;
|
|
|
|
u32 titlePatchCount = 0;
|
|
u64 *titlePatchTitleID = NULL;
|
|
u32 *titlePatchVersion = NULL;
|
|
FsStorageId *titlePatchStorageId = NULL;
|
|
|
|
u32 titleAddOnCount = 0;
|
|
u64 *titleAddOnTitleID = NULL;
|
|
u32 *titleAddOnVersion = NULL;
|
|
FsStorageId *titleAddOnStorageId = NULL;
|
|
|
|
u32 sdCardTitleAppCount = 0;
|
|
u32 sdCardTitlePatchCount = 0;
|
|
u32 sdCardTitleAddOnCount = 0;
|
|
|
|
u32 nandUserTitleAppCount = 0;
|
|
u32 nandUserTitlePatchCount = 0;
|
|
u32 nandUserTitleAddOnCount = 0;
|
|
|
|
char **titleName = NULL;
|
|
char **fixedTitleName = NULL;
|
|
char **titleAuthor = NULL;
|
|
char **titleAppVersionStr = NULL;
|
|
u8 **titleIcon = NULL;
|
|
|
|
exefs_ctx_t exeFsContext;
|
|
romfs_ctx_t romFsContext;
|
|
|
|
char curRomFsPath[NAME_BUF_LEN] = {'\0'};
|
|
romfs_browser_entry *romFsBrowserEntries = NULL;
|
|
|
|
orphan_patch_addon_entry *orphanEntries = NULL;
|
|
|
|
char strbuf[NAME_BUF_LEN * 4] = {'\0'};
|
|
|
|
char appLaunchPath[NAME_BUF_LEN] = {'\0'};
|
|
|
|
bool isGameCardInserted()
|
|
{
|
|
bool inserted;
|
|
if (R_FAILED(fsDeviceOperatorIsGameCardInserted(&fsOperatorInstance, &inserted))) return false;
|
|
return inserted;
|
|
}
|
|
|
|
void fsGameCardDetectionThreadFunc(void *arg)
|
|
{
|
|
int idx;
|
|
Result rc;
|
|
|
|
while(true)
|
|
{
|
|
rc = waitMulti(&idx, -1, waiterForEvent(&fsGameCardKernelEvent), waiterForUEvent(&exitEvent));
|
|
if (R_SUCCEEDED(rc))
|
|
{
|
|
if (idx == 0)
|
|
{
|
|
// Retrieve current gamecard status
|
|
gameCardInserted = isGameCardInserted();
|
|
eventClear(&fsGameCardKernelEvent);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
waitMulti(&idx, 0, waiterForEvent(&fsGameCardKernelEvent), waiterForUEvent(&exitEvent));
|
|
}
|
|
|
|
void delay(u8 seconds)
|
|
{
|
|
if (!seconds) return;
|
|
|
|
u64 nanoseconds = seconds * (u64)1000000000;
|
|
svcSleepThread(nanoseconds);
|
|
|
|
uiRefreshDisplay();
|
|
}
|
|
|
|
void formatETAString(u64 curTime, char *output, u32 outSize)
|
|
{
|
|
if (!output || !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(output, outSize, "%02luH%02luM%02luS", hour, min, sec);
|
|
}
|
|
|
|
void freeStringsPtr(char **var)
|
|
{
|
|
if (var)
|
|
{
|
|
u64 i;
|
|
for(i = 0; var[i]; i++) free(var[i]);
|
|
free(var);
|
|
}
|
|
}
|
|
|
|
void initExeFsContext()
|
|
{
|
|
memset(&(exeFsContext.ncmStorage), 0, sizeof(NcmContentStorage));
|
|
memset(&(exeFsContext.ncaId), 0, sizeof(NcmNcaId));
|
|
memset(&(exeFsContext.aes_ctx), 0, sizeof(Aes128CtrContext));
|
|
exeFsContext.exefs_offset = 0;
|
|
exeFsContext.exefs_size = 0;
|
|
memset(&(exeFsContext.exefs_header), 0, sizeof(pfs0_header));
|
|
exeFsContext.exefs_entries_offset = 0;
|
|
exeFsContext.exefs_entries = NULL;
|
|
exeFsContext.exefs_str_table_offset = 0;
|
|
exeFsContext.exefs_str_table = NULL;
|
|
exeFsContext.exefs_data_offset = 0;
|
|
}
|
|
|
|
void freeExeFsContext()
|
|
{
|
|
// Remember to close this NCM service resource
|
|
serviceClose(&(exeFsContext.ncmStorage.s));
|
|
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.ncmStorage), 0, sizeof(NcmContentStorage));
|
|
memset(&(romFsContext.ncaId), 0, sizeof(NcmNcaId));
|
|
memset(&(romFsContext.aes_ctx), 0, sizeof(Aes128CtrContext));
|
|
romFsContext.romfs_offset = 0;
|
|
romFsContext.romfs_size = 0;
|
|
romFsContext.romfs_dirtable_offset = 0;
|
|
romFsContext.romfs_dirtable_size = 0;
|
|
romFsContext.romfs_dir_entries = NULL;
|
|
romFsContext.romfs_filetable_offset = 0;
|
|
romFsContext.romfs_filetable_size = 0;
|
|
romFsContext.romfs_file_entries = NULL;
|
|
romFsContext.romfs_filedata_offset = 0;
|
|
}
|
|
|
|
void freeRomFsContext()
|
|
{
|
|
// Remember to close this NCM service resource
|
|
serviceClose(&(romFsContext.ncmStorage.s));
|
|
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 freeGameCardInfo()
|
|
{
|
|
if (hfs0_header != NULL)
|
|
{
|
|
free(hfs0_header);
|
|
hfs0_header = NULL;
|
|
}
|
|
|
|
hfs0_offset = 0;
|
|
hfs0_size = 0;
|
|
hfs0_partition_cnt = 0;
|
|
|
|
if (partitionHfs0Header != NULL)
|
|
{
|
|
free(partitionHfs0Header);
|
|
partitionHfs0Header = NULL;
|
|
}
|
|
|
|
partitionHfs0HeaderOffset = 0;
|
|
partitionHfs0HeaderSize = 0;
|
|
partitionHfs0FileCount = 0;
|
|
partitionHfs0StrTableSize = 0;
|
|
|
|
gameCardSize = 0;
|
|
memset(gameCardSizeStr, 0, sizeof(gameCardSizeStr));
|
|
|
|
trimmedCardSize = 0;
|
|
memset(trimmedCardSizeStr, 0, sizeof(trimmedCardSizeStr));
|
|
|
|
gameCardUpdateTitleID = 0;
|
|
gameCardUpdateVersion = 0;
|
|
memset(gameCardUpdateVersionStr, 0, sizeof(gameCardUpdateVersionStr));
|
|
}
|
|
|
|
void freeTitleInfo()
|
|
{
|
|
titleAppCount = 0;
|
|
|
|
if (titleAppTitleID != NULL)
|
|
{
|
|
free(titleAppTitleID);
|
|
titleAppTitleID = NULL;
|
|
}
|
|
|
|
if (titleAppVersion != NULL)
|
|
{
|
|
free(titleAppVersion);
|
|
titleAppVersion = NULL;
|
|
}
|
|
|
|
if (titleAppVersionStr != NULL)
|
|
{
|
|
freeStringsPtr(titleAppVersionStr);
|
|
titleAppVersionStr = NULL;
|
|
}
|
|
|
|
if (titleAppStorageId != NULL)
|
|
{
|
|
free(titleAppStorageId);
|
|
titleAppStorageId = NULL;
|
|
}
|
|
|
|
titlePatchCount = 0;
|
|
|
|
if (titlePatchTitleID != NULL)
|
|
{
|
|
free(titlePatchTitleID);
|
|
titlePatchTitleID = NULL;
|
|
}
|
|
|
|
if (titlePatchVersion != NULL)
|
|
{
|
|
free(titlePatchVersion);
|
|
titlePatchVersion = NULL;
|
|
}
|
|
|
|
if (titlePatchStorageId != NULL)
|
|
{
|
|
free(titlePatchStorageId);
|
|
titlePatchStorageId = NULL;
|
|
}
|
|
|
|
titleAddOnCount = 0;
|
|
|
|
if (titleAddOnTitleID != NULL)
|
|
{
|
|
free(titleAddOnTitleID);
|
|
titleAddOnTitleID = NULL;
|
|
}
|
|
|
|
if (titleAddOnVersion != NULL)
|
|
{
|
|
free(titleAddOnVersion);
|
|
titleAddOnVersion = NULL;
|
|
}
|
|
|
|
if (titleAddOnStorageId != NULL)
|
|
{
|
|
free(titleAddOnStorageId);
|
|
titleAddOnStorageId = NULL;
|
|
}
|
|
|
|
sdCardTitleAppCount = 0;
|
|
sdCardTitlePatchCount = 0;
|
|
sdCardTitleAddOnCount = 0;
|
|
|
|
nandUserTitleAppCount = 0;
|
|
nandUserTitlePatchCount = 0;
|
|
nandUserTitleAddOnCount = 0;
|
|
|
|
if (titleName != NULL)
|
|
{
|
|
freeStringsPtr(titleName);
|
|
titleName = NULL;
|
|
}
|
|
|
|
if (fixedTitleName != NULL)
|
|
{
|
|
freeStringsPtr(fixedTitleName);
|
|
fixedTitleName = NULL;
|
|
}
|
|
|
|
if (titleAuthor != NULL)
|
|
{
|
|
freeStringsPtr(titleAuthor);
|
|
titleAuthor = NULL;
|
|
}
|
|
|
|
if (titleIcon != NULL)
|
|
{
|
|
freeStringsPtr((char**)titleIcon);
|
|
titleIcon = NULL;
|
|
}
|
|
}
|
|
|
|
void freeGlobalData()
|
|
{
|
|
freeGameCardInfo();
|
|
|
|
freeTitleInfo();
|
|
|
|
freeExeFsContext();
|
|
|
|
freeRomFsContext();
|
|
|
|
if (romFsBrowserEntries != NULL)
|
|
{
|
|
free(romFsBrowserEntries);
|
|
romFsBrowserEntries = NULL;
|
|
}
|
|
|
|
if (orphanEntries != NULL)
|
|
{
|
|
free(orphanEntries);
|
|
orphanEntries = NULL;
|
|
}
|
|
}
|
|
|
|
bool listTitlesByType(NcmContentMetaDatabase *ncmDb, u8 filter)
|
|
{
|
|
if (!ncmDb || (filter != META_DB_REGULAR_APPLICATION && filter != META_DB_PATCH && filter != META_DB_ADDON))
|
|
{
|
|
uiStatusMsg("listTitlesByType: invalid parameters (0x%02X filter).", filter);
|
|
return false;
|
|
}
|
|
|
|
bool success = false, proceed = true, freeBuf = false;
|
|
|
|
Result result;
|
|
|
|
NcmApplicationContentMetaKey *titleList = NULL;
|
|
NcmApplicationContentMetaKey *titleListTmp = NULL;
|
|
size_t titleListSize = sizeof(NcmApplicationContentMetaKey);
|
|
|
|
u32 written = 0;
|
|
u32 total = 0;
|
|
|
|
u64 *titleIDs = NULL, *tmpTIDs = NULL;
|
|
u32 *versions = NULL, *tmpVersions = NULL;
|
|
|
|
titleList = calloc(1, titleListSize);
|
|
if (titleList)
|
|
{
|
|
if (R_SUCCEEDED(result = ncmContentMetaDatabaseListApplication(ncmDb, filter, titleList, titleListSize, &written, &total)))
|
|
{
|
|
if (written && total)
|
|
{
|
|
if (total > written)
|
|
{
|
|
titleListSize *= total;
|
|
titleListTmp = realloc(titleList, titleListSize);
|
|
if (titleListTmp)
|
|
{
|
|
titleList = titleListTmp;
|
|
memset(titleList, 0, titleListSize);
|
|
|
|
if (R_SUCCEEDED(result = ncmContentMetaDatabaseListApplication(ncmDb, filter, titleList, titleListSize, &written, &total)))
|
|
{
|
|
if (written != total)
|
|
{
|
|
uiStatusMsg("listTitlesByType: title count mismatch in ncmContentMetaDatabaseListApplication (%u != %u) (0x%02X filter).", written, total, filter);
|
|
proceed = false;
|
|
}
|
|
} else {
|
|
uiStatusMsg("listTitlesByType: ncmContentMetaDatabaseListApplication failed! (0x%08X) (0x%02X filter).", result, filter);
|
|
proceed = false;
|
|
}
|
|
} else {
|
|
uiStatusMsg("listTitlesByType: error reallocating output buffer for ncmContentMetaDatabaseListApplication (%u %s) (0x%02X filter).", total, (total == 1 ? "entry" : "entries"), filter);
|
|
proceed = false;
|
|
}
|
|
}
|
|
|
|
if (proceed)
|
|
{
|
|
titleIDs = calloc(total, sizeof(u64));
|
|
versions = calloc(total, sizeof(u32));
|
|
|
|
if (titleIDs != NULL && versions != NULL)
|
|
{
|
|
u32 i;
|
|
for(i = 0; i < total; i++)
|
|
{
|
|
titleIDs[i] = titleList[i].metaRecord.titleId;
|
|
versions[i] = titleList[i].metaRecord.version;
|
|
}
|
|
|
|
if (filter == META_DB_REGULAR_APPLICATION)
|
|
{
|
|
// If ptr == NULL, realloc will essentially act as a malloc
|
|
tmpTIDs = realloc(titleAppTitleID, (titleAppCount + total) * sizeof(u64));
|
|
tmpVersions = realloc(titleAppVersion, (titleAppCount + total) * sizeof(u32));
|
|
|
|
if (tmpTIDs != NULL && tmpVersions != NULL)
|
|
{
|
|
titleAppTitleID = tmpTIDs;
|
|
memcpy(titleAppTitleID + titleAppCount, titleIDs, total * sizeof(u64));
|
|
|
|
titleAppVersion = tmpVersions;
|
|
memcpy(titleAppVersion + titleAppCount, versions, total * sizeof(u32));
|
|
|
|
titleAppCount += total;
|
|
|
|
success = true;
|
|
} else {
|
|
if (tmpTIDs != NULL)
|
|
{
|
|
titleAppTitleID = tmpTIDs;
|
|
free(tmpTIDs);
|
|
}
|
|
|
|
if (tmpVersions != NULL)
|
|
{
|
|
titleAppVersion = tmpVersions;
|
|
free(tmpVersions);
|
|
}
|
|
|
|
freeBuf = true;
|
|
}
|
|
} else
|
|
if (filter == META_DB_PATCH)
|
|
{
|
|
// If ptr == NULL, realloc will essentially act as a malloc
|
|
tmpTIDs = realloc(titlePatchTitleID, (titlePatchCount + total) * sizeof(u64));
|
|
tmpVersions = realloc(titlePatchVersion, (titlePatchCount + total) * sizeof(u32));
|
|
|
|
if (tmpTIDs != NULL && tmpVersions != NULL)
|
|
{
|
|
titlePatchTitleID = tmpTIDs;
|
|
memcpy(titlePatchTitleID + titlePatchCount, titleIDs, total * sizeof(u64));
|
|
|
|
titlePatchVersion = tmpVersions;
|
|
memcpy(titlePatchVersion + titlePatchCount, versions, total * sizeof(u32));
|
|
|
|
titlePatchCount += total;
|
|
|
|
success = true;
|
|
} else {
|
|
if (tmpTIDs != NULL)
|
|
{
|
|
titlePatchTitleID = tmpTIDs;
|
|
free(tmpTIDs);
|
|
}
|
|
|
|
if (tmpVersions != NULL)
|
|
{
|
|
titlePatchVersion = tmpVersions;
|
|
free(tmpVersions);
|
|
}
|
|
|
|
freeBuf = true;
|
|
}
|
|
} else
|
|
if (filter == META_DB_ADDON)
|
|
{
|
|
// If ptr == NULL, realloc will essentially act as a malloc
|
|
tmpTIDs = realloc(titleAddOnTitleID, (titleAddOnCount + total) * sizeof(u64));
|
|
tmpVersions = realloc(titleAddOnVersion, (titleAddOnCount + total) * sizeof(u32));
|
|
|
|
if (tmpTIDs != NULL && tmpVersions != NULL)
|
|
{
|
|
titleAddOnTitleID = tmpTIDs;
|
|
memcpy(titleAddOnTitleID + titleAddOnCount, titleIDs, total * sizeof(u64));
|
|
|
|
titleAddOnVersion = tmpVersions;
|
|
memcpy(titleAddOnVersion + titleAddOnCount, versions, total * sizeof(u32));
|
|
|
|
titleAddOnCount += total;
|
|
|
|
success = true;
|
|
} else {
|
|
if (tmpTIDs != NULL)
|
|
{
|
|
titleAddOnTitleID = tmpTIDs;
|
|
free(tmpTIDs);
|
|
}
|
|
|
|
if (tmpVersions != NULL)
|
|
{
|
|
titleAddOnVersion = tmpVersions;
|
|
free(tmpVersions);
|
|
}
|
|
|
|
freeBuf = true;
|
|
}
|
|
}
|
|
} else {
|
|
freeBuf = true;
|
|
}
|
|
|
|
if (freeBuf)
|
|
{
|
|
if (titleIDs != NULL) free(titleIDs);
|
|
|
|
if (versions != NULL) free(versions);
|
|
|
|
uiStatusMsg("listTitlesByType: failed to allocate memory for TID/version buffer! (0x%02X filter).", filter);
|
|
}
|
|
}
|
|
} else {
|
|
// There are no titles that match the provided filter in the opened storage device
|
|
success = true;
|
|
}
|
|
} else {
|
|
uiStatusMsg("listTitlesByType: ncmContentMetaDatabaseListApplication failed! (0x%08X) (0x%02X filter).", result, filter);
|
|
}
|
|
|
|
free(titleList);
|
|
} else {
|
|
uiStatusMsg("listTitlesByType: unable to allocate memory for the ApplicationContentMetaKey struct (0x%02X filter).", filter);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
bool getTitleIDAndVersionList(FsStorageId curStorageId)
|
|
{
|
|
if (curStorageId != FsStorageId_GameCard && curStorageId != FsStorageId_SdCard && curStorageId != FsStorageId_NandUser)
|
|
{
|
|
uiStatusMsg("getTitleIDAndVersionList: invalid storage ID!");
|
|
return false;
|
|
}
|
|
|
|
bool listApp = false, listPatch = false, listAddOn = false, success = false;
|
|
|
|
Result result;
|
|
NcmContentMetaDatabase ncmDb;
|
|
|
|
u32 i;
|
|
FsStorageId *tmpStorages = NULL;
|
|
u32 curAppCount = titleAppCount, curPatchCount = titlePatchCount, curAddOnCount = titleAddOnCount;
|
|
|
|
if (R_SUCCEEDED(result = ncmOpenContentMetaDatabase(curStorageId, &ncmDb)))
|
|
{
|
|
listApp = listTitlesByType(&ncmDb, META_DB_REGULAR_APPLICATION);
|
|
if (listApp && titleAppCount > curAppCount)
|
|
{
|
|
tmpStorages = realloc(titleAppStorageId, titleAppCount * sizeof(FsStorageId));
|
|
if (tmpStorages)
|
|
{
|
|
titleAppStorageId = tmpStorages;
|
|
|
|
tmpStorages = NULL;
|
|
|
|
for(i = curAppCount; i < titleAppCount; i++) titleAppStorageId[i] = curStorageId;
|
|
} else {
|
|
titleAppCount = curAppCount;
|
|
listApp = false;
|
|
}
|
|
}
|
|
|
|
listPatch = listTitlesByType(&ncmDb, META_DB_PATCH);
|
|
if (listPatch && titlePatchCount > curPatchCount)
|
|
{
|
|
tmpStorages = realloc(titlePatchStorageId, titlePatchCount * sizeof(FsStorageId));
|
|
if (tmpStorages)
|
|
{
|
|
titlePatchStorageId = tmpStorages;
|
|
|
|
tmpStorages = NULL;
|
|
|
|
for(i = curPatchCount; i < titlePatchCount; i++) titlePatchStorageId[i] = curStorageId;
|
|
} else {
|
|
titlePatchCount = curPatchCount;
|
|
listPatch = false;
|
|
}
|
|
}
|
|
|
|
listAddOn = listTitlesByType(&ncmDb, META_DB_ADDON);
|
|
if (listAddOn && titleAddOnCount > curAddOnCount)
|
|
{
|
|
tmpStorages = realloc(titleAddOnStorageId, titleAddOnCount * sizeof(FsStorageId));
|
|
if (tmpStorages)
|
|
{
|
|
titleAddOnStorageId = tmpStorages;
|
|
|
|
tmpStorages = NULL;
|
|
|
|
for(i = curAddOnCount; i < titleAddOnCount; i++) titleAddOnStorageId[i] = curStorageId;
|
|
} else {
|
|
titleAddOnCount = curAddOnCount;
|
|
listAddOn = false;
|
|
}
|
|
}
|
|
|
|
success = (listApp || listPatch || listAddOn);
|
|
|
|
serviceClose(&(ncmDb.s));
|
|
} else {
|
|
uiStatusMsg("getTitleIDAndVersionList: ncmOpenContentMetaDatabase failed! (0x%08X)", result);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
void convertTitleVersionToDecimal(u32 version, char *versionBuf, size_t versionBufSize)
|
|
{
|
|
u8 major = (u8)((version >> 26) & 0x3F);
|
|
u8 middle = (u8)((version >> 20) & 0x3F);
|
|
u8 minor = (u8)((version >> 16) & 0xF);
|
|
u16 build = (u16)version;
|
|
|
|
snprintf(versionBuf, versionBufSize, "%u (%u.%u.%u.%u)", version, major, middle, minor, build);
|
|
}
|
|
|
|
bool getTitleControlNacp(u64 titleID, char *nameBuf, int nameBufSize, char *authorBuf, int authorBufSize, u8 **iconBuf)
|
|
{
|
|
if (!titleID || !nameBuf || !nameBufSize || !authorBuf || !authorBufSize || !iconBuf)
|
|
{
|
|
uiStatusMsg("getTitleControlNacp: invalid parameters to retrieve Control.nacp.");
|
|
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)
|
|
{
|
|
if (R_SUCCEEDED(result = nsGetApplicationControlData(1, titleID, buf, sizeof(NsApplicationControlData), &outsize)))
|
|
{
|
|
if (outsize >= sizeof(buf->nacp))
|
|
{
|
|
if (R_SUCCEEDED(result = nacpGetLanguageEntry(&buf->nacp, &langentry)))
|
|
{
|
|
strncpy(nameBuf, langentry->name, nameBufSize);
|
|
strncpy(authorBuf, langentry->author, authorBufSize);
|
|
getNameAndAuthor = true;
|
|
} else {
|
|
uiStatusMsg("getTitleControlNacp: GetLanguageEntry failed! (0x%08X)", result);
|
|
}
|
|
|
|
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 = (getNameAndAuthor && getIcon);
|
|
} else {
|
|
uiStatusMsg("getTitleControlNacp: Control.nacp buffer size (%u bytes) is too small! Expected: %u bytes", outsize, sizeof(buf->nacp));
|
|
}
|
|
} else {
|
|
uiStatusMsg("getTitleControlNacp: GetApplicationControlData failed! (0x%08X)", result);
|
|
}
|
|
|
|
free(buf);
|
|
} else {
|
|
uiStatusMsg("getTitleControlNacp: Unable to allocate memory for the ns service operations.");
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
void removeIllegalCharacters(char *name)
|
|
{
|
|
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 createOutputDirectories()
|
|
{
|
|
mkdir(OUTPUT_DUMP_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);
|
|
}
|
|
|
|
void strtrim(char *str)
|
|
{
|
|
if (!str || !*str) return;
|
|
|
|
char *start = str;
|
|
char *end = start + strlen(str);
|
|
|
|
while(--end >= start)
|
|
{
|
|
if (!isspace(*end)) break;
|
|
}
|
|
|
|
*(++end) = '\0';
|
|
|
|
while(isspace(*start)) start++;
|
|
|
|
if (start != str) memmove(str, start, end - start + 1);
|
|
}
|
|
|
|
bool getRootHfs0Header()
|
|
{
|
|
u32 magic;
|
|
Result result;
|
|
FsGameCardHandle handle;
|
|
FsStorage gameCardStorage;
|
|
char gamecard_header[GAMECARD_HEADER_SIZE] = {'\0'};
|
|
|
|
hfs0_partition_cnt = 0;
|
|
|
|
workaroundPartitionZeroAccess();
|
|
|
|
if (R_FAILED(result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle)))
|
|
{
|
|
uiStatusMsg("getRootHfs0Header: GetGameCardHandle failed! (0x%08X)", result);
|
|
return false;
|
|
}
|
|
|
|
if (R_FAILED(result = fsOpenGameCardStorage(&gameCardStorage, &handle, 0)))
|
|
{
|
|
uiStatusMsg("getRootHfs0Header: OpenGameCardStorage failed! (0x%08X)", result);
|
|
return false;
|
|
}
|
|
|
|
if (R_FAILED(result = fsStorageRead(&gameCardStorage, 0, gamecard_header, GAMECARD_HEADER_SIZE)))
|
|
{
|
|
uiStatusMsg("getRootHfs0Header: StorageRead failed to read %u-byte chunk from offset 0x%016lX! (0x%08X)", GAMECARD_HEADER_SIZE, 0, result);
|
|
fsStorageClose(&gameCardStorage);
|
|
return false;
|
|
}
|
|
|
|
u8 cardSize = (u8)gamecard_header[GAMECARD_SIZE_ADDR];
|
|
|
|
switch(cardSize)
|
|
{
|
|
case 0xFA: // 1 GiB
|
|
gameCardSize = GAMECARD_SIZE_1GiB;
|
|
break;
|
|
case 0xF8: // 2 GiB
|
|
gameCardSize = GAMECARD_SIZE_2GiB;
|
|
break;
|
|
case 0xF0: // 4 GiB
|
|
gameCardSize = GAMECARD_SIZE_4GiB;
|
|
break;
|
|
case 0xE0: // 8 GiB
|
|
gameCardSize = GAMECARD_SIZE_8GiB;
|
|
break;
|
|
case 0xE1: // 16 GiB
|
|
gameCardSize = GAMECARD_SIZE_16GiB;
|
|
break;
|
|
case 0xE2: // 32 GiB
|
|
gameCardSize = GAMECARD_SIZE_32GiB;
|
|
break;
|
|
default:
|
|
uiStatusMsg("getRootHfs0Header: Invalid gamecard size value: 0x%02X", cardSize);
|
|
fsStorageClose(&gameCardStorage);
|
|
return false;
|
|
}
|
|
|
|
convertSize(gameCardSize, gameCardSizeStr, sizeof(gameCardSizeStr) / sizeof(gameCardSizeStr[0]));
|
|
|
|
memcpy(&trimmedCardSize, gamecard_header + GAMECARD_DATAEND_ADDR, sizeof(u64));
|
|
trimmedCardSize = (GAMECARD_HEADER_SIZE + (trimmedCardSize * MEDIA_UNIT_SIZE));
|
|
convertSize(trimmedCardSize, trimmedCardSizeStr, sizeof(trimmedCardSizeStr) / sizeof(trimmedCardSizeStr[0]));
|
|
|
|
memcpy(&hfs0_offset, gamecard_header + HFS0_OFFSET_ADDR, sizeof(u64));
|
|
memcpy(&hfs0_size, gamecard_header + HFS0_SIZE_ADDR, sizeof(u64));
|
|
|
|
hfs0_header = malloc(hfs0_size);
|
|
if (!hfs0_header)
|
|
{
|
|
uiStatusMsg("getRootHfs0Header: Unable to allocate memory for the root HFS0 header!");
|
|
|
|
gameCardSize = 0;
|
|
memset(gameCardSizeStr, 0, sizeof(gameCardSizeStr));
|
|
|
|
trimmedCardSize = 0;
|
|
memset(trimmedCardSizeStr, 0, sizeof(trimmedCardSizeStr));
|
|
|
|
hfs0_offset = 0;
|
|
hfs0_size = 0;
|
|
|
|
fsStorageClose(&gameCardStorage);
|
|
|
|
return false;
|
|
}
|
|
|
|
if (R_FAILED(result = fsStorageRead(&gameCardStorage, hfs0_offset, hfs0_header, hfs0_size)))
|
|
{
|
|
uiStatusMsg("getRootHfs0Header: StorageRead failed to read %u-byte chunk from offset 0x%016lX! (0x%08X)", hfs0_size, hfs0_offset, result);
|
|
|
|
gameCardSize = 0;
|
|
memset(gameCardSizeStr, 0, sizeof(gameCardSizeStr));
|
|
|
|
trimmedCardSize = 0;
|
|
memset(trimmedCardSizeStr, 0, sizeof(trimmedCardSizeStr));
|
|
|
|
free(hfs0_header);
|
|
hfs0_header = NULL;
|
|
hfs0_offset = 0;
|
|
hfs0_size = 0;
|
|
|
|
fsStorageClose(&gameCardStorage);
|
|
|
|
return false;
|
|
}
|
|
|
|
memcpy(&magic, hfs0_header, sizeof(u32));
|
|
magic = bswap_32(magic);
|
|
if (magic != HFS0_MAGIC)
|
|
{
|
|
uiStatusMsg("getRootHfs0Header: Magic word mismatch! 0x%08X != 0x%08X", magic, HFS0_MAGIC);
|
|
|
|
gameCardSize = 0;
|
|
memset(gameCardSizeStr, 0, sizeof(gameCardSizeStr));
|
|
|
|
trimmedCardSize = 0;
|
|
memset(trimmedCardSizeStr, 0, sizeof(trimmedCardSizeStr));
|
|
|
|
free(hfs0_header);
|
|
hfs0_header = NULL;
|
|
hfs0_offset = 0;
|
|
hfs0_size = 0;
|
|
|
|
fsStorageClose(&gameCardStorage);
|
|
|
|
return false;
|
|
}
|
|
|
|
memcpy(&hfs0_partition_cnt, hfs0_header + HFS0_FILE_COUNT_ADDR, sizeof(u32));
|
|
|
|
fsStorageClose(&gameCardStorage);
|
|
|
|
return true;
|
|
}
|
|
|
|
void getGameCardUpdateInfo()
|
|
{
|
|
Result result;
|
|
FsGameCardHandle handle;
|
|
|
|
gameCardUpdateTitleID = 0;
|
|
gameCardUpdateVersion = 0;
|
|
|
|
if (R_FAILED(result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle)))
|
|
{
|
|
uiStatusMsg("getGameCardUpdateInfo: GetGameCardHandle failed! (0x%08X)", result);
|
|
return;
|
|
}
|
|
|
|
// Get bundled FW version update
|
|
if (R_SUCCEEDED(result = fsDeviceOperatorUpdatePartitionInfo(&fsOperatorInstance, &handle, &gameCardUpdateVersion, &gameCardUpdateTitleID)))
|
|
{
|
|
if (gameCardUpdateTitleID == GAMECARD_UPDATE_TITLEID)
|
|
{
|
|
char decimalVersion[64] = {'\0'};
|
|
convertTitleVersionToDecimal(gameCardUpdateVersion, decimalVersion, sizeof(decimalVersion));
|
|
|
|
switch(gameCardUpdateVersion)
|
|
{
|
|
case SYSUPDATE_100:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "1.0.0 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_200:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "2.0.0 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_210:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "2.1.0 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_220:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "2.2.0 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_230:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "2.3.0 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_300:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "3.0.0 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_301:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "3.0.1 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_302:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "3.0.2 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_400:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "4.0.0 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_401:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "4.0.1 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_410:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "4.1.0 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_500:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "5.0.0 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_501:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "5.0.1 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_502:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "5.0.2 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_510:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "5.1.0 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_600:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "6.0.0 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_601:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "6.0.1 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_610:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "6.1.0 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_620:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "6.2.0 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_700:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "7.0.0 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_701:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "7.0.1 - v%s", decimalVersion);
|
|
break;
|
|
case SYSUPDATE_800:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "8.0.0 - v%s", decimalVersion);
|
|
break;
|
|
default:
|
|
snprintf(gameCardUpdateVersionStr, sizeof(gameCardUpdateVersionStr) / sizeof(gameCardUpdateVersionStr[0]), "UNKNOWN - v%s", decimalVersion);
|
|
break;
|
|
}
|
|
} else {
|
|
uiStatusMsg("getGameCardUpdateInfo: update Title ID mismatch! %016lX != %016lX", gameCardUpdateTitleID, GAMECARD_UPDATE_TITLEID);
|
|
}
|
|
} else {
|
|
uiStatusMsg("getGameCardUpdateInfo: UpdatePartitionInfo failed! (0x%08X)", result);
|
|
}
|
|
}
|
|
|
|
void loadTitleInfo()
|
|
{
|
|
if (menuType == MENUTYPE_MAIN)
|
|
{
|
|
freeGlobalData();
|
|
return;
|
|
}
|
|
|
|
bool proceed = false, freeBuf = false;
|
|
|
|
if (menuType == MENUTYPE_GAMECARD)
|
|
{
|
|
if (gameCardInserted)
|
|
{
|
|
if (hfs0_header != NULL) return;
|
|
|
|
/* Don't access the gamecard immediately to avoid conflicts with the fsp-srv, ncm and ns services */
|
|
uiPleaseWait(GAMECARD_WAIT_TIME);
|
|
|
|
if (!getRootHfs0Header())
|
|
{
|
|
uiPrintHeadline();
|
|
return;
|
|
}
|
|
|
|
getGameCardUpdateInfo();
|
|
|
|
freeTitleInfo();
|
|
|
|
proceed = getTitleIDAndVersionList(FsStorageId_GameCard);
|
|
} else {
|
|
freeGlobalData();
|
|
return;
|
|
}
|
|
} else
|
|
if (menuType == MENUTYPE_SDCARD_EMMC)
|
|
{
|
|
if (titleAppCount || titlePatchCount || titleAddOnCount) return;
|
|
|
|
uiPleaseWait(1);
|
|
|
|
freeTitleInfo();
|
|
|
|
if (getTitleIDAndVersionList(FsStorageId_SdCard))
|
|
{
|
|
sdCardTitleAppCount = titleAppCount;
|
|
sdCardTitlePatchCount = titlePatchCount;
|
|
sdCardTitleAddOnCount = titleAddOnCount;
|
|
|
|
if (getTitleIDAndVersionList(FsStorageId_NandUser))
|
|
{
|
|
nandUserTitleAppCount = (titleAppCount - sdCardTitleAppCount);
|
|
nandUserTitlePatchCount = (titlePatchCount - sdCardTitlePatchCount);
|
|
nandUserTitleAddOnCount = (titleAddOnCount - sdCardTitleAddOnCount);
|
|
|
|
proceed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (proceed && titleAppCount > 0)
|
|
{
|
|
titleName = calloc(titleAppCount + 1, sizeof(char*));
|
|
fixedTitleName = calloc(titleAppCount + 1, sizeof(char*));
|
|
titleAuthor = calloc(titleAppCount + 1, sizeof(char*));
|
|
titleAppVersionStr = calloc(titleAppCount + 1, sizeof(char*));
|
|
titleIcon = calloc(titleAppCount + 1, sizeof(u8*));
|
|
|
|
if (titleName != NULL && fixedTitleName != NULL && titleAuthor != NULL && titleAppVersionStr != NULL && titleIcon != NULL)
|
|
{
|
|
u32 i;
|
|
for(i = 0; i < titleAppCount; i++)
|
|
{
|
|
titleName[i] = calloc(NACP_APPNAME_LEN + 1, sizeof(char));
|
|
fixedTitleName[i] = calloc(NACP_APPNAME_LEN + 1, sizeof(char));
|
|
titleAuthor[i] = calloc(NACP_AUTHOR_LEN + 1, sizeof(char));
|
|
titleAppVersionStr[i] = calloc(VERSION_STR_LEN + 1, sizeof(char));
|
|
|
|
if (titleName[i] != NULL && fixedTitleName[i] != NULL && titleAuthor[i] != NULL && titleAppVersionStr[i] != NULL)
|
|
{
|
|
convertTitleVersionToDecimal(titleAppVersion[i], titleAppVersionStr[i], VERSION_STR_LEN);
|
|
|
|
if (getTitleControlNacp(titleAppTitleID[i], titleName[i], NACP_APPNAME_LEN, titleAuthor[i], NACP_AUTHOR_LEN, &(titleIcon[i])))
|
|
{
|
|
strtrim(titleName[i]);
|
|
|
|
strtrim(titleAuthor[i]);
|
|
|
|
snprintf(fixedTitleName[i], NACP_APPNAME_LEN, titleName[i]);
|
|
removeIllegalCharacters(fixedTitleName[i]);
|
|
} else {
|
|
freeBuf = true;
|
|
break;
|
|
}
|
|
} else {
|
|
uiStatusMsg("loadTitleInfo: error allocating memory for title information (application #%u).", i + 1);
|
|
freeBuf = true;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
uiStatusMsg("loadTitleInfo: error allocating memory for title information.");
|
|
freeBuf = true;
|
|
}
|
|
|
|
if (freeBuf) freeTitleInfo();
|
|
}
|
|
|
|
uiPrintHeadline();
|
|
}
|
|
|
|
bool getHfs0EntryDetails(u8 *hfs0Header, u64 hfs0HeaderOffset, u64 hfs0HeaderSize, u32 num_entries, u32 entry_idx, bool isRoot, u32 partitionIndex, u64 *out_offset, u64 *out_size)
|
|
{
|
|
if (hfs0Header == NULL) return false;
|
|
|
|
if (entry_idx > (num_entries - 1)) return false;
|
|
|
|
if ((HFS0_ENTRY_TABLE_ADDR + (sizeof(hfs0_entry_table) * num_entries)) > hfs0HeaderSize) return false;
|
|
|
|
hfs0_entry_table *entryTable = calloc(num_entries, sizeof(hfs0_entry_table));
|
|
if (!entryTable) return false;
|
|
|
|
memcpy(entryTable, hfs0Header + HFS0_ENTRY_TABLE_ADDR, sizeof(hfs0_entry_table) * num_entries);
|
|
|
|
// Determine the partition index that's going to be used for offset calculation
|
|
// If we're dealing with a root HFS0 header, just use entry_idx
|
|
// Otherwise, partitionIndex must be used, because entry_idx represents the file entry we must look for in the provided HFS0 partition header
|
|
u32 part_idx = (isRoot ? entry_idx : partitionIndex);
|
|
|
|
switch(part_idx)
|
|
{
|
|
case 0: // Update (contained within IStorage instance with partition ID 0)
|
|
case 1: // Normal or Logo (depending on the gamecard type) (contained within IStorage instance with partition ID 0)
|
|
// Root HFS0: the header offset used to calculate the partition offset is relative to the true gamecard image start
|
|
// Partition HFS0: the header offset used to calculate the file offset is also relative to the true gamecard image start (but it was calculated in a previous call to this function)
|
|
*out_offset = (hfs0HeaderOffset + hfs0HeaderSize + entryTable[entry_idx].file_offset);
|
|
break;
|
|
case 2:
|
|
// Check if we're dealing with a type 0x01 gamecard
|
|
if (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT)
|
|
{
|
|
// Secure (contained within IStorage instance with partition ID 1)
|
|
// Root HFS0: the resulting partition offset must be zero, because the secure partition is stored in a different IStorage instance
|
|
// Partition HFS0: the resulting file offset is relative to the start of the IStorage instance. Thus, it isn't necessary to use the header offset as part of the calculation
|
|
*out_offset = (isRoot ? 0 : (hfs0HeaderSize + entryTable[entry_idx].file_offset));
|
|
} else {
|
|
// Normal (contained within IStorage instance with partition ID 0)
|
|
// Root HFS0: the header offset used to calculate the partition offset is relative to the true gamecard image start
|
|
// Partition HFS0: the header offset used to calculate the file offset is also relative to the true gamecard image start (but it was calculated in a previous call to this function)
|
|
*out_offset = (hfs0HeaderOffset + hfs0HeaderSize + entryTable[entry_idx].file_offset);
|
|
}
|
|
break;
|
|
case 3: // Secure (gamecard type 0x02) (contained within IStorage instance with partition ID 1)
|
|
// Root HFS0: the resulting partition offset must be zero, because the secure partition is stored in a different IStorage instance
|
|
// Partition HFS0: the resulting file offset is relative to the start of the IStorage instance. Thus, it isn't necessary to use the header offset as part of the calculation
|
|
*out_offset = (isRoot ? 0 : (hfs0HeaderSize + entryTable[entry_idx].file_offset));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Store the file size for the desired HFS0 entry
|
|
*out_size = entryTable[entry_idx].file_size;
|
|
|
|
free(entryTable);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool getPartitionHfs0Header(u32 partition)
|
|
{
|
|
if (hfs0_header == NULL) return false;
|
|
|
|
if (partitionHfs0Header != NULL)
|
|
{
|
|
free(partitionHfs0Header);
|
|
partitionHfs0Header = NULL;
|
|
partitionHfs0HeaderOffset = 0;
|
|
partitionHfs0HeaderSize = 0;
|
|
partitionHfs0FileCount = 0;
|
|
partitionHfs0StrTableSize = 0;
|
|
}
|
|
|
|
u8 buf[MEDIA_UNIT_SIZE] = {0};
|
|
Result result;
|
|
FsGameCardHandle handle;
|
|
FsStorage gameCardStorage;
|
|
u64 partitionSize = 0;
|
|
u32 magic = 0;
|
|
bool success = false;
|
|
|
|
if (getHfs0EntryDetails(hfs0_header, hfs0_offset, hfs0_size, hfs0_partition_cnt, partition, true, 0, &partitionHfs0HeaderOffset, &partitionSize))
|
|
{
|
|
workaroundPartitionZeroAccess();
|
|
|
|
if (R_SUCCEEDED(result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle)))
|
|
{
|
|
/*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "GetGameCardHandle succeeded: 0x%08X", handle.value);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;*/
|
|
|
|
// Same ugly hack from dumpRawHfs0Partition()
|
|
if (R_SUCCEEDED(result = fsOpenGameCardStorage(&gameCardStorage, &handle, HFS0_TO_ISTORAGE_IDX(hfs0_partition_cnt, partition))))
|
|
{
|
|
/*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "OpenGameCardStorage succeeded: 0x%08X", handle);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;*/
|
|
|
|
// First read MEDIA_UNIT_SIZE bytes
|
|
if (R_SUCCEEDED(result = fsStorageRead(&gameCardStorage, partitionHfs0HeaderOffset, buf, MEDIA_UNIT_SIZE)))
|
|
{
|
|
// Check the HFS0 magic word
|
|
memcpy(&magic, buf, sizeof(u32));
|
|
magic = bswap_32(magic);
|
|
if (magic == HFS0_MAGIC)
|
|
{
|
|
// Calculate the size for the partition HFS0 header
|
|
memcpy(&partitionHfs0FileCount, buf + HFS0_FILE_COUNT_ADDR, sizeof(u32));
|
|
memcpy(&partitionHfs0StrTableSize, buf + HFS0_STR_TABLE_SIZE_ADDR, sizeof(u32));
|
|
partitionHfs0HeaderSize = (HFS0_ENTRY_TABLE_ADDR + (sizeof(hfs0_entry_table) * partitionHfs0FileCount) + partitionHfs0StrTableSize);
|
|
|
|
/*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Partition #%u HFS0 header offset (relative to IStorage instance): 0x%016lX", partition, partitionHfs0HeaderOffset);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;
|
|
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Partition #%u HFS0 header size: %lu bytes", partition, partitionHfs0HeaderSize);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;
|
|
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Partition #%u file count: %u", partition, partitionHfs0FileCount);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;
|
|
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Partition #%u string table size: %u bytes", partition, partitionHfs0StrTableSize);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;
|
|
|
|
uiRefreshDisplay();*/
|
|
|
|
// Round up the partition HFS0 header size to a MEDIA_UNIT_SIZE bytes boundary
|
|
partitionHfs0HeaderSize = round_up(partitionHfs0HeaderSize, MEDIA_UNIT_SIZE);
|
|
|
|
partitionHfs0Header = malloc(partitionHfs0HeaderSize);
|
|
if (partitionHfs0Header)
|
|
{
|
|
// Check if we were dealing with the correct header size all along
|
|
if (partitionHfs0HeaderSize == MEDIA_UNIT_SIZE)
|
|
{
|
|
// Just copy what we already have
|
|
memcpy(partitionHfs0Header, buf, MEDIA_UNIT_SIZE);
|
|
success = true;
|
|
} else {
|
|
// Read the whole HFS0 header
|
|
if (R_SUCCEEDED(result = fsStorageRead(&gameCardStorage, partitionHfs0HeaderOffset, partitionHfs0Header, partitionHfs0HeaderSize)))
|
|
{
|
|
success = true;
|
|
} else {
|
|
free(partitionHfs0Header);
|
|
partitionHfs0Header = NULL;
|
|
partitionHfs0HeaderOffset = 0;
|
|
partitionHfs0HeaderSize = 0;
|
|
partitionHfs0FileCount = 0;
|
|
partitionHfs0StrTableSize = 0;
|
|
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "StorageRead failed (0x%08X) at offset 0x%016lX", result, partitionHfs0HeaderOffset);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
}
|
|
|
|
/*if (success)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Partition #%u HFS0 header successfully retrieved!", partition);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
}*/
|
|
} else {
|
|
partitionHfs0HeaderOffset = 0;
|
|
partitionHfs0HeaderSize = 0;
|
|
partitionHfs0FileCount = 0;
|
|
partitionHfs0StrTableSize = 0;
|
|
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to allocate memory for the HFS0 header from partition #%u!", partition);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
} else {
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Magic word mismatch! 0x%08X != 0x%08X", magic, HFS0_MAGIC);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
} else {
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "StorageRead failed (0x%08X) at offset 0x%016lX", result, partitionHfs0HeaderOffset);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
|
|
fsStorageClose(&gameCardStorage);
|
|
} else {
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "OpenGameCardStorage failed! (0x%08X)", result);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
} else {
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "GetGameCardHandle failed! (0x%08X)", result);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
} else {
|
|
uiDrawString("Error: unable to get partition details from the root HFS0 header!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
bool getHfs0FileList(u32 partition)
|
|
{
|
|
if (!getPartitionHfs0Header(partition))
|
|
{
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (!partitionHfs0Header)
|
|
{
|
|
uiDrawString("HFS0 partition header information unavailable!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (!partitionHfs0FileCount)
|
|
{
|
|
uiDrawString("The selected partition is empty!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (partitionHfs0FileCount > FILENAME_MAX_CNT)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "HFS0 partition contains more than %u files! (%u entries)", FILENAME_MAX_CNT, partitionHfs0FileCount);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
hfs0_entry_table *entryTable = calloc(partitionHfs0FileCount, sizeof(hfs0_entry_table));
|
|
if (!entryTable)
|
|
{
|
|
uiDrawString("Unable to allocate memory for the HFS0 file entries!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
memcpy(entryTable, partitionHfs0Header + HFS0_ENTRY_TABLE_ADDR, sizeof(hfs0_entry_table) * partitionHfs0FileCount);
|
|
|
|
memset(filenameBuffer, 0, FILENAME_BUFFER_SIZE);
|
|
|
|
int i;
|
|
int max_elements = (int)partitionHfs0FileCount;
|
|
char *nextFilename = filenameBuffer;
|
|
|
|
filenamesCount = 0;
|
|
|
|
for(i = 0; i < max_elements; i++)
|
|
{
|
|
u32 filename_offset = (HFS0_ENTRY_TABLE_ADDR + (sizeof(hfs0_entry_table) * partitionHfs0FileCount) + entryTable[i].filename_offset);
|
|
addStringToFilenameBuffer((char*)partitionHfs0Header + filename_offset, &nextFilename);
|
|
}
|
|
|
|
free(entryTable);
|
|
|
|
breaks += 2;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Used to retrieve tik/cert files from the HFS0 Secure partition
|
|
bool getPartitionHfs0FileByName(FsStorage *gameCardStorage, const char *filename, u8 *outBuf, u64 outBufSize)
|
|
{
|
|
if (!partitionHfs0Header || !partitionHfs0FileCount || !partitionHfs0HeaderSize || !gameCardStorage || !filename || !outBuf || !outBufSize)
|
|
{
|
|
uiDrawString("Error: invalid parameters to retrieve file from HFS0 partition!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
return NULL;
|
|
}
|
|
|
|
bool success = false, proceed = true, found = false;
|
|
|
|
u32 i;
|
|
Result result;
|
|
hfs0_entry_table tmp_hfs0_entry;
|
|
|
|
for(i = 0; i < partitionHfs0FileCount; i++)
|
|
{
|
|
memcpy(&tmp_hfs0_entry, partitionHfs0Header + (u64)HFS0_ENTRY_TABLE_ADDR + ((u64)i * sizeof(hfs0_entry_table)), sizeof(hfs0_entry_table));
|
|
|
|
if (!strncasecmp((char*)partitionHfs0Header + (u64)HFS0_ENTRY_TABLE_ADDR + ((u64)partitionHfs0FileCount * sizeof(hfs0_entry_table)) + (u64)tmp_hfs0_entry.filename_offset, filename, strlen(filename)))
|
|
{
|
|
found = true;
|
|
|
|
if (outBufSize > tmp_hfs0_entry.file_size)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: file \"%s\" is smaller than expected! (0x%016lX < 0x%016lX)", filename, tmp_hfs0_entry.file_size, outBufSize);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
if (R_FAILED(result = fsStorageRead(gameCardStorage, partitionHfs0HeaderSize + tmp_hfs0_entry.file_offset, outBuf, outBufSize)))
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unable to read file \"%s\" from the HFS0 partition!", filename);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
proceed = false;
|
|
break;
|
|
}
|
|
|
|
success = true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (proceed && !found)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unable to find file \"%s\" in the HFS0 partition!", filename);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
bool calculateExeFsExtractedDataSize(u64 *out)
|
|
{
|
|
if (!exeFsContext.exefs_header.file_cnt || !exeFsContext.exefs_entries || !out)
|
|
{
|
|
uiDrawString("Error: invalid parameters to calculate extracted data size for the ExeFS section!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
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 calculateRomFsExtractedDataSize(u64 *out)
|
|
{
|
|
if (!romFsContext.romfs_filetable_size || !romFsContext.romfs_file_entries || !out)
|
|
{
|
|
uiDrawString("Error: invalid parameters to calculate extracted data size for the RomFS section!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
return false;
|
|
}
|
|
|
|
u64 offset = 0, totalSize = 0;
|
|
|
|
while(offset < romFsContext.romfs_filetable_size)
|
|
{
|
|
romfs_file *entry = (romfs_file*)((u8*)romFsContext.romfs_file_entries + offset);
|
|
|
|
totalSize += entry->dataSize;
|
|
|
|
offset += round_up(ROMFS_NONAME_FILEENTRY_SIZE + entry->nameLen, 4);
|
|
}
|
|
|
|
*out = totalSize;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool readProgramNcaExeFsOrRomFs(u32 appIndex, bool readRomFs)
|
|
{
|
|
Result result;
|
|
u32 i = 0;
|
|
u32 written = 0;
|
|
u32 total = 0;
|
|
u32 appCount = 0;
|
|
u32 ncmAppIndex = 0;
|
|
u32 appNcaCount = 0;
|
|
u32 partition = 0;
|
|
|
|
FsStorageId curStorageId;
|
|
|
|
NcmContentMetaDatabase ncmDb;
|
|
memset(&ncmDb, 0, sizeof(NcmContentMetaDatabase));
|
|
|
|
NcmContentMetaRecordsHeader contentRecordsHeader;
|
|
memset(&contentRecordsHeader, 0, sizeof(NcmContentMetaRecordsHeader));
|
|
|
|
u64 contentRecordsHeaderReadSize = 0;
|
|
|
|
NcmContentStorage ncmStorage;
|
|
memset(&ncmStorage, 0, sizeof(NcmContentStorage));
|
|
|
|
NcmApplicationContentMetaKey *appList = NULL;
|
|
NcmContentRecord *appContentRecords = NULL;
|
|
size_t appListSize = sizeof(NcmApplicationContentMetaKey);
|
|
|
|
NcmNcaId ncaId;
|
|
char ncaIdStr[33] = {'\0'};
|
|
u8 ncaHeader[NCA_FULL_HEADER_LENGTH] = {0};
|
|
nca_header_t dec_nca_header;
|
|
|
|
u8 decrypted_nca_keys[NCA_KEY_AREA_SIZE];
|
|
|
|
bool success = false, foundProgram = false;
|
|
|
|
if (!titleAppStorageId)
|
|
{
|
|
uiDrawString("Error: title storage ID unavailable!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
curStorageId = titleAppStorageId[appIndex];
|
|
|
|
if (curStorageId != FsStorageId_GameCard && curStorageId != FsStorageId_SdCard && curStorageId != FsStorageId_NandUser)
|
|
{
|
|
uiDrawString("Error: invalid title storage ID!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
switch(curStorageId)
|
|
{
|
|
case FsStorageId_GameCard:
|
|
appCount = titleAppCount;
|
|
ncmAppIndex = appIndex;
|
|
break;
|
|
case FsStorageId_SdCard:
|
|
appCount = sdCardTitleAppCount;
|
|
ncmAppIndex = appIndex;
|
|
break;
|
|
case FsStorageId_NandUser:
|
|
appCount = nandUserTitleAppCount;
|
|
ncmAppIndex = (appIndex - sdCardTitleAppCount);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (!appCount)
|
|
{
|
|
uiDrawString("Error: invalid base application count!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (ncmAppIndex > (appCount - 1))
|
|
{
|
|
uiDrawString("Error: invalid base application index!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
appListSize *= appCount;
|
|
|
|
// If we're dealing with a gamecard, call workaroundPartitionZeroAccess() and read the secure partition header. Otherwise, ncmContentStorageReadContentIdFile() will fail with error 0x00171002
|
|
if (curStorageId == FsStorageId_GameCard)
|
|
{
|
|
partition = (hfs0_partition_cnt - 1); // Select the secure partition
|
|
|
|
workaroundPartitionZeroAccess();
|
|
|
|
if (!getPartitionHfs0Header(partition))
|
|
{
|
|
breaks += 2;
|
|
return false;
|
|
}
|
|
|
|
if (!partitionHfs0FileCount)
|
|
{
|
|
uiDrawString("The Secure HFS0 partition is empty!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
uiDrawString("Looking for the Program NCA...", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
appList = calloc(1, appListSize);
|
|
if (!appList)
|
|
{
|
|
uiDrawString("Error: unable to allocate memory for the ApplicationContentMetaKey struct!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
goto out;
|
|
}
|
|
|
|
if (R_FAILED(result = ncmOpenContentMetaDatabase(curStorageId, &ncmDb)))
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmOpenContentMetaDatabase failed! (0x%08X)", result);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
goto out;
|
|
}
|
|
|
|
if (R_FAILED(result = ncmContentMetaDatabaseListApplication(&ncmDb, META_DB_REGULAR_APPLICATION, appList, appListSize, &written, &total)))
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentMetaDatabaseListApplication failed! (0x%08X)", result);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
goto out;
|
|
}
|
|
|
|
if (!written || !total)
|
|
{
|
|
uiDrawString("Error: ncmContentMetaDatabaseListApplication wrote no entries to output buffer!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
goto out;
|
|
}
|
|
|
|
if (written != total)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: title count mismatch in ncmContentMetaDatabaseListApplication (%u != %u)", written, total);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
goto out;
|
|
}
|
|
|
|
if (R_FAILED(result = ncmContentMetaDatabaseGet(&ncmDb, &(appList[ncmAppIndex].metaRecord), sizeof(NcmContentMetaRecordsHeader), &contentRecordsHeader, &contentRecordsHeaderReadSize)))
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentMetaDatabaseGet failed! (0x%08X)", result);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
goto out;
|
|
}
|
|
|
|
appNcaCount = (u32)(contentRecordsHeader.numContentRecords);
|
|
|
|
appContentRecords = calloc(appNcaCount, sizeof(NcmContentRecord));
|
|
if (!appContentRecords)
|
|
{
|
|
uiDrawString("Error: unable to allocate memory for the ContentRecord struct!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
goto out;
|
|
}
|
|
|
|
if (R_FAILED(result = ncmContentMetaDatabaseListContentInfo(&ncmDb, &(appList[ncmAppIndex].metaRecord), 0, appContentRecords, appNcaCount * sizeof(NcmContentRecord), &written)))
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentMetaDatabaseListContentInfo failed! (0x%08X)", result);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
goto out;
|
|
}
|
|
|
|
if (R_FAILED(result = ncmOpenContentStorage(curStorageId, &ncmStorage)))
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmOpenContentStorage failed! (0x%08X)", result);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
goto out;
|
|
}
|
|
|
|
for(i = 0; i < appNcaCount; i++)
|
|
{
|
|
if (appContentRecords[i].type == NcmContentType_Program)
|
|
{
|
|
memcpy(&ncaId, &(appContentRecords[i].ncaId), sizeof(NcmNcaId));
|
|
convertDataToHexString(appContentRecords[i].ncaId.c, 16, ncaIdStr, 33);
|
|
foundProgram = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!foundProgram)
|
|
{
|
|
uiDrawString("Error: unable to find Program NCA!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
goto out;
|
|
}
|
|
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Found Program NCA: \"%s.nca\".", ncaIdStr);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
uiRefreshDisplay();
|
|
breaks += 2;
|
|
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Retrieving %s...", (!readRomFs ? "ExeFS entries" : "RomFS entry tables"));
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
uiRefreshDisplay();
|
|
breaks++;
|
|
|
|
if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, ncaHeader, NCA_FULL_HEADER_LENGTH)))
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to read header from Program NCA! (0x%08X)", result);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
goto out;
|
|
}
|
|
|
|
// Decrypt the NCA header
|
|
if (!decryptNcaHeader(ncaHeader, NCA_FULL_HEADER_LENGTH, &dec_nca_header, NULL, decrypted_nca_keys, (curStorageId != FsStorageId_GameCard))) goto out;
|
|
|
|
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 (curStorageId == FsStorageId_GameCard)
|
|
{
|
|
if (has_rights_id)
|
|
{
|
|
uiDrawString("Error: Rights ID field in Program NCA header not empty!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
if (!readRomFs)
|
|
{
|
|
// Read file entries from the ExeFS section
|
|
if (!readExeFsEntryFromNca(&ncmStorage, &ncaId, &dec_nca_header, decrypted_nca_keys)) goto out;
|
|
} else {
|
|
// Read directory and file tables from the RomFS section
|
|
if (!readRomFsEntryFromNca(&ncmStorage, &ncaId, &dec_nca_header, decrypted_nca_keys)) goto out;
|
|
}
|
|
|
|
success = true;
|
|
|
|
out:
|
|
if (appContentRecords) free(appContentRecords);
|
|
|
|
if (!success) serviceClose(&(ncmStorage.s));
|
|
|
|
serviceClose(&(ncmDb.s));
|
|
|
|
if (appList) free(appList);
|
|
|
|
if (curStorageId == FsStorageId_GameCard)
|
|
{
|
|
if (partitionHfs0Header)
|
|
{
|
|
free(partitionHfs0Header);
|
|
partitionHfs0Header = NULL;
|
|
partitionHfs0HeaderOffset = 0;
|
|
partitionHfs0HeaderSize = 0;
|
|
partitionHfs0FileCount = 0;
|
|
partitionHfs0StrTableSize = 0;
|
|
}
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
bool getExeFsFileList()
|
|
{
|
|
if (!exeFsContext.exefs_header.file_cnt || !exeFsContext.exefs_entries || !exeFsContext.exefs_str_table)
|
|
{
|
|
uiDrawString("Error: invalid parameters to retrieve ExeFS section filelist!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
return false;
|
|
}
|
|
|
|
if (exeFsContext.exefs_header.file_cnt > FILENAME_MAX_CNT)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "ExeFS section contains more than %u entries! (%u entries)", FILENAME_MAX_CNT, exeFsContext.exefs_header.file_cnt);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
return false;
|
|
}
|
|
|
|
u32 i;
|
|
|
|
memset(filenameBuffer, 0, FILENAME_BUFFER_SIZE);
|
|
filenamesCount = 0;
|
|
|
|
char *nextFilename = filenameBuffer;
|
|
|
|
for(i = 0; i < exeFsContext.exefs_header.file_cnt; i++)
|
|
{
|
|
char *cur_filename = (exeFsContext.exefs_str_table + exeFsContext.exefs_entries[i].filename_offset);
|
|
addStringToFilenameBuffer(cur_filename, &nextFilename);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool getRomFsParentDir(u32 dir_offset, u32 *out)
|
|
{
|
|
if (!romFsContext.romfs_dirtable_size || dir_offset > romFsContext.romfs_dirtable_size || !romFsContext.romfs_dir_entries)
|
|
{
|
|
uiDrawString("Error: invalid parameters to retrieve parent RomFS section directory!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
return false;
|
|
}
|
|
|
|
romfs_dir *entry = (romfs_dir*)((u8*)romFsContext.romfs_dir_entries + dir_offset);
|
|
|
|
*out = entry->parent;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool generateCurrentRomFsPath(u32 dir_offset)
|
|
{
|
|
if (!romFsContext.romfs_dirtable_size || dir_offset > romFsContext.romfs_dirtable_size || !romFsContext.romfs_dir_entries)
|
|
{
|
|
uiDrawString("Error: invalid parameters to generate current RomFS section path!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
return false;
|
|
}
|
|
|
|
// Generate current path if we're not dealing with the root directory
|
|
if (dir_offset)
|
|
{
|
|
romfs_dir *entry = (romfs_dir*)((u8*)romFsContext.romfs_dir_entries + dir_offset);
|
|
|
|
if (!entry->nameLen)
|
|
{
|
|
uiDrawString("Error: directory entry without name in RomFS section!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
return false;
|
|
}
|
|
|
|
// Check if we're not a root dir child
|
|
if (entry->parent)
|
|
{
|
|
if (!generateCurrentRomFsPath(entry->parent)) return false;
|
|
}
|
|
|
|
// Concatenate entry name
|
|
strcat(curRomFsPath, "/");
|
|
strncat(curRomFsPath, (char*)entry->name, entry->nameLen);
|
|
} else {
|
|
strcat(curRomFsPath, "/");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool getRomFsFileList(u32 dir_offset)
|
|
{
|
|
u64 entryOffset = 0;
|
|
u32 dirEntryCnt = 1; // Always add the parent directory entry ("..")
|
|
u32 fileEntryCnt = 0;
|
|
u32 totalEntryCnt = 0;
|
|
u32 i = 1;
|
|
u32 romFsParentDir = 0;
|
|
|
|
if (romFsBrowserEntries != NULL)
|
|
{
|
|
free(romFsBrowserEntries);
|
|
romFsBrowserEntries = NULL;
|
|
}
|
|
|
|
memset(curRomFsPath, 0, NAME_BUF_LEN);
|
|
|
|
if (!romFsContext.romfs_dirtable_size || dir_offset > romFsContext.romfs_dirtable_size || !romFsContext.romfs_dir_entries || !romFsContext.romfs_filetable_size || !romFsContext.romfs_file_entries)
|
|
{
|
|
uiDrawString("Error: invalid parameters to retrieve RomFS section filelist!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
return false;
|
|
}
|
|
|
|
if (!getRomFsParentDir(dir_offset, &romFsParentDir)) return false;
|
|
|
|
if (!generateCurrentRomFsPath(dir_offset)) return false;
|
|
|
|
// First count the directory entries
|
|
entryOffset = ROMFS_NONAME_DIRENTRY_SIZE; // Always skip the first entry (root directory)
|
|
|
|
while(entryOffset < romFsContext.romfs_dirtable_size)
|
|
{
|
|
romfs_dir *entry = (romfs_dir*)((u8*)romFsContext.romfs_dir_entries + entryOffset);
|
|
|
|
if (!entry->nameLen)
|
|
{
|
|
uiDrawString("Error: directory entry without name in RomFS section!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
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 < romFsContext.romfs_filetable_size)
|
|
{
|
|
romfs_file *entry = (romfs_file*)((u8*)romFsContext.romfs_file_entries + entryOffset);
|
|
|
|
if (!entry->nameLen)
|
|
{
|
|
uiDrawString("Error: file entry without name in RomFS section!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
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);
|
|
|
|
if (totalEntryCnt > FILENAME_MAX_CNT)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Current RomFS dir contains more than %u entries! (%u entries)", FILENAME_MAX_CNT, totalEntryCnt);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
return false;
|
|
}
|
|
|
|
// Allocate memory for our entries
|
|
romFsBrowserEntries = calloc(totalEntryCnt, sizeof(romfs_browser_entry));
|
|
if (!romFsBrowserEntries)
|
|
{
|
|
uiDrawString("Error: unable to allocate memory for file/dir attributes in RomFS section!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
return false;
|
|
}
|
|
|
|
memset(filenameBuffer, 0, FILENAME_BUFFER_SIZE);
|
|
filenamesCount = 0;
|
|
|
|
char *nextFilename = filenameBuffer;
|
|
|
|
char curName[NAME_BUF_LEN] = {'\0'};
|
|
|
|
// Add parent directory entry ("..")
|
|
romFsBrowserEntries[0].type = ROMFS_ENTRY_DIR;
|
|
romFsBrowserEntries[0].offset = romFsParentDir;
|
|
addStringToFilenameBuffer("..", &nextFilename);
|
|
|
|
// First add the directory entries
|
|
if (dirEntryCnt > 1)
|
|
{
|
|
entryOffset = ROMFS_NONAME_DIRENTRY_SIZE; // Always skip the first entry (root directory)
|
|
|
|
while(entryOffset < romFsContext.romfs_dirtable_size)
|
|
{
|
|
romfs_dir *entry = (romfs_dir*)((u8*)romFsContext.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
|
|
u32 strWidth = uiGetStrWidth(curName);
|
|
|
|
if ((BROWSER_ICON_DIMENSION + 16 + strWidth) >= (FB_WIDTH - (font_height * 5)))
|
|
{
|
|
while((BROWSER_ICON_DIMENSION + 16 + strWidth) >= (FB_WIDTH - (font_height * 5)))
|
|
{
|
|
curName[strlen(curName) - 1] = '\0';
|
|
strWidth = uiGetStrWidth(curName);
|
|
}
|
|
|
|
strcat(curName, "...");
|
|
}
|
|
|
|
addStringToFilenameBuffer(curName, &nextFilename);
|
|
|
|
i++;
|
|
}
|
|
|
|
entryOffset += round_up(ROMFS_NONAME_DIRENTRY_SIZE + entry->nameLen, 4);
|
|
}
|
|
}
|
|
|
|
// Now add the file entries
|
|
if (fileEntryCnt > 0)
|
|
{
|
|
entryOffset = 0;
|
|
|
|
while(entryOffset < romFsContext.romfs_filetable_size)
|
|
{
|
|
romfs_file *entry = (romfs_file*)((u8*)romFsContext.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;
|
|
|
|
snprintf(curName, entry->nameLen + 1, (char*)entry->name);
|
|
|
|
// Fix entry name length
|
|
u32 strWidth = uiGetStrWidth(curName);
|
|
|
|
if ((BROWSER_ICON_DIMENSION + 16 + strWidth) >= (FB_WIDTH - (font_height * 5)))
|
|
{
|
|
while((BROWSER_ICON_DIMENSION + 16 + strWidth) >= (FB_WIDTH - (font_height * 5)))
|
|
{
|
|
curName[strlen(curName) - 1] = '\0';
|
|
strWidth = uiGetStrWidth(curName);
|
|
}
|
|
|
|
strcat(curName, "...");
|
|
}
|
|
|
|
addStringToFilenameBuffer(curName, &nextFilename);
|
|
|
|
i++;
|
|
}
|
|
|
|
entryOffset += round_up(ROMFS_NONAME_FILEENTRY_SIZE + entry->nameLen, 4);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int getSdCardFreeSpace(u64 *out)
|
|
{
|
|
Result result;
|
|
FsFileSystem *sdfs = NULL;
|
|
u64 size = 0;
|
|
int rc = 0;
|
|
|
|
sdfs = fsdevGetDefaultFileSystem();
|
|
if (!sdfs)
|
|
{
|
|
uiStatusMsg("getSdCardFreeSpace: fsdevGetDefaultFileSystem failed!");
|
|
return rc;
|
|
}
|
|
|
|
if (R_SUCCEEDED(result = fsFsGetFreeSpace(sdfs, "/", &size)))
|
|
{
|
|
*out = size;
|
|
rc = 1;
|
|
} else {
|
|
uiStatusMsg("getSdCardFreeSpace: fsFsGetFreeSpace failed! (0x%08X)", result);
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
void convertSize(u64 size, char *out, int bufsize)
|
|
{
|
|
char buffer[16];
|
|
double bytes = (double)size;
|
|
|
|
if (bytes < 1000.0)
|
|
{
|
|
snprintf(buffer, sizeof(buffer), "%.0lf B", bytes);
|
|
} else
|
|
if (bytes < 10.0*KiB)
|
|
{
|
|
snprintf(buffer, sizeof(buffer), "%.2lf KiB", floor((bytes*100.0)/KiB)/100.0);
|
|
} else
|
|
if (bytes < 100.0*KiB)
|
|
{
|
|
snprintf(buffer, sizeof(buffer), "%.1lf KiB", floor((bytes*10.0)/KiB)/10.0);
|
|
} else
|
|
if (bytes < 1000.0*KiB)
|
|
{
|
|
snprintf(buffer, sizeof(buffer), "%.0lf KiB", floor(bytes/KiB));
|
|
} else
|
|
if (bytes < 10.0*MiB)
|
|
{
|
|
snprintf(buffer, sizeof(buffer), "%.2lf MiB", floor((bytes*100.0)/MiB)/100.0);
|
|
} else
|
|
if (bytes < 100.0*MiB)
|
|
{
|
|
snprintf(buffer, sizeof(buffer), "%.1lf MiB", floor((bytes*10.0)/MiB)/10.0);
|
|
} else
|
|
if (bytes < 1000.0*MiB)
|
|
{
|
|
snprintf(buffer, sizeof(buffer), "%.0lf MiB", floor(bytes/MiB));
|
|
} else
|
|
if (bytes < 10.0*GiB)
|
|
{
|
|
snprintf(buffer, sizeof(buffer), "%.2lf GiB", floor((bytes*100.0)/GiB)/100.0);
|
|
} else
|
|
if (bytes < 100.0*GiB)
|
|
{
|
|
snprintf(buffer, sizeof(buffer), "%.1lf GiB", floor((bytes*10.0)/GiB)/10.0);
|
|
} else {
|
|
snprintf(buffer, sizeof(buffer), "%.0lf GiB", floor(bytes/GiB));
|
|
}
|
|
|
|
snprintf(out, bufsize, "%s", buffer);
|
|
}
|
|
|
|
void addStringToFilenameBuffer(const char *string, char **nextFilename)
|
|
{
|
|
filenames[filenamesCount++] = *nextFilename;
|
|
snprintf(*nextFilename, FILENAME_LENGTH, string);
|
|
*nextFilename += FILENAME_LENGTH;
|
|
}
|
|
|
|
char *generateDumpFullName()
|
|
{
|
|
if (!titleAppCount || !fixedTitleName || !titleAppTitleID || !titleAppVersion) return NULL;
|
|
|
|
u32 i, j;
|
|
|
|
char tmp[512] = {'\0'};
|
|
char *fullname = NULL;
|
|
char *fullnameTmp = NULL;
|
|
|
|
size_t strsize = NAME_BUF_LEN;
|
|
fullname = calloc(strsize, sizeof(char));
|
|
if (!fullname) return NULL;
|
|
|
|
for(i = 0; i < titleAppCount; i++)
|
|
{
|
|
u32 highestVersion = titleAppVersion[i];
|
|
|
|
// Check if our current gamecard has any bundled updates for this application. If so, use the highest update version available
|
|
if (titlePatchCount > 0 && titlePatchTitleID != NULL && titlePatchVersion != NULL)
|
|
{
|
|
for(j = 0; j < titlePatchCount; j++)
|
|
{
|
|
if (titlePatchTitleID[j] == (titleAppTitleID[i] | APPLICATION_PATCH_BITMASK) && titlePatchVersion[j] > highestVersion) highestVersion = titlePatchVersion[j];
|
|
}
|
|
}
|
|
|
|
snprintf(tmp, sizeof(tmp) / sizeof(tmp[0]), "%s v%u (%016lX)", fixedTitleName[i], highestVersion, titleAppTitleID[i]);
|
|
|
|
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;
|
|
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)
|
|
{
|
|
if (!titleAppCount || !fixedTitleName || !titleAppTitleID || (selectedNspDumpType == DUMP_APP_NSP && !titleAppVersion) || (selectedNspDumpType == DUMP_PATCH_NSP && (!titlePatchCount || !titlePatchTitleID || !titlePatchVersion)) || (selectedNspDumpType == DUMP_ADDON_NSP && (!titleAddOnCount || !titleAddOnTitleID || !titleAddOnVersion))) return NULL;
|
|
|
|
u32 app;
|
|
bool foundApp = false;
|
|
|
|
size_t strsize = NAME_BUF_LEN;
|
|
char *fullname = calloc(strsize, sizeof(char));
|
|
if (!fullname) return NULL;
|
|
|
|
if (selectedNspDumpType == DUMP_APP_NSP)
|
|
{
|
|
snprintf(fullname, strsize, "%s v%u (%016lX) (BASE)", fixedTitleName[titleIndex], titleAppVersion[titleIndex], titleAppTitleID[titleIndex]);
|
|
} else
|
|
if (selectedNspDumpType == DUMP_PATCH_NSP)
|
|
{
|
|
for(app = 0; app < titleAppCount; app++)
|
|
{
|
|
if (titlePatchTitleID[titleIndex] == (titleAppTitleID[app] | APPLICATION_PATCH_BITMASK))
|
|
{
|
|
foundApp = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (foundApp)
|
|
{
|
|
snprintf(fullname, strsize, "%s v%u (%016lX) (UPD)", fixedTitleName[app], titlePatchVersion[titleIndex], titlePatchTitleID[titleIndex]);
|
|
} else {
|
|
snprintf(fullname, strsize, "%016lX v%u (UPD)", titlePatchTitleID[titleIndex], titlePatchVersion[titleIndex]);
|
|
}
|
|
} else
|
|
if (selectedNspDumpType == DUMP_ADDON_NSP)
|
|
{
|
|
for(app = 0; app < titleAppCount; app++)
|
|
{
|
|
if ((titleAddOnTitleID[titleIndex] & APPLICATION_ADDON_BITMASK) == (titleAppTitleID[app] & APPLICATION_ADDON_BITMASK))
|
|
{
|
|
foundApp = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (foundApp)
|
|
{
|
|
snprintf(fullname, strsize, "%s v%u (%016lX) (DLC)", fixedTitleName[app], titleAddOnVersion[titleIndex], titleAddOnTitleID[titleIndex]);
|
|
} else {
|
|
snprintf(fullname, strsize, "%016lX v%u (DLC)", titleAddOnTitleID[titleIndex], titleAddOnVersion[titleIndex]);
|
|
}
|
|
} else {
|
|
free(fullname);
|
|
fullname = NULL;
|
|
}
|
|
|
|
return fullname;
|
|
}
|
|
|
|
void retrieveDescriptionForPatchOrAddOn(u64 titleID, u32 version, bool addOn, bool addAppName, const char *prefix, char *outBuf, size_t outBufSize)
|
|
{
|
|
if (!outBuf || !outBufSize) return;
|
|
|
|
char versionStr[128] = {'\0'};
|
|
convertTitleVersionToDecimal(version, versionStr, sizeof(versionStr));
|
|
|
|
if (!titleAppCount || !titleAppTitleID || !titleName || !*titleName || !addAppName)
|
|
{
|
|
if (prefix)
|
|
{
|
|
snprintf(outBuf, outBufSize, "%s%016lX v%s", prefix, titleID, versionStr);
|
|
} else {
|
|
snprintf(outBuf, outBufSize, "%016lX v%s", titleID, versionStr);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
u32 app;
|
|
bool foundApp = false;
|
|
|
|
for(app = 0; app < titleAppCount; app++)
|
|
{
|
|
if ((!addOn && titleID == (titleAppTitleID[app] | APPLICATION_PATCH_BITMASK)) || (addOn && (titleID & APPLICATION_ADDON_BITMASK) == (titleAppTitleID[app] & APPLICATION_ADDON_BITMASK)))
|
|
{
|
|
foundApp = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (foundApp)
|
|
{
|
|
if (prefix)
|
|
{
|
|
snprintf(outBuf, outBufSize, "%s%s | %016lX v%s", prefix, titleName[app], titleID, versionStr);
|
|
} else {
|
|
snprintf(outBuf, outBufSize, "%s | %016lX v%s", titleName[app], titleID, versionStr);
|
|
}
|
|
} else {
|
|
if (prefix)
|
|
{
|
|
snprintf(outBuf, outBufSize, "%s%016lX v%s", prefix, titleID, versionStr);
|
|
} else {
|
|
snprintf(outBuf, outBufSize, "%016lX v%s", titleID, versionStr);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool checkOrphanPatchOrAddOn(bool addOn)
|
|
{
|
|
if (!titleAppCount || !titleAppTitleID || (!addOn && (!titlePatchCount || !titlePatchTitleID)) || (addOn && (!titleAddOnCount || !titleAddOnTitleID))) return false;
|
|
|
|
u32 i, j;
|
|
u32 count = (!addOn ? titlePatchCount : titleAddOnCount);
|
|
|
|
for(i = 0; i < count; i++)
|
|
{
|
|
bool foundMatch = false;
|
|
|
|
for(j = 0; j < titleAppCount; j++)
|
|
{
|
|
if ((!addOn && titlePatchTitleID[i] == (titleAppTitleID[j] | APPLICATION_PATCH_BITMASK)) || (addOn && (titleAddOnTitleID[i] & APPLICATION_ADDON_BITMASK) == (titleAppTitleID[j] & APPLICATION_ADDON_BITMASK)))
|
|
{
|
|
foundMatch = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!foundMatch) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void generateOrphanPatchOrAddOnList()
|
|
{
|
|
u32 i, j;
|
|
char *nextFilename = filenameBuffer;
|
|
filenamesCount = 0;
|
|
|
|
bool foundMatch;
|
|
char versionStr[128] = {'\0'};
|
|
u32 orphanEntryIndex = 0, patchCount = 0, addOnCount = 0;
|
|
orphan_patch_addon_entry *orphanPatchEntries = NULL, *orphanAddOnEntries = NULL;
|
|
|
|
if (orphanEntries != NULL)
|
|
{
|
|
free(orphanEntries);
|
|
orphanEntries = NULL;
|
|
}
|
|
|
|
if ((!titlePatchCount || !titlePatchTitleID || !titlePatchVersion) && (!titleAddOnCount || !titleAddOnTitleID || !titleAddOnVersion)) return;
|
|
|
|
if (titlePatchCount)
|
|
{
|
|
orphanPatchEntries = calloc(titlePatchCount, sizeof(orphan_patch_addon_entry));
|
|
if (!orphanPatchEntries) return;
|
|
|
|
for(i = 0; i < titlePatchCount; i++)
|
|
{
|
|
foundMatch = false;
|
|
|
|
if (titleAppCount && titleAppTitleID)
|
|
{
|
|
for(j = 0; j < titleAppCount; j++)
|
|
{
|
|
if (titlePatchTitleID[i] == (titleAppTitleID[j] | APPLICATION_PATCH_BITMASK))
|
|
{
|
|
foundMatch = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!foundMatch)
|
|
{
|
|
convertTitleVersionToDecimal(titlePatchVersion[i], versionStr, sizeof(versionStr));
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%016lX v%s (Update)", titlePatchTitleID[i], versionStr);
|
|
addStringToFilenameBuffer(strbuf, &nextFilename);
|
|
|
|
orphanPatchEntries[orphanEntryIndex].index = i;
|
|
orphanPatchEntries[orphanEntryIndex].type = ORPHAN_ENTRY_TYPE_PATCH;
|
|
|
|
orphanEntryIndex++;
|
|
patchCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (titleAddOnCount)
|
|
{
|
|
orphanAddOnEntries = calloc(titleAddOnCount, sizeof(orphan_patch_addon_entry));
|
|
if (!orphanAddOnEntries)
|
|
{
|
|
if (orphanPatchEntries) free(orphanPatchEntries);
|
|
return;
|
|
}
|
|
|
|
orphanEntryIndex = 0;
|
|
|
|
for(i = 0; i < titleAddOnCount; i++)
|
|
{
|
|
foundMatch = false;
|
|
|
|
if (titleAppCount && titleAppTitleID)
|
|
{
|
|
for(j = 0; j < titleAppCount; j++)
|
|
{
|
|
if ((titleAddOnTitleID[i] & APPLICATION_ADDON_BITMASK) == (titleAppTitleID[j] & APPLICATION_ADDON_BITMASK))
|
|
{
|
|
foundMatch = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!foundMatch)
|
|
{
|
|
convertTitleVersionToDecimal(titleAddOnVersion[i], versionStr, sizeof(versionStr));
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%016lX v%s (DLC)", titleAddOnTitleID[i], versionStr);
|
|
addStringToFilenameBuffer(strbuf, &nextFilename);
|
|
|
|
orphanAddOnEntries[orphanEntryIndex].index = i;
|
|
orphanAddOnEntries[orphanEntryIndex].type = ORPHAN_ENTRY_TYPE_ADDON;
|
|
|
|
orphanEntryIndex++;
|
|
addOnCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
filenamesCount = (patchCount + addOnCount);
|
|
|
|
orphanEntries = calloc(filenamesCount, sizeof(orphan_patch_addon_entry));
|
|
if (!orphanEntries)
|
|
{
|
|
filenamesCount = 0;
|
|
if (orphanPatchEntries) free(orphanPatchEntries);
|
|
if (orphanAddOnEntries) free(orphanAddOnEntries);
|
|
return;
|
|
}
|
|
|
|
if (titlePatchCount)
|
|
{
|
|
if (patchCount) memcpy(orphanEntries, orphanPatchEntries, patchCount * sizeof(orphan_patch_addon_entry));
|
|
|
|
free(orphanPatchEntries);
|
|
}
|
|
|
|
if (titleAddOnCount)
|
|
{
|
|
if (addOnCount) memcpy(orphanEntries + patchCount, orphanAddOnEntries, addOnCount * sizeof(orphan_patch_addon_entry));
|
|
|
|
free(orphanAddOnEntries);
|
|
}
|
|
}
|
|
|
|
bool checkIfBaseApplicationHasPatchOrAddOn(u32 appIndex, bool addOn)
|
|
{
|
|
if (!titleAppCount || appIndex > (titleAppCount - 1) || !titleAppTitleID || (!addOn && (!titlePatchCount || !titlePatchTitleID)) || (addOn && (!titleAddOnCount || !titleAddOnTitleID))) return false;
|
|
|
|
u32 i;
|
|
u32 count = (!addOn ? titlePatchCount : titleAddOnCount);
|
|
|
|
for(i = 0; i < count; i++)
|
|
{
|
|
if ((!addOn && (titleAppTitleID[appIndex] | APPLICATION_PATCH_BITMASK) == titlePatchTitleID[i]) || (addOn && (titleAppTitleID[appIndex] & APPLICATION_ADDON_BITMASK) == (titleAddOnTitleID[i] & APPLICATION_ADDON_BITMASK))) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool checkIfPatchOrAddOnBelongsToBaseApplication(u32 titleIndex, u32 appIndex, bool addOn)
|
|
{
|
|
if (!titleAppCount || appIndex > (titleAppCount - 1) || !titleAppTitleID || (!addOn && (!titlePatchCount || titleIndex > (titlePatchCount - 1) || !titlePatchTitleID)) || (addOn && (!titleAddOnCount || titleIndex > (titleAddOnCount - 1) || !titleAddOnTitleID))) return false;
|
|
|
|
if ((!addOn && titlePatchTitleID[titleIndex] == (titleAppTitleID[appIndex] | APPLICATION_PATCH_BITMASK)) || (addOn && (titleAddOnTitleID[titleIndex] & APPLICATION_ADDON_BITMASK) == (titleAppTitleID[appIndex] & APPLICATION_ADDON_BITMASK))) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
u32 retrieveFirstPatchOrAddOnIndexFromBaseApplication(u32 appIndex, bool addOn)
|
|
{
|
|
if (!titleAppCount || appIndex > (titleAppCount - 1) || !titleAppTitleID || (!addOn && (!titlePatchCount || !titlePatchTitleID)) || (addOn && (!titleAddOnCount || !titleAddOnTitleID))) 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 (!titleAppCount || appIndex > (titleAppCount - 1) || !titleAppTitleID || !startTitleIndex || startTitleIndex >= count || (!addOn && (!titlePatchCount || !titlePatchTitleID)) || (addOn && (!titleAddOnCount || !titleAddOnTitleID))) return retTitleIndex;
|
|
|
|
for(curTitleIndex = startTitleIndex; curTitleIndex > 0; curTitleIndex--)
|
|
{
|
|
if (checkIfPatchOrAddOnBelongsToBaseApplication((curTitleIndex - 1), appIndex, addOn))
|
|
{
|
|
retTitleIndex = (curTitleIndex - 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return retTitleIndex;
|
|
}
|
|
|
|
u32 retrieveNextPatchOrAddOnIndexFromBaseApplication(u32 startTitleIndex, u32 appIndex, bool addOn)
|
|
{
|
|
u32 count = (!addOn ? titlePatchCount : titleAddOnCount);
|
|
u32 retTitleIndex = startTitleIndex;
|
|
u32 curTitleIndex = 0;
|
|
|
|
if (!titleAppCount || appIndex > (titleAppCount - 1) || !titleAppTitleID || startTitleIndex >= count || (!addOn && (!titlePatchCount || !titlePatchTitleID)) || (addOn && (!titleAddOnCount || !titleAddOnTitleID))) return retTitleIndex;
|
|
|
|
for(curTitleIndex = (startTitleIndex + 1); curTitleIndex < count; curTitleIndex++)
|
|
{
|
|
if (checkIfPatchOrAddOnBelongsToBaseApplication(curTitleIndex, appIndex, addOn))
|
|
{
|
|
retTitleIndex = curTitleIndex;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return retTitleIndex;
|
|
}
|
|
|
|
void waitForButtonPress()
|
|
{
|
|
uiDrawString("Press any button to continue", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
uiRefreshDisplay();
|
|
|
|
while(true)
|
|
{
|
|
hidScanInput();
|
|
|
|
u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO);
|
|
|
|
if (keysDown && !((keysDown & KEY_TOUCH) || (keysDown & KEY_LSTICK_LEFT) || (keysDown & KEY_LSTICK_RIGHT) || (keysDown & KEY_LSTICK_UP) || (keysDown & KEY_LSTICK_DOWN) || \
|
|
(keysDown & KEY_RSTICK_LEFT) || (keysDown & KEY_RSTICK_RIGHT) || (keysDown & KEY_RSTICK_UP) || (keysDown & KEY_RSTICK_DOWN))) break;
|
|
}
|
|
}
|
|
|
|
void printProgressBar(progress_ctx_t *progressCtx, bool calcData, u64 chunkSize)
|
|
{
|
|
if (!progressCtx) return;
|
|
|
|
if (calcData)
|
|
{
|
|
timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx->now));
|
|
|
|
progressCtx->lastSpeed = (((double)(progressCtx->curOffset + chunkSize) / (double)DUMP_BUFFER_SIZE) / (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)DUMP_BUFFER_SIZE) / progressCtx->averageSpeed);
|
|
|
|
progressCtx->progress = (u8)(((progressCtx->curOffset + chunkSize) * 100) / progressCtx->totalSize);
|
|
}
|
|
|
|
formatETAString(progressCtx->remainingTime, progressCtx->etaInfo, sizeof(progressCtx->etaInfo) / sizeof(progressCtx->etaInfo[0]));
|
|
|
|
convertSize(progressCtx->curOffset + chunkSize, progressCtx->curOffsetStr, sizeof(progressCtx->curOffsetStr) / sizeof(progressCtx->curOffsetStr[0]));
|
|
|
|
uiFill(0, (progressCtx->line_offset * (font_height + (font_height / 4))) + 8, FB_WIDTH / 4, (font_height + (font_height / 4)), BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB);
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%.2lf MiB/s [ETA: %s]", progressCtx->averageSpeed, progressCtx->etaInfo);
|
|
uiDrawString(strbuf, font_height * 2, (progressCtx->line_offset * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
|
|
uiFill(FB_WIDTH / 4, (progressCtx->line_offset * (font_height + (font_height / 4))) + 10, FB_WIDTH / 2, (font_height + (font_height / 4)), 0, 0, 0);
|
|
uiFill(FB_WIDTH / 4, (progressCtx->line_offset * (font_height + (font_height / 4))) + 10, (((u32)progressCtx->progress * ((u32)FB_WIDTH / 2)) / 100), (font_height + (font_height / 4)), 0, 255, 0);
|
|
|
|
uiFill(FB_WIDTH - (FB_WIDTH / 4), (progressCtx->line_offset * (font_height + (font_height / 4))) + 8, FB_WIDTH / 4, (font_height + (font_height / 4)), BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB);
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%u%% [%s / %s]", progressCtx->progress, progressCtx->curOffsetStr, progressCtx->totalSizeStr);
|
|
uiDrawString(strbuf, FB_WIDTH - (FB_WIDTH / 4) + (font_height * 2), (progressCtx->line_offset * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
|
|
uiRefreshDisplay();
|
|
}
|
|
|
|
void setProgressBarError(progress_ctx_t *progressCtx)
|
|
{
|
|
if (!progressCtx) return;
|
|
|
|
uiFill(FB_WIDTH / 4, (progressCtx->line_offset * (font_height + (font_height / 4))) + 10, FB_WIDTH / 2, (font_height + (font_height / 4)), 0, 0, 0);
|
|
uiFill(FB_WIDTH / 4, (progressCtx->line_offset * (font_height + (font_height / 4))) + 10, (((u32)progressCtx->progress * ((u32)FB_WIDTH / 2)) / 100), (font_height + (font_height / 4)), 255, 0, 0);
|
|
}
|
|
|
|
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(message, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;
|
|
}
|
|
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "[ %s ] Yes | [ %s ] No", NINTENDO_FONT_A, NINTENDO_FONT_B);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks += 2;
|
|
|
|
uiRefreshDisplay();
|
|
|
|
bool ret = false;
|
|
|
|
while(true)
|
|
{
|
|
hidScanInput();
|
|
|
|
u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO);
|
|
|
|
if (keysDown & KEY_A)
|
|
{
|
|
ret = true;
|
|
break;
|
|
} else
|
|
if (keysDown & KEY_B)
|
|
{
|
|
ret = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool checkIfDumpedNspContainsConsoleData(const char *nspPath)
|
|
{
|
|
if (!nspPath || !strlen(nspPath)) return false;
|
|
|
|
FILE *nspFile = NULL;
|
|
u64 nspSize = 0;
|
|
|
|
size_t read_bytes;
|
|
pfs0_header nspHeader;
|
|
pfs0_entry_table *nspEntries = NULL;
|
|
char *nspStrTable = NULL;
|
|
|
|
u32 i;
|
|
bool foundTik = false;
|
|
u64 tikOffset = 0, tikSize = 0;
|
|
rsa2048_sha256_ticket tikData;
|
|
|
|
const u8 titlekey_block_0x190_empty_hash[0x20] = {
|
|
0x2D, 0xFB, 0xA6, 0x33, 0x81, 0x70, 0x46, 0xC7, 0xF5, 0x59, 0xED, 0x4B, 0x93, 0x07, 0x60, 0x48,
|
|
0x43, 0x5F, 0x7E, 0x1A, 0x90, 0xF1, 0x4E, 0xB8, 0x03, 0x5C, 0x04, 0xB9, 0xEB, 0xAE, 0x25, 0x37
|
|
};
|
|
|
|
u8 titlekey_block_0x190_hash[0x20];
|
|
|
|
nspFile = fopen(nspPath, "rb");
|
|
if (!nspFile) return false;
|
|
|
|
fseek(nspFile, 0, SEEK_END);
|
|
nspSize = ftell(nspFile);
|
|
rewind(nspFile);
|
|
|
|
if (nspSize < sizeof(pfs0_header))
|
|
{
|
|
fclose(nspFile);
|
|
return false;
|
|
}
|
|
|
|
read_bytes = fread(&nspHeader, 1, sizeof(pfs0_header), nspFile);
|
|
|
|
if (read_bytes != sizeof(pfs0_header) || bswap_32(nspHeader.magic) != PFS0_MAGIC || nspSize < (sizeof(pfs0_header) + (sizeof(pfs0_entry_table) * (u64)nspHeader.file_cnt) + (u64)nspHeader.str_table_size))
|
|
{
|
|
fclose(nspFile);
|
|
return false;
|
|
}
|
|
|
|
nspEntries = calloc((u64)nspHeader.file_cnt, sizeof(pfs0_entry_table));
|
|
if (!nspEntries)
|
|
{
|
|
fclose(nspFile);
|
|
return false;
|
|
}
|
|
|
|
read_bytes = fread(nspEntries, 1, sizeof(pfs0_entry_table) * (u64)nspHeader.file_cnt, nspFile);
|
|
|
|
if (read_bytes != (sizeof(pfs0_entry_table) * (u64)nspHeader.file_cnt))
|
|
{
|
|
free(nspEntries);
|
|
fclose(nspFile);
|
|
return false;
|
|
}
|
|
|
|
nspStrTable = calloc((u64)nspHeader.str_table_size, sizeof(char));
|
|
if (!nspStrTable)
|
|
{
|
|
free(nspEntries);
|
|
fclose(nspFile);
|
|
return false;
|
|
}
|
|
|
|
read_bytes = fread(nspStrTable, 1, (u64)nspHeader.str_table_size, nspFile);
|
|
|
|
if (read_bytes != (u64)nspHeader.str_table_size)
|
|
{
|
|
free(nspStrTable);
|
|
free(nspEntries);
|
|
fclose(nspFile);
|
|
return false;
|
|
}
|
|
|
|
for(i = 0; i < nspHeader.file_cnt; i++)
|
|
{
|
|
char *curFilename = (nspStrTable + nspEntries[i].filename_offset);
|
|
|
|
if (!strncasecmp(curFilename + strlen(curFilename) - 4, ".tik", 4))
|
|
{
|
|
tikOffset = (sizeof(pfs0_header) + (sizeof(pfs0_entry_table) * (u64)nspHeader.file_cnt) + (u64)nspHeader.str_table_size + nspEntries[i].file_offset);
|
|
tikSize = nspEntries[i].file_size;
|
|
foundTik = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
free(nspStrTable);
|
|
free(nspEntries);
|
|
|
|
if (!foundTik || tikSize != ETICKET_TIK_FILE_SIZE || nspSize < (tikOffset + tikSize))
|
|
{
|
|
fclose(nspFile);
|
|
return false;
|
|
}
|
|
|
|
fseek(nspFile, tikOffset, SEEK_SET);
|
|
|
|
read_bytes = fread(&tikData, 1, ETICKET_TIK_FILE_SIZE, nspFile);
|
|
|
|
fclose(nspFile);
|
|
|
|
if (read_bytes != ETICKET_TIK_FILE_SIZE) return false;
|
|
|
|
sha256CalculateHash(titlekey_block_0x190_hash, tikData.titlekey_block + 0x10, 0xF0);
|
|
|
|
if (!strncmp(tikData.sig_issuer, "Root-CA00000003-XS00000021", 26) || memcmp(titlekey_block_0x190_hash, titlekey_block_0x190_empty_hash, 0x20) != 0 || tikData.titlekey_type != ETICKET_TITLEKEY_COMMON || tikData.ticket_id != 0 || tikData.device_id != 0 || tikData.account_id != 0) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void removeDirectory(const char *path)
|
|
{
|
|
if (!path || !strlen(path)) return;
|
|
|
|
struct dirent *ent;
|
|
char cur_path[NAME_BUF_LEN] = {'\0'};
|
|
|
|
DIR *dir = opendir(path);
|
|
if (dir)
|
|
{
|
|
while((ent = readdir(dir)) != NULL)
|
|
{
|
|
if ((strlen(ent->d_name) == 1 && !strcmp(ent->d_name, ".")) || (strlen(ent->d_name) == 2 && !strcmp(ent->d_name, ".."))) continue;
|
|
|
|
snprintf(cur_path, sizeof(cur_path) / sizeof(cur_path[0]), "%s/%s", path, ent->d_name);
|
|
|
|
if (ent->d_type == DT_DIR)
|
|
{
|
|
removeDirectory(cur_path);
|
|
} else {
|
|
unlink(cur_path);
|
|
}
|
|
}
|
|
|
|
closedir(dir);
|
|
|
|
rmdir(path);
|
|
}
|
|
}
|
|
|
|
bool parseNSWDBRelease(xmlDocPtr doc, xmlNodePtr cur, u64 gc_tid, u32 crc)
|
|
{
|
|
if (!doc || !cur) return false;
|
|
|
|
xmlChar *key;
|
|
xmlNodePtr node = cur;
|
|
|
|
u8 imageSize = (u8)(gameCardSize / GAMECARD_SIZE_1GiB);
|
|
|
|
u8 xmlImageSize = 0;
|
|
u64 xmlTitleID = 0;
|
|
u32 xmlCrc = 0;
|
|
char xmlReleaseName[256] = {'\0'};
|
|
|
|
bool found = false;
|
|
|
|
while(node != NULL)
|
|
{
|
|
if ((!xmlStrcmp(node->name, (const xmlChar *)nswReleasesChildrenImageSize)))
|
|
{
|
|
key = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
|
|
if (key)
|
|
{
|
|
xmlImageSize = (u8)atoi((const char*)key);
|
|
xmlFree(key);
|
|
}
|
|
} else
|
|
if ((!xmlStrcmp(node->name, (const xmlChar *)nswReleasesChildrenTitleID)))
|
|
{
|
|
key = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
|
|
if (key)
|
|
{
|
|
xmlTitleID = strtoull((const char*)key, NULL, 16);
|
|
xmlFree(key);
|
|
}
|
|
} else
|
|
if ((!xmlStrcmp(node->name, (const xmlChar *)nswReleasesChildrenImgCrc)))
|
|
{
|
|
key = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
|
|
if (key)
|
|
{
|
|
xmlCrc = strtoul((const char*)key, NULL, 16);
|
|
xmlFree(key);
|
|
}
|
|
}
|
|
if ((!xmlStrcmp(node->name, (const xmlChar *)nswReleasesChildrenReleaseName)))
|
|
{
|
|
key = xmlNodeListGetString(doc, node->xmlChildrenNode, 1);
|
|
if (key)
|
|
{
|
|
snprintf(xmlReleaseName, sizeof(xmlReleaseName) / sizeof(xmlReleaseName[0]), "%s", (char*)key);
|
|
xmlFree(key);
|
|
}
|
|
}
|
|
|
|
node = node->next;
|
|
}
|
|
|
|
/*if (xmlImageSize && xmlTitleID && strlen(xmlReleaseName))
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XML Image Size: %u.", xmlImageSize);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;
|
|
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XML Title ID: %016lX.", xmlTitleID);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;
|
|
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XML Image CRC32: %08X.", xmlCrc);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;
|
|
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XML Release Name: %s.", xmlReleaseName);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks += 2;
|
|
}*/
|
|
|
|
if (xmlImageSize == imageSize && xmlTitleID == gc_tid && xmlCrc == crc)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Found matching Scene release: \"%s\" (CRC32: %08X). This is a good dump!", xmlReleaseName, xmlCrc);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0);
|
|
found = true;
|
|
} else {
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dump doesn't match Scene release: \"%s\"! (CRC32: %08X)", xmlReleaseName, xmlCrc);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
|
|
breaks++;
|
|
|
|
return found;
|
|
}
|
|
|
|
xmlXPathObjectPtr getNodeSet(xmlDocPtr doc, xmlChar *xpath)
|
|
{
|
|
xmlXPathContextPtr context = NULL;
|
|
xmlXPathObjectPtr result = NULL;
|
|
|
|
context = xmlXPathNewContext(doc);
|
|
result = xmlXPathEvalExpression(xpath, context);
|
|
|
|
if (xmlXPathNodeSetIsEmpty(result->nodesetval))
|
|
{
|
|
xmlXPathFreeObject(result);
|
|
return NULL;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void gameCardDumpNSWDBCheck(u32 crc)
|
|
{
|
|
if (!titleAppCount || !titleAppTitleID || !hfs0_partition_cnt) return;
|
|
|
|
xmlDocPtr doc = NULL;
|
|
bool found = false;
|
|
|
|
doc = xmlParseFile(nswReleasesXmlPath);
|
|
if (doc)
|
|
{
|
|
u32 i;
|
|
for(i = 0; i < titleAppCount; i++)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "//%s/%s[.//%s='%016lX']", nswReleasesRootElement, nswReleasesChildren, nswReleasesChildrenTitleID, titleAppTitleID[i]);
|
|
xmlXPathObjectPtr nodeSet = getNodeSet(doc, (xmlChar*)strbuf);
|
|
if (nodeSet)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Found %d %s with Title ID \"%016lX\".", nodeSet->nodesetval->nodeNr, (nodeSet->nodesetval->nodeNr > 1 ? "releases" : "release"), titleAppTitleID[i]);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;
|
|
|
|
uiRefreshDisplay();
|
|
|
|
u32 i;
|
|
for(i = 0; i < nodeSet->nodesetval->nodeNr; i++)
|
|
{
|
|
xmlNodePtr node = nodeSet->nodesetval->nodeTab[i]->xmlChildrenNode;
|
|
|
|
found = parseNSWDBRelease(doc, node, titleAppTitleID[i], crc);
|
|
if (found) break;
|
|
}
|
|
|
|
xmlXPathFreeObject(nodeSet);
|
|
|
|
if (!found)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "No checksum matches found in XML document for Title ID \"%016lX\"!", titleAppTitleID[i]);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
if ((i + 1) < titleAppCount) breaks += 2;
|
|
} else {
|
|
breaks--;
|
|
break;
|
|
}
|
|
} else {
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "No records with Title ID \"%016lX\" found within the XML document!", titleAppTitleID[i]);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
if ((i + 1) < titleAppCount) breaks += 2;
|
|
}
|
|
}
|
|
|
|
xmlFreeDoc(doc);
|
|
|
|
if (!found)
|
|
{
|
|
breaks++;
|
|
uiDrawString("This could either be a bad dump or an undumped cartridge.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
} else {
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to open and/or parse \"%s\"!", nswReleasesXmlPath);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
}
|
|
|
|
Result networkInit()
|
|
{
|
|
Result result = socketInitializeDefault();
|
|
if (R_SUCCEEDED(result)) curl_global_init(CURL_GLOBAL_ALL);
|
|
return result;
|
|
}
|
|
|
|
void networkDeinit()
|
|
{
|
|
curl_global_cleanup();
|
|
socketExit();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void updateNSWDBXml()
|
|
{
|
|
Result result;
|
|
CURL *curl;
|
|
CURLcode res;
|
|
long http_code = 0;
|
|
double size = 0.0;
|
|
bool success = false;
|
|
|
|
if (R_SUCCEEDED(result = networkInit()))
|
|
{
|
|
curl = curl_easy_init();
|
|
if (curl)
|
|
{
|
|
FILE *nswdbXml = fopen(nswReleasesXmlTmpPath, "wb");
|
|
if (nswdbXml)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Downloading XML database from \"%s\", please wait...", nswReleasesXmlUrl);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks += 2;
|
|
|
|
if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication)
|
|
{
|
|
uiDrawString("Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
breaks += 2;
|
|
}
|
|
|
|
uiRefreshDisplay();
|
|
|
|
curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 102400L);
|
|
curl_easy_setopt(curl, CURLOPT_URL, nswReleasesXmlUrl);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCurlFile);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, nswdbXml);
|
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent);
|
|
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);
|
|
|
|
res = curl_easy_perform(curl);
|
|
|
|
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)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Successfully downloaded %.0lf bytes!", size);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0);
|
|
success = true;
|
|
} else {
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to request XML database! HTTP status code: %ld", http_code);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
|
|
fclose(nswdbXml);
|
|
|
|
if (success)
|
|
{
|
|
unlink(nswReleasesXmlPath);
|
|
rename(nswReleasesXmlTmpPath, nswReleasesXmlPath);
|
|
} else {
|
|
unlink(nswReleasesXmlTmpPath);
|
|
}
|
|
} else {
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to open \"%s\" in write mode!", nswReleasesXmlTmpPath);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
|
|
curl_easy_cleanup(curl);
|
|
} else {
|
|
uiDrawString("Error: failed to initialize CURL context!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
|
|
networkDeinit();
|
|
} else {
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to initialize socket! (%08X)", result);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
|
|
breaks += 2;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void updateApplication()
|
|
{
|
|
if (envIsNso())
|
|
{
|
|
uiDrawString("Error: unable to update application. It is not running as a NRO.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
breaks += 2;
|
|
return;
|
|
}
|
|
|
|
Result result;
|
|
CURL *curl;
|
|
CURLcode res;
|
|
long http_code = 0;
|
|
double size = 0.0;
|
|
char downloadUrl[512] = {'\0'}, releaseTag[32] = {'\0'};
|
|
bool success = false;
|
|
struct json_object *jobj, *name, *assets;
|
|
FILE *nxDumpToolNro = NULL;
|
|
char nroPath[NAME_BUF_LEN] = {'\0'};
|
|
|
|
if (R_SUCCEEDED(result = networkInit()))
|
|
{
|
|
curl = curl_easy_init();
|
|
if (curl)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Requesting latest release information from \"%s\"...", githubReleasesApiUrl);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;
|
|
|
|
uiRefreshDisplay();
|
|
|
|
curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 102400L);
|
|
curl_easy_setopt(curl, CURLOPT_URL, githubReleasesApiUrl);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCurlBuffer);
|
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent);
|
|
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);
|
|
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS);
|
|
|
|
res = curl_easy_perform(curl);
|
|
|
|
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)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Parsing response JSON data from \"%s\"...", githubReleasesApiUrl);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;
|
|
|
|
uiRefreshDisplay();
|
|
|
|
jobj = json_tokener_parse(result_buf);
|
|
if (jobj != NULL)
|
|
{
|
|
if (json_object_object_get_ex(jobj, "name", &name) && json_object_get_type(name) == json_type_string)
|
|
{
|
|
snprintf(releaseTag, sizeof(releaseTag) / sizeof(releaseTag[0]), json_object_get_string(name));
|
|
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Latest release: %s.", releaseTag);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;
|
|
|
|
uiRefreshDisplay();
|
|
|
|
// Compare versions
|
|
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';
|
|
}
|
|
|
|
if (versionNumCmp(releaseTag, APP_VERSION) > 0)
|
|
{
|
|
if (json_object_object_get_ex(jobj, "assets", &assets) && json_object_get_type(assets) == json_type_array)
|
|
{
|
|
assets = json_object_array_get_idx(assets, 0);
|
|
if (assets != NULL)
|
|
{
|
|
if (json_object_object_get_ex(assets, "browser_download_url", &assets) && json_object_get_type(assets) == json_type_string)
|
|
{
|
|
snprintf(downloadUrl, sizeof(downloadUrl) / sizeof(downloadUrl[0]), json_object_get_string(assets));
|
|
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Download URL: \"%s\".", downloadUrl);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks++;
|
|
|
|
uiDrawString("Please wait...", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
breaks += 2;
|
|
|
|
if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication)
|
|
{
|
|
uiDrawString("Do not press the " NINTENDO_FONT_HOME " button. Doing so could corrupt the SD card filesystem.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
breaks += 2;
|
|
}
|
|
|
|
uiRefreshDisplay();
|
|
|
|
snprintf(nroPath, sizeof(nroPath) / sizeof(nroPath[0]), "%s.tmp", (strlen(appLaunchPath) ? appLaunchPath : nxDumpToolPath));
|
|
|
|
nxDumpToolNro = fopen(nroPath, "wb");
|
|
if (nxDumpToolNro)
|
|
{
|
|
curl_easy_reset(curl);
|
|
|
|
curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 102400L);
|
|
curl_easy_setopt(curl, CURLOPT_URL, downloadUrl);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCurlFile);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, nxDumpToolNro);
|
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent);
|
|
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);
|
|
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS);
|
|
|
|
res = curl_easy_perform(curl);
|
|
|
|
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)
|
|
{
|
|
success = true;
|
|
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Successfully downloaded %.0lf bytes!", size);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0);
|
|
breaks++;
|
|
|
|
uiDrawString("Please restart the application to reflect the changes.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0);
|
|
} else {
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to request latest update binary! HTTP status code: %ld", http_code);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
|
|
fclose(nxDumpToolNro);
|
|
|
|
if (success)
|
|
{
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), nroPath);
|
|
nroPath[strlen(nroPath) - 4] = '\0';
|
|
|
|
unlink(nroPath);
|
|
rename(strbuf, nroPath);
|
|
} else {
|
|
unlink(nroPath);
|
|
}
|
|
} else {
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to open \"%s\" in write mode!", nroPath);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
} else {
|
|
uiDrawString("Error: unable to parse download URL from JSON response!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
} else {
|
|
uiDrawString("Error: unable to parse object at index 0 from \"assets\" array in JSON response!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
} else {
|
|
uiDrawString("Error: unable to parse \"assets\" array from JSON response!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
} else {
|
|
uiDrawString("You already have the latest version!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255);
|
|
}
|
|
} else {
|
|
uiDrawString("Error: unable to parse version tag from JSON response!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
|
|
json_object_put(jobj);
|
|
} else {
|
|
uiDrawString("Error: unable to parse JSON response!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
} else {
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to request latest release information! HTTP status code: %ld", http_code);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
|
|
if (result_buf) free(result_buf);
|
|
|
|
curl_easy_cleanup(curl);
|
|
} else {
|
|
uiDrawString("Error: failed to initialize CURL context!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
|
|
networkDeinit();
|
|
} else {
|
|
snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to initialize socket! (%08X)", result);
|
|
uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0);
|
|
}
|
|
|
|
breaks += 2;
|
|
}
|