From 7c4e7a4db080a5e168fa8ae59cced964ab44d992 Mon Sep 17 00:00:00 2001 From: Pablo Curiel Date: Wed, 29 Jul 2020 17:02:21 -0400 Subject: [PATCH] New test app: system title NCA section dumper. --- code_templates/gc_key_area.c | 106 ++++ code_templates/system_title_dumper.c | 438 ++++++++++++++++ code_templates/threaded_usb_bktr_dumper.c | 491 ++++++++++++++++++ source/gamecard.c | 18 +- source/gamecard.h | 7 +- source/main.c | 602 ++++++++++------------ source/nca.c | 40 +- source/nca.h | 3 +- source/pfs.c | 2 +- source/title.c | 21 +- source/title.h | 16 +- source/usb.c | 2 +- source/utils.c | 68 ++- source/utils.h | 13 +- 14 files changed, 1438 insertions(+), 389 deletions(-) create mode 100644 code_templates/gc_key_area.c create mode 100644 code_templates/system_title_dumper.c create mode 100644 code_templates/threaded_usb_bktr_dumper.c diff --git a/code_templates/gc_key_area.c b/code_templates/gc_key_area.c new file mode 100644 index 0000000..21e135f --- /dev/null +++ b/code_templates/gc_key_area.c @@ -0,0 +1,106 @@ +/* + * main.c + * + * Copyright (c) 2020, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * nxdumptool is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "utils.h" +#include "gamecard.h" + +static void consolePrint(const char *text, ...) +{ + va_list v; + va_start(v, text); + vfprintf(stdout, text, v); + va_end(v); + consoleUpdate(NULL); +} + +int main(int argc, char *argv[]) +{ + (void)argc; + (void)argv; + + int ret = 0; + + GameCardKeyArea gc_key_area = {0}; + char path[FS_MAX_PATH] = {0}; + + LOGFILE("nxdumptool starting."); + + consoleInit(NULL); + + consolePrint("initializing...\n"); + + if (!utilsInitializeResources()) + { + ret = -1; + goto out; + } + + consolePrint("waiting for gamecard. press b to cancel.\n"); + + while(true) + { + hidScanInput(); + + if (utilsHidKeysAllDown() == KEY_B) + { + consolePrint("process cancelled\n"); + goto out2; + } + + u8 status = gamecardGetStatus(); + if (status == GameCardStatus_InsertedAndInfoLoaded) break; + if (status == GameCardStatus_InsertedAndInfoNotLoaded) consolePrint("gamecard inserted but info couldn't be loaded from it. check nogc patch setting.\n"); + } + + consolePrint("gamecard detected.\n"); + + if (!gamecardGetKeyArea(&gc_key_area)) + { + consolePrint("failed to get gamecard key area\n"); + goto out2; + } + + consolePrint("get gamecard key area ok\n"); + + sprintf(path, "sdmc:/card_key_area_%016lX.bin", gc_key_area.initial_data.package_id); + + FILE *kafd = fopen(path, "wb"); + if (!kafd) + { + consolePrint("failed to open \"%s\" for writing\n", path); + goto out2; + } + + fwrite(&gc_key_area, 1, sizeof(GameCardKeyArea), kafd); + fclose(kafd); + + consolePrint("successfully saved key area to \"%s\"\n", path); + +out2: + consolePrint("press any button to exit\n"); + utilsWaitForButtonPress(KEY_NONE); + +out: + utilsCloseResources(); + + consoleExit(NULL); + + return ret; +} diff --git a/code_templates/system_title_dumper.c b/code_templates/system_title_dumper.c new file mode 100644 index 0000000..6cc3bd8 --- /dev/null +++ b/code_templates/system_title_dumper.c @@ -0,0 +1,438 @@ +/* + * main.c + * + * Copyright (c) 2020, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * nxdumptool is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "utils.h" +#include "nca.h" +#include "title.h" +#include "pfs.h" +#include "romfs.h" + +#define BLOCK_SIZE 0x800000 +#define OUTPATH "sdmc:/systitle_dumps" + +static u8 *buf = NULL; +static FILE *filefd = NULL; +static char path[FS_MAX_PATH * 2] = {0}; + +static void consolePrint(const char *text, ...) +{ + va_list v; + va_start(v, text); + vfprintf(stdout, text, v); + va_end(v); + consoleUpdate(NULL); +} + +static void dumpPartitionFs(TitleInfo *info, NcaFsSectionContext *nca_fs_ctx) +{ + if (!buf || !info || !nca_fs_ctx) return; + + u32 pfs_entry_count = 0; + PartitionFileSystemContext pfs_ctx = {0}; + PartitionFileSystemEntry *pfs_entry = NULL; + char *pfs_entry_name = NULL; + + size_t path_len = 0; + + if (!pfsInitializeContext(&pfs_ctx, nca_fs_ctx)) + { + consolePrint("pfs initialize ctx failed!\n"); + goto end; + } + + if (!(pfs_entry_count = pfsGetEntryCount(&pfs_ctx))) + { + consolePrint("pfs entry count is zero!\n"); + goto end; + } + + snprintf(path, sizeof(path), OUTPATH "/%016lX - %s/%s (%s)/Section %u (%s)", info->meta_key.id, info->app_metadata->lang_entry.name, ((NcaContext*)nca_fs_ctx->nca_ctx)->content_id_str, \ + titleGetNcmContentTypeName(((NcaContext*)nca_fs_ctx->nca_ctx)->content_type), nca_fs_ctx->section_num, ncaGetFsSectionTypeName(nca_fs_ctx->section_type)); + utilsCreateDirectoryTree(path, true); + path_len = strlen(path); + + for(u32 i = 0; i < pfs_entry_count; i++) + { + if (!(pfs_entry = pfsGetEntryByIndex(&pfs_ctx, i)) || !(pfs_entry_name = pfsGetEntryNameByIndex(&pfs_ctx, i)) || !strlen(pfs_entry_name)) + { + consolePrint("pfs get entry / get name #%u failed!\n", i); + goto end; + } + + path[path_len] = '\0'; + strcat(path, "/"); + strcat(path, pfs_entry_name); + utilsReplaceIllegalCharacters(path + path_len + 1, true); + + filefd = fopen(path, "wb"); + if (!filefd) + { + consolePrint("failed to create \"%s\"!\n", path); + goto end; + } + + consolePrint("dumping \"%s\"...\n", pfs_entry_name); + + u64 blksize = BLOCK_SIZE; + for(u64 j = 0; j < pfs_entry->size; j += blksize) + { + if (blksize > (pfs_entry->size - j)) blksize = (pfs_entry->size - j); + + if (!pfsReadEntryData(&pfs_ctx, pfs_entry, buf, blksize, j)) + { + consolePrint("failed to read 0x%lX block from offset 0x%lX!\n", blksize, j); + goto end; + } + + fwrite(buf, 1, blksize, filefd); + } + + fclose(filefd); + filefd = NULL; + } + + consolePrint("pfs dump complete\n"); + +end: + if (filefd) + { + fclose(filefd); + remove(path); + } + + pfsFreeContext(&pfs_ctx); +} + +static void dumpRomFs(TitleInfo *info, NcaFsSectionContext *nca_fs_ctx) +{ + if (!buf || !info || !nca_fs_ctx) return; + + u64 romfs_file_table_offset = 0; + RomFileSystemContext romfs_ctx = {0}; + RomFileSystemFileEntry *romfs_file_entry = NULL; + + size_t path_len = 0; + + if (!romfsInitializeContext(&romfs_ctx, nca_fs_ctx)) + { + consolePrint("romfs initialize ctx failed!\n"); + goto end; + } + + snprintf(path, sizeof(path), OUTPATH "/%016lX - %s/%s (%s)/Section %u (%s)", info->meta_key.id, info->app_metadata->lang_entry.name, ((NcaContext*)nca_fs_ctx->nca_ctx)->content_id_str, \ + titleGetNcmContentTypeName(((NcaContext*)nca_fs_ctx->nca_ctx)->content_type), nca_fs_ctx->section_num, ncaGetFsSectionTypeName(nca_fs_ctx->section_type)); + utilsCreateDirectoryTree(path, true); + path_len = strlen(path); + + while(romfs_file_table_offset < romfs_ctx.file_table_size) + { + if (!(romfs_file_entry = romfsGetFileEntryByOffset(&romfs_ctx, romfs_file_table_offset)) || \ + !romfsGeneratePathFromFileEntry(&romfs_ctx, romfs_file_entry, path + path_len, sizeof(path) - path_len, RomFileSystemPathIllegalCharReplaceType_KeepAsciiCharsOnly)) + { + consolePrint("romfs get entry / generate path failed for 0x%lX!\n", romfs_file_table_offset); + goto end; + } + + utilsCreateDirectoryTree(path, false); + + filefd = fopen(path, "wb"); + if (!filefd) + { + consolePrint("failed to create \"%s\"!\n", path); + goto end; + } + + consolePrint("dumping \"%s\"...\n", path + path_len); + + u64 blksize = BLOCK_SIZE; + for(u64 j = 0; j < romfs_file_entry->size; j += blksize) + { + if (blksize > (romfs_file_entry->size - j)) blksize = (romfs_file_entry->size - j); + + if (!romfsReadFileEntryData(&romfs_ctx, romfs_file_entry, buf, blksize, j)) + { + consolePrint("failed to read 0x%lX block from offset 0x%lX!\n", blksize, j); + goto end; + } + + fwrite(buf, 1, blksize, filefd); + } + + fclose(filefd); + filefd = NULL; + + romfs_file_table_offset += ALIGN_UP(sizeof(RomFileSystemFileEntry) + romfs_file_entry->name_length, 4); + } + + consolePrint("romfs dump complete\n"); + +end: + if (filefd) + { + fclose(filefd); + remove(path); + } + + romfsFreeContext(&romfs_ctx); +} + +static void dumpFsSection(TitleInfo *info, NcaFsSectionContext *nca_fs_ctx) +{ + if (!buf || !info || !nca_fs_ctx) return; + + switch(nca_fs_ctx->section_type) + { + case NcaFsSectionType_PartitionFs: + dumpPartitionFs(info, nca_fs_ctx); + break; + case NcaFsSectionType_RomFs: + case NcaFsSectionType_Nca0RomFs: + dumpRomFs(info, nca_fs_ctx); + break; + default: + consolePrint("invalid section type!\n"); + break; + } +} + +int main(int argc, char *argv[]) +{ + (void)argc; + (void)argv; + + int ret = 0; + + LOGFILE(APP_TITLE " starting."); + + consoleInit(NULL); + + consolePrint("initializing...\n"); + + if (!utilsInitializeResources()) + { + ret = -1; + goto out; + } + + u32 app_count = 0; + TitleApplicationMetadata **app_metadata = NULL; + TitleInfo *cur_title_info = NULL; + + u32 selected_idx = 0, menu = 0, page_size = 30, cur_page = 0; + u32 title_idx = 0, nca_idx = 0; + char nca_id_str[0x21] = {0}; + + NcaContext *nca_ctx = NULL; + Ticket tik = {0}; + + app_metadata = titleGetApplicationMetadataEntries(true, &app_count); + if (!app_metadata || !app_count) + { + consolePrint("app metadata failed\n"); + goto out2; + } + + consolePrint("app metadata succeeded\n"); + + buf = malloc(BLOCK_SIZE); + if (!buf) + { + consolePrint("buf failed\n"); + goto out2; + } + + consolePrint("buf succeeded\n"); + + nca_ctx = calloc(1, sizeof(NcaContext)); + if (!nca_ctx) + { + consolePrint("nca ctx buf failed\n"); + goto out2; + } + + consolePrint("nca ctx buf succeeded\n"); + + utilsSleep(1); + + while(true) + { + consoleClear(); + + printf("select a %s.", menu == 0 ? "system title to view its contents" : (menu == 1 ? "content" : "fs section")); + printf("\npress b to %s.\n\n", menu == 0 ? "exit" : "go back"); + + if (menu >= 1) printf("selected title: %016lX - %s\n", app_metadata[title_idx]->title_id, app_metadata[title_idx]->lang_entry.name); + if (menu == 2) printf("selected content: %s\n", nca_id_str); + if (menu >= 1) printf("\n"); + + u32 max_val = (menu == 0 ? app_count : (menu == 1 ? cur_title_info->content_count : NCA_FS_HEADER_COUNT)); + for(u32 i = cur_page; i < max_val; i++) + { + if (i >= (cur_page + page_size)) break; + + printf("%s", i == selected_idx ? " -> " : " "); + + if (menu == 0) + { + printf("%016lX - %s\n", app_metadata[i]->title_id, app_metadata[i]->lang_entry.name); + } else + if (menu == 1) + { + utilsGenerateHexStringFromData(nca_id_str, sizeof(nca_id_str), cur_title_info->content_infos[i].content_id.c, SHA256_HASH_SIZE / 2); + printf("%s (%s)\n", nca_id_str, titleGetNcmContentTypeName(cur_title_info->content_infos[i].content_type)); + } else + if (menu == 2) + { + printf("fs section #%u (%s)\n", i + 1, ncaGetFsSectionTypeName(nca_ctx->fs_contexts[i].section_type)); + } + } + + printf("\n"); + + consoleUpdate(NULL); + + u64 btn_down = 0, btn_held = 0; + while(!btn_down && !btn_held) + { + hidScanInput(); + btn_down = utilsHidKeysAllDown(); + btn_held = utilsHidKeysAllHeld(); + } + + if (btn_down & KEY_A) + { + bool error = false; + + if (menu == 0) + { + title_idx = selected_idx; + } else + if (menu == 1) + { + nca_idx = selected_idx; + utilsGenerateHexStringFromData(nca_id_str, sizeof(nca_id_str), cur_title_info->content_infos[nca_idx].content_id.c, SHA256_HASH_SIZE / 2); + } + + menu++; + + if (menu == 1) + { + cur_title_info = titleGetInfoFromStorageByTitleId(NcmStorageId_BuiltInSystem, app_metadata[title_idx]->title_id); + if (!cur_title_info) + { + consolePrint("failed to get title info\n"); + error = true; + } + } else + if (menu == 2) + { + if (!ncaInitializeContext(nca_ctx, cur_title_info->storage_id, 0, &(cur_title_info->content_infos[nca_idx]), &tik)) + { + consolePrint("nca initialize ctx failed\n"); + error = true; + } + } else + if (menu == 3) + { + consoleClear(); + dumpFsSection(cur_title_info, &(nca_ctx->fs_contexts[selected_idx])); + } + + if (error || menu >= 3) + { + utilsSleep(3); + menu--; + } else { + selected_idx = cur_page = 0; + } + } else + if ((btn_down & KEY_DDOWN) || (btn_held & (KEY_LSTICK_DOWN | KEY_RSTICK_DOWN))) + { + selected_idx++; + + if (selected_idx >= max_val) + { + if (btn_down & KEY_DDOWN) + { + selected_idx = cur_page = 0; + } else { + selected_idx = (max_val - 1); + } + } else + if (selected_idx >= (cur_page + page_size)) + { + cur_page += page_size; + } + } else + if ((btn_down & KEY_DUP) || (btn_held & (KEY_LSTICK_UP | KEY_RSTICK_UP))) + { + selected_idx--; + + if (selected_idx == UINT32_MAX) + { + if (btn_down & KEY_DUP) + { + selected_idx = (max_val - 1); + cur_page = (max_val - (max_val % page_size)); + } else { + selected_idx = 0; + } + } else + if (selected_idx < cur_page) + { + cur_page -= page_size; + } + } else + if (btn_down & KEY_B) + { + menu--; + + if (menu == UINT32_MAX) + { + break; + } else { + selected_idx = (menu == 0 ? title_idx : nca_idx); + cur_page = (selected_idx - (selected_idx % page_size)); + } + } + + if (btn_held & (KEY_LSTICK_DOWN | KEY_RSTICK_DOWN | KEY_LSTICK_UP | KEY_RSTICK_UP)) svcSleepThread(50000000); // 50 ms + } + +out2: + if (menu != UINT32_MAX) + { + consolePrint("press any button to exit\n"); + utilsWaitForButtonPress(KEY_NONE); + } + + if (nca_ctx) free(nca_ctx); + + if (buf) free(buf); + + if (app_metadata) free(app_metadata); + +out: + utilsCloseResources(); + + consoleExit(NULL); + + return ret; +} diff --git a/code_templates/threaded_usb_bktr_dumper.c b/code_templates/threaded_usb_bktr_dumper.c new file mode 100644 index 0000000..83caf81 --- /dev/null +++ b/code_templates/threaded_usb_bktr_dumper.c @@ -0,0 +1,491 @@ +/* + * main.c + * + * Copyright (c) 2020, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * nxdumptool is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "utils.h" +#include "bktr.h" +#include "gamecard.h" +#include "usb.h" +#include "title.h" + +#define TEST_BUF_SIZE 0x800000 + +static Mutex g_fileMutex = 0; +static CondVar g_readCondvar = 0, g_writeCondvar = 0; + +typedef struct +{ + //FILE *fileobj; + BktrContext *bktr_ctx; + void *data; + size_t data_size; + size_t data_written; + size_t total_size; + bool read_error; + bool write_error; + bool transfer_cancelled; +} ThreadSharedData; + +static void consolePrint(const char *text, ...) +{ + va_list v; + va_start(v, text); + vfprintf(stdout, text, v); + va_end(v); + consoleUpdate(NULL); +} + +static int read_thread_func(void *arg) +{ + ThreadSharedData *shared_data = (ThreadSharedData*)arg; + if (!shared_data || !shared_data->data || !shared_data->total_size) + { + shared_data->read_error = true; + return -1; + } + + u8 *buf = malloc(TEST_BUF_SIZE); + if (!buf) + { + shared_data->read_error = true; + return -2; + } + + u64 file_table_offset = 0; + RomFileSystemFileEntry *file_entry = NULL; + char path[FS_MAX_PATH] = {0}; + + while(file_table_offset < shared_data->bktr_ctx->patch_romfs_ctx.file_table_size) + { + /* Check if the transfer has been cancelled by the user */ + if (shared_data->transfer_cancelled) + { + condvarWakeAll(&g_writeCondvar); + break; + } + + /* Retrieve RomFS file entry information */ + shared_data->read_error = (!(file_entry = bktrGetFileEntryByOffset(shared_data->bktr_ctx, file_table_offset)) || \ + !bktrGeneratePathFromFileEntry(shared_data->bktr_ctx, file_entry, path, FS_MAX_PATH, RomFileSystemPathIllegalCharReplaceType_IllegalFsChars)); + if (shared_data->read_error) + { + condvarWakeAll(&g_writeCondvar); + break; + } + + /* Wait until the previous file data chunk has been written */ + mutexLock(&g_fileMutex); + if (shared_data->data_size && !shared_data->write_error) condvarWait(&g_readCondvar, &g_fileMutex); + mutexUnlock(&g_fileMutex); + if (shared_data->write_error) break; + + /* Send current file properties */ + shared_data->read_error = !usbSendFileProperties(file_entry->size, path); + if (shared_data->read_error) + { + condvarWakeAll(&g_writeCondvar); + break; + } + + for(u64 offset = 0, blksize = TEST_BUF_SIZE; offset < file_entry->size; offset += blksize) + { + if (blksize > (file_entry->size - offset)) blksize = (file_entry->size - offset); + + /* Check if the transfer has been cancelled by the user */ + if (shared_data->transfer_cancelled) + { + condvarWakeAll(&g_writeCondvar); + break; + } + + /* Read current file data chunk */ + shared_data->read_error = !bktrReadFileEntryData(shared_data->bktr_ctx, file_entry, buf, blksize, offset); + if (shared_data->read_error) + { + condvarWakeAll(&g_writeCondvar); + break; + } + + /* Wait until the previous file data chunk has been written */ + mutexLock(&g_fileMutex); + + if (shared_data->data_size && !shared_data->write_error) condvarWait(&g_readCondvar, &g_fileMutex); + + if (shared_data->write_error) + { + mutexUnlock(&g_fileMutex); + break; + } + + /* Copy current file data chunk to the shared buffer */ + memcpy(shared_data->data, buf, blksize); + shared_data->data_size = blksize; + + /* Wake up the write thread to continue writing data */ + mutexUnlock(&g_fileMutex); + condvarWakeAll(&g_writeCondvar); + } + + if (shared_data->read_error || shared_data->write_error || shared_data->transfer_cancelled) break; + + file_table_offset += ALIGN_UP(sizeof(RomFileSystemFileEntry) + file_entry->name_length, 4); + } + + free(buf); + + return (shared_data->read_error ? -3 : 0); +} + +static int write_thread_func(void *arg) +{ + ThreadSharedData *shared_data = (ThreadSharedData*)arg; + if (!shared_data || !shared_data->data) + { + shared_data->write_error = true; + return -1; + } + + while(shared_data->data_written < shared_data->total_size) + { + /* Wait until the current file data chunk has been read */ + mutexLock(&g_fileMutex); + + if (!shared_data->data_size && !shared_data->read_error) condvarWait(&g_writeCondvar, &g_fileMutex); + + if (shared_data->read_error || shared_data->transfer_cancelled) + { + mutexUnlock(&g_fileMutex); + break; + } + + //shared_data->write_error = (fwrite(shared_data->data, 1, shared_data->data_size, shared_data->fileobj) != shared_data->data_size); + + /* Write current file data chunk */ + shared_data->write_error = !usbSendFileData(shared_data->data, shared_data->data_size); + if (!shared_data->write_error) + { + shared_data->data_written += shared_data->data_size; + shared_data->data_size = 0; + } + + /* Wake up the read thread to continue reading data */ + mutexUnlock(&g_fileMutex); + condvarWakeAll(&g_readCondvar); + + if (shared_data->write_error) break; + } + + return 0; +} + +int main(int argc, char *argv[]) +{ + (void)argc; + (void)argv; + + int ret = 0; + + LOGFILE(APP_TITLE " starting."); + + consoleInit(NULL); + + consolePrint("initializing...\n"); + + if (!utilsInitializeResources()) + { + ret = -1; + goto out; + } + + u32 app_count = 0, selected_idx = 0; + TitleApplicationMetadata **app_metadata = NULL; + TitleUserApplicationData user_app_data = {0}; + + u8 *buf = NULL; + + NcaContext *base_nca_ctx = NULL, *update_nca_ctx = NULL; + Ticket base_tik = {0}, update_tik = {0}; + + BktrContext bktr_ctx = {0}; + + ThreadSharedData shared_data = {0}; + thrd_t read_thread, write_thread; + + app_metadata = titleGetApplicationMetadataEntries(false, &app_count); + if (!app_metadata || !app_count) + { + consolePrint("app metadata failed\n"); + goto out2; + } + + consolePrint("app metadata succeeded\n"); + + buf = usbAllocatePageAlignedBuffer(TEST_BUF_SIZE); + if (!buf) + { + consolePrint("buf failed\n"); + goto out2; + } + + consolePrint("buf succeeded\n"); + + base_nca_ctx = calloc(1, sizeof(NcaContext)); + if (!base_nca_ctx) + { + consolePrint("base nca ctx buf failed\n"); + goto out2; + } + + consolePrint("base nca ctx buf succeeded\n"); + + update_nca_ctx = calloc(1, sizeof(NcaContext)); + if (!update_nca_ctx) + { + consolePrint("update nca ctx buf failed\n"); + goto out2; + } + + consolePrint("update nca ctx buf succeeded\n"); + + utilsSleep(1); + + while(true) + { + consoleClear(); + printf("select a base title with an available update.\nthe updated romfs will be dumped via usb.\npress b to cancel.\n\n"); + + for(u32 i = 0; i < app_count; i++) printf("%s%s (%016lX)\n", i == selected_idx ? " -> " : " ", app_metadata[i]->lang_entry.name, app_metadata[i]->title_id); + consoleUpdate(NULL); + + u64 btn = 0; + + while(true) + { + hidScanInput(); + + btn = utilsHidKeysAllDown(); + if (btn) break; + + if (titleRefreshGameCardTitleInfo()) + { + free(app_metadata); + + app_metadata = titleGetApplicationMetadataEntries(false, &app_count); + if (!app_metadata) + { + consolePrint("\napp metadata failed\n"); + goto out2; + } + + selected_idx = 0; + + break; + } + } + + if (btn & KEY_A) + { + if (!titleGetUserApplicationData(app_metadata[selected_idx]->title_id, &user_app_data) || !user_app_data.app_info || !user_app_data.patch_info) + { + consolePrint("\nthe selected title either doesn't have available base content or update content.\n"); + utilsSleep(3); + continue; + } + + break; + } else + if (btn & KEY_DOWN) + { + if ((selected_idx + 1) < app_count) + { + selected_idx++; + } else { + selected_idx = 0; + } + } else + if (btn & KEY_UP) + { + if (selected_idx == 0) + { + selected_idx = (app_count - 1); + } else { + selected_idx--; + } + } else + if (btn & KEY_B) + { + consolePrint("\nprocess cancelled.\n"); + goto out2; + } + } + + consoleClear(); + consolePrint("selected title:\n%s (%016lX)\n\n", app_metadata[selected_idx]->lang_entry.name, app_metadata[selected_idx]->title_id); + + if (!ncaInitializeContext(base_nca_ctx, user_app_data.app_info->storage_id, (user_app_data.app_info->storage_id == NcmStorageId_GameCard ? GameCardHashFileSystemPartitionType_Secure : 0), \ + titleGetContentInfoByTypeAndIdOffset(user_app_data.app_info, NcmContentType_Program, 0), &base_tik)) + { + consolePrint("nca initialize base ctx failed\n"); + goto out2; + } + + if (!ncaInitializeContext(update_nca_ctx, user_app_data.patch_info->storage_id, (user_app_data.patch_info->storage_id == NcmStorageId_GameCard ? GameCardHashFileSystemPartitionType_Secure : 0), \ + titleGetContentInfoByTypeAndIdOffset(user_app_data.patch_info, NcmContentType_Program, 0), &update_tik)) + { + consolePrint("nca initialize update ctx failed\n"); + goto out2; + } + + if (!bktrInitializeContext(&bktr_ctx, &(base_nca_ctx->fs_contexts[1]), &(update_nca_ctx->fs_contexts[1]))) + { + consolePrint("bktr initialize ctx failed\n"); + goto out2; + } + + consolePrint("bktr initialize ctx succeeded\n"); + + shared_data.bktr_ctx = &bktr_ctx; + shared_data.data = buf; + shared_data.data_size = 0; + shared_data.data_written = 0; + bktrGetTotalDataSize(&bktr_ctx, &(shared_data.total_size)); + + consolePrint("waiting for usb connection... "); + + time_t start = time(NULL); + bool usb_conn = false; + + while(true) + { + time_t now = time(NULL); + if ((now - start) >= 10) break; + consolePrint("%lu ", now - start); + + if ((usb_conn = usbIsReady())) break; + utilsSleep(1); + } + + consolePrint("\n"); + + if (!usb_conn) + { + consolePrint("usb connection failed\n"); + goto out2; + } + + consolePrint("creating threads\n"); + thrd_create(&read_thread, read_thread_func, &shared_data); + thrd_create(&write_thread, write_thread_func, &shared_data); + + u8 prev_time = 0; + u64 prev_size = 0; + u8 percent = 0; + + time_t btn_cancel_start_tmr = 0, btn_cancel_end_tmr = 0; + bool btn_cancel_cur_state = false, btn_cancel_prev_state = false; + + consolePrint("hold b to cancel\n\n"); + + start = time(NULL); + + while(shared_data.data_written < shared_data.total_size) + { + if (shared_data.read_error || shared_data.write_error) break; + + time_t now = time(NULL); + struct tm *ts = localtime(&now); + size_t size = shared_data.data_written; + + hidScanInput(); + btn_cancel_cur_state = (utilsHidKeysAllDown() & KEY_B); + + if (btn_cancel_cur_state && btn_cancel_cur_state != btn_cancel_prev_state) + { + btn_cancel_start_tmr = now; + } else + if (btn_cancel_cur_state && btn_cancel_cur_state == btn_cancel_prev_state) + { + btn_cancel_end_tmr = now; + if ((btn_cancel_end_tmr - btn_cancel_start_tmr) >= 3) + { + mutexLock(&g_fileMutex); + shared_data.transfer_cancelled = true; + mutexUnlock(&g_fileMutex); + break; + } + } else { + btn_cancel_start_tmr = btn_cancel_end_tmr = 0; + } + + btn_cancel_prev_state = btn_cancel_cur_state; + + if (prev_time == ts->tm_sec || prev_size == size) continue; + + percent = (u8)((size * 100) / shared_data.total_size); + + prev_time = ts->tm_sec; + prev_size = size; + + printf("%lu / %lu (%u%%) | Time elapsed: %lu\n", size, shared_data.total_size, percent, (now - start)); + consoleUpdate(NULL); + } + + start = (time(NULL) - start); + + consolePrint("\nwaiting for threads to join\n"); + thrd_join(read_thread, NULL); + consolePrint("read_thread done: %lu\n", time(NULL)); + thrd_join(write_thread, NULL); + consolePrint("write_thread done: %lu\n", time(NULL)); + + if (shared_data.read_error || shared_data.write_error) + { + consolePrint("usb transfer error\n"); + goto out2; + } + + if (shared_data.transfer_cancelled) + { + consolePrint("process cancelled\n"); + goto out2; + } + + consolePrint("process completed in %lu seconds\n", start); + +out2: + consolePrint("press any button to exit\n"); + utilsWaitForButtonPress(KEY_NONE); + + bktrFreeContext(&bktr_ctx); + + if (update_nca_ctx) free(update_nca_ctx); + + if (base_nca_ctx) free(base_nca_ctx); + + if (buf) free(buf); + + if (app_metadata) free(app_metadata); + +out: + utilsCloseResources(); + + consoleExit(NULL); + + return ret; +} diff --git a/source/gamecard.c b/source/gamecard.c index 6f40e98..5297cce 100644 --- a/source/gamecard.c +++ b/source/gamecard.c @@ -117,6 +117,16 @@ static MemoryLocation g_fsProgramMemory = { .data_size = 0 }; +static const char *g_gameCardHfsPartitionNames[] = { + [GameCardHashFileSystemPartitionType_Root] = "root", + [GameCardHashFileSystemPartitionType_Update] = "update", + [GameCardHashFileSystemPartitionType_Logo] = "logo", + [GameCardHashFileSystemPartitionType_Normal] = "normal", + [GameCardHashFileSystemPartitionType_Secure] = "secure", + [GameCardHashFileSystemPartitionType_Boot] = "boot", + [GameCardHashFileSystemPartitionType_Boot + 1] = "unknown" +}; + /* Function prototypes. */ static bool gamecardCreateDetectionThread(void); @@ -359,6 +369,12 @@ bool gamecardGetBundledFirmwareUpdateVersion(u32 *out) return ret; } +const char *gamecardGetHashFileSystemPartitionName(u8 hfs_partition_type) +{ + u8 idx = (hfs_partition_type > GameCardHashFileSystemPartitionType_Boot ? (GameCardHashFileSystemPartitionType_Boot + 1) : hfs_partition_type); + return g_gameCardHfsPartitionNames[idx]; +} + bool gamecardGetEntryCountFromHashFileSystemPartition(u8 hfs_partition_type, u32 *out_count) { bool ret = false; @@ -1059,7 +1075,7 @@ static GameCardHashFileSystemHeader *gamecardGetHashFileSystemPartitionHeader(u8 if (hfs_partition_type != GameCardHashFileSystemPartitionType_Root) { - if (gamecardGetHashFileSystemEntryIndexByName(fs_header, GAMECARD_HFS_PARTITION_NAME(hfs_partition_type), &hfs_partition_idx)) + if (gamecardGetHashFileSystemEntryIndexByName(fs_header, gamecardGetHashFileSystemPartitionName(hfs_partition_type), &hfs_partition_idx)) { fs_header = (GameCardHashFileSystemHeader*)g_gameCardHfsPartitions[hfs_partition_idx].header; if (out_hfs_partition_idx) *out_hfs_partition_idx = hfs_partition_idx; diff --git a/source/gamecard.h b/source/gamecard.h index eeb095f..c45da4d 100644 --- a/source/gamecard.h +++ b/source/gamecard.h @@ -32,10 +32,6 @@ #define GAMECARD_UPDATE_TID (u64)0x0100000000000816 -#define GAMECARD_HFS_PARTITION_NAME(x) ((x) == GameCardHashFileSystemPartitionType_Root ? "root" : ((x) == GameCardHashFileSystemPartitionType_Update ? "update" : \ - ((x) == GameCardHashFileSystemPartitionType_Logo ? "logo" : ((x) == GameCardHashFileSystemPartitionType_Normal ? "normal" : \ - ((x) == GameCardHashFileSystemPartitionType_Secure ? "secure" : ((x) == GameCardHashFileSystemPartitionType_Boot ? "boot" : "unknown")))))) - /// Encrypted using AES-128-ECB with the common titlekek generator key (stored in the .rodata segment from the Lotus firmware). typedef struct { u64 package_id; ///< Matches package_id from GameCardHeader. @@ -222,6 +218,9 @@ bool gamecardGetTrimmedSize(u64 *out); bool gamecardGetRomCapacity(u64 *out); ///< Not the same as gamecardGetTotalSize(). bool gamecardGetBundledFirmwareUpdateVersion(u32 *out); +/// Returns a pointer to a string holding the name of the provided hash file system partition type. +const char *gamecardGetHashFileSystemPartitionName(u8 hfs_partition_type); + /// Retrieves the entry count from a hash FS partition. bool gamecardGetEntryCountFromHashFileSystemPartition(u8 hfs_partition_type, u32 *out_count); diff --git a/source/main.c b/source/main.c index cff8d5a..6cc3bd8 100644 --- a/source/main.c +++ b/source/main.c @@ -19,28 +19,17 @@ */ #include "utils.h" -#include "bktr.h" -#include "gamecard.h" -#include "usb.h" +#include "nca.h" #include "title.h" +#include "pfs.h" +#include "romfs.h" -#define TEST_BUF_SIZE 0x800000 +#define BLOCK_SIZE 0x800000 +#define OUTPATH "sdmc:/systitle_dumps" -static Mutex g_fileMutex = 0; -static CondVar g_readCondvar = 0, g_writeCondvar = 0; - -typedef struct -{ - //FILE *fileobj; - BktrContext *bktr_ctx; - void *data; - size_t data_size; - size_t data_written; - size_t total_size; - bool read_error; - bool write_error; - bool transfer_cancelled; -} ThreadSharedData; +static u8 *buf = NULL; +static FILE *filefd = NULL; +static char path[FS_MAX_PATH * 2] = {0}; static void consolePrint(const char *text, ...) { @@ -51,147 +40,176 @@ static void consolePrint(const char *text, ...) consoleUpdate(NULL); } -static int read_thread_func(void *arg) +static void dumpPartitionFs(TitleInfo *info, NcaFsSectionContext *nca_fs_ctx) { - ThreadSharedData *shared_data = (ThreadSharedData*)arg; - if (!shared_data || !shared_data->data || !shared_data->total_size) + if (!buf || !info || !nca_fs_ctx) return; + + u32 pfs_entry_count = 0; + PartitionFileSystemContext pfs_ctx = {0}; + PartitionFileSystemEntry *pfs_entry = NULL; + char *pfs_entry_name = NULL; + + size_t path_len = 0; + + if (!pfsInitializeContext(&pfs_ctx, nca_fs_ctx)) { - shared_data->read_error = true; - return -1; + consolePrint("pfs initialize ctx failed!\n"); + goto end; } - u8 *buf = malloc(TEST_BUF_SIZE); - if (!buf) + if (!(pfs_entry_count = pfsGetEntryCount(&pfs_ctx))) { - shared_data->read_error = true; - return -2; + consolePrint("pfs entry count is zero!\n"); + goto end; } - u64 file_table_offset = 0; - RomFileSystemFileEntry *file_entry = NULL; - char path[FS_MAX_PATH] = {0}; + snprintf(path, sizeof(path), OUTPATH "/%016lX - %s/%s (%s)/Section %u (%s)", info->meta_key.id, info->app_metadata->lang_entry.name, ((NcaContext*)nca_fs_ctx->nca_ctx)->content_id_str, \ + titleGetNcmContentTypeName(((NcaContext*)nca_fs_ctx->nca_ctx)->content_type), nca_fs_ctx->section_num, ncaGetFsSectionTypeName(nca_fs_ctx->section_type)); + utilsCreateDirectoryTree(path, true); + path_len = strlen(path); - while(file_table_offset < shared_data->bktr_ctx->patch_romfs_ctx.file_table_size) + for(u32 i = 0; i < pfs_entry_count; i++) { - /* Check if the transfer has been cancelled by the user */ - if (shared_data->transfer_cancelled) + if (!(pfs_entry = pfsGetEntryByIndex(&pfs_ctx, i)) || !(pfs_entry_name = pfsGetEntryNameByIndex(&pfs_ctx, i)) || !strlen(pfs_entry_name)) { - condvarWakeAll(&g_writeCondvar); - break; + consolePrint("pfs get entry / get name #%u failed!\n", i); + goto end; } - /* Retrieve RomFS file entry information */ - shared_data->read_error = (!(file_entry = bktrGetFileEntryByOffset(shared_data->bktr_ctx, file_table_offset)) || \ - !bktrGeneratePathFromFileEntry(shared_data->bktr_ctx, file_entry, path, FS_MAX_PATH, RomFileSystemPathIllegalCharReplaceType_IllegalFsChars)); - if (shared_data->read_error) + path[path_len] = '\0'; + strcat(path, "/"); + strcat(path, pfs_entry_name); + utilsReplaceIllegalCharacters(path + path_len + 1, true); + + filefd = fopen(path, "wb"); + if (!filefd) { - condvarWakeAll(&g_writeCondvar); - break; + consolePrint("failed to create \"%s\"!\n", path); + goto end; } - /* Wait until the previous file data chunk has been written */ - mutexLock(&g_fileMutex); - if (shared_data->data_size && !shared_data->write_error) condvarWait(&g_readCondvar, &g_fileMutex); - mutexUnlock(&g_fileMutex); - if (shared_data->write_error) break; + consolePrint("dumping \"%s\"...\n", pfs_entry_name); - /* Send current file properties */ - shared_data->read_error = !usbSendFileProperties(file_entry->size, path); - if (shared_data->read_error) + u64 blksize = BLOCK_SIZE; + for(u64 j = 0; j < pfs_entry->size; j += blksize) { - condvarWakeAll(&g_writeCondvar); - break; - } - - for(u64 offset = 0, blksize = TEST_BUF_SIZE; offset < file_entry->size; offset += blksize) - { - if (blksize > (file_entry->size - offset)) blksize = (file_entry->size - offset); + if (blksize > (pfs_entry->size - j)) blksize = (pfs_entry->size - j); - /* Check if the transfer has been cancelled by the user */ - if (shared_data->transfer_cancelled) + if (!pfsReadEntryData(&pfs_ctx, pfs_entry, buf, blksize, j)) { - condvarWakeAll(&g_writeCondvar); - break; + consolePrint("failed to read 0x%lX block from offset 0x%lX!\n", blksize, j); + goto end; } - /* Read current file data chunk */ - shared_data->read_error = !bktrReadFileEntryData(shared_data->bktr_ctx, file_entry, buf, blksize, offset); - if (shared_data->read_error) - { - condvarWakeAll(&g_writeCondvar); - break; - } - - /* Wait until the previous file data chunk has been written */ - mutexLock(&g_fileMutex); - - if (shared_data->data_size && !shared_data->write_error) condvarWait(&g_readCondvar, &g_fileMutex); - - if (shared_data->write_error) - { - mutexUnlock(&g_fileMutex); - break; - } - - /* Copy current file data chunk to the shared buffer */ - memcpy(shared_data->data, buf, blksize); - shared_data->data_size = blksize; - - /* Wake up the write thread to continue writing data */ - mutexUnlock(&g_fileMutex); - condvarWakeAll(&g_writeCondvar); + fwrite(buf, 1, blksize, filefd); } - if (shared_data->read_error || shared_data->write_error || shared_data->transfer_cancelled) break; - - file_table_offset += ALIGN_UP(sizeof(RomFileSystemFileEntry) + file_entry->name_length, 4); + fclose(filefd); + filefd = NULL; } - free(buf); + consolePrint("pfs dump complete\n"); - return (shared_data->read_error ? -3 : 0); +end: + if (filefd) + { + fclose(filefd); + remove(path); + } + + pfsFreeContext(&pfs_ctx); } -static int write_thread_func(void *arg) +static void dumpRomFs(TitleInfo *info, NcaFsSectionContext *nca_fs_ctx) { - ThreadSharedData *shared_data = (ThreadSharedData*)arg; - if (!shared_data || !shared_data->data) + if (!buf || !info || !nca_fs_ctx) return; + + u64 romfs_file_table_offset = 0; + RomFileSystemContext romfs_ctx = {0}; + RomFileSystemFileEntry *romfs_file_entry = NULL; + + size_t path_len = 0; + + if (!romfsInitializeContext(&romfs_ctx, nca_fs_ctx)) { - shared_data->write_error = true; - return -1; + consolePrint("romfs initialize ctx failed!\n"); + goto end; } - while(shared_data->data_written < shared_data->total_size) + snprintf(path, sizeof(path), OUTPATH "/%016lX - %s/%s (%s)/Section %u (%s)", info->meta_key.id, info->app_metadata->lang_entry.name, ((NcaContext*)nca_fs_ctx->nca_ctx)->content_id_str, \ + titleGetNcmContentTypeName(((NcaContext*)nca_fs_ctx->nca_ctx)->content_type), nca_fs_ctx->section_num, ncaGetFsSectionTypeName(nca_fs_ctx->section_type)); + utilsCreateDirectoryTree(path, true); + path_len = strlen(path); + + while(romfs_file_table_offset < romfs_ctx.file_table_size) { - /* Wait until the current file data chunk has been read */ - mutexLock(&g_fileMutex); - - if (!shared_data->data_size && !shared_data->read_error) condvarWait(&g_writeCondvar, &g_fileMutex); - - if (shared_data->read_error || shared_data->transfer_cancelled) + if (!(romfs_file_entry = romfsGetFileEntryByOffset(&romfs_ctx, romfs_file_table_offset)) || \ + !romfsGeneratePathFromFileEntry(&romfs_ctx, romfs_file_entry, path + path_len, sizeof(path) - path_len, RomFileSystemPathIllegalCharReplaceType_KeepAsciiCharsOnly)) { - mutexUnlock(&g_fileMutex); + consolePrint("romfs get entry / generate path failed for 0x%lX!\n", romfs_file_table_offset); + goto end; + } + + utilsCreateDirectoryTree(path, false); + + filefd = fopen(path, "wb"); + if (!filefd) + { + consolePrint("failed to create \"%s\"!\n", path); + goto end; + } + + consolePrint("dumping \"%s\"...\n", path + path_len); + + u64 blksize = BLOCK_SIZE; + for(u64 j = 0; j < romfs_file_entry->size; j += blksize) + { + if (blksize > (romfs_file_entry->size - j)) blksize = (romfs_file_entry->size - j); + + if (!romfsReadFileEntryData(&romfs_ctx, romfs_file_entry, buf, blksize, j)) + { + consolePrint("failed to read 0x%lX block from offset 0x%lX!\n", blksize, j); + goto end; + } + + fwrite(buf, 1, blksize, filefd); + } + + fclose(filefd); + filefd = NULL; + + romfs_file_table_offset += ALIGN_UP(sizeof(RomFileSystemFileEntry) + romfs_file_entry->name_length, 4); + } + + consolePrint("romfs dump complete\n"); + +end: + if (filefd) + { + fclose(filefd); + remove(path); + } + + romfsFreeContext(&romfs_ctx); +} + +static void dumpFsSection(TitleInfo *info, NcaFsSectionContext *nca_fs_ctx) +{ + if (!buf || !info || !nca_fs_ctx) return; + + switch(nca_fs_ctx->section_type) + { + case NcaFsSectionType_PartitionFs: + dumpPartitionFs(info, nca_fs_ctx); + break; + case NcaFsSectionType_RomFs: + case NcaFsSectionType_Nca0RomFs: + dumpRomFs(info, nca_fs_ctx); + break; + default: + consolePrint("invalid section type!\n"); break; - } - - //shared_data->write_error = (fwrite(shared_data->data, 1, shared_data->data_size, shared_data->fileobj) != shared_data->data_size); - - /* Write current file data chunk */ - shared_data->write_error = !usbSendFileData(shared_data->data, shared_data->data_size); - if (!shared_data->write_error) - { - shared_data->data_written += shared_data->data_size; - shared_data->data_size = 0; - } - - /* Wake up the read thread to continue reading data */ - mutexUnlock(&g_fileMutex); - condvarWakeAll(&g_readCondvar); - - if (shared_data->write_error) break; } - - return 0; } int main(int argc, char *argv[]) @@ -213,21 +231,18 @@ int main(int argc, char *argv[]) goto out; } - u32 app_count = 0, selected_idx = 0; + u32 app_count = 0; TitleApplicationMetadata **app_metadata = NULL; - TitleUserApplicationData user_app_data = {0}; + TitleInfo *cur_title_info = NULL; - u8 *buf = NULL; + u32 selected_idx = 0, menu = 0, page_size = 30, cur_page = 0; + u32 title_idx = 0, nca_idx = 0; + char nca_id_str[0x21] = {0}; - NcaContext *base_nca_ctx = NULL, *update_nca_ctx = NULL; - Ticket base_tik = {0}, update_tik = {0}; + NcaContext *nca_ctx = NULL; + Ticket tik = {0}; - BktrContext bktr_ctx = {0}; - - ThreadSharedData shared_data = {0}; - thrd_t read_thread, write_thread; - - app_metadata = titleGetApplicationMetadataEntries(false, &app_count); + app_metadata = titleGetApplicationMetadataEntries(true, &app_count); if (!app_metadata || !app_count) { consolePrint("app metadata failed\n"); @@ -236,7 +251,7 @@ int main(int argc, char *argv[]) consolePrint("app metadata succeeded\n"); - buf = usbAllocatePageAlignedBuffer(TEST_BUF_SIZE); + buf = malloc(BLOCK_SIZE); if (!buf) { consolePrint("buf failed\n"); @@ -245,235 +260,170 @@ int main(int argc, char *argv[]) consolePrint("buf succeeded\n"); - base_nca_ctx = calloc(1, sizeof(NcaContext)); - if (!base_nca_ctx) + nca_ctx = calloc(1, sizeof(NcaContext)); + if (!nca_ctx) { - consolePrint("base nca ctx buf failed\n"); + consolePrint("nca ctx buf failed\n"); goto out2; } - consolePrint("base nca ctx buf succeeded\n"); - - update_nca_ctx = calloc(1, sizeof(NcaContext)); - if (!update_nca_ctx) - { - consolePrint("update nca ctx buf failed\n"); - goto out2; - } - - consolePrint("update nca ctx buf succeeded\n"); + consolePrint("nca ctx buf succeeded\n"); utilsSleep(1); while(true) { consoleClear(); - printf("select a base title with an available update.\nthe updated romfs will be dumped via usb.\npress b to cancel.\n\n"); - for(u32 i = 0; i < app_count; i++) printf("%s%s (%016lX)\n", i == selected_idx ? " -> " : " ", app_metadata[i]->lang_entry.name, app_metadata[i]->title_id); + printf("select a %s.", menu == 0 ? "system title to view its contents" : (menu == 1 ? "content" : "fs section")); + printf("\npress b to %s.\n\n", menu == 0 ? "exit" : "go back"); + + if (menu >= 1) printf("selected title: %016lX - %s\n", app_metadata[title_idx]->title_id, app_metadata[title_idx]->lang_entry.name); + if (menu == 2) printf("selected content: %s\n", nca_id_str); + if (menu >= 1) printf("\n"); + + u32 max_val = (menu == 0 ? app_count : (menu == 1 ? cur_title_info->content_count : NCA_FS_HEADER_COUNT)); + for(u32 i = cur_page; i < max_val; i++) + { + if (i >= (cur_page + page_size)) break; + + printf("%s", i == selected_idx ? " -> " : " "); + + if (menu == 0) + { + printf("%016lX - %s\n", app_metadata[i]->title_id, app_metadata[i]->lang_entry.name); + } else + if (menu == 1) + { + utilsGenerateHexStringFromData(nca_id_str, sizeof(nca_id_str), cur_title_info->content_infos[i].content_id.c, SHA256_HASH_SIZE / 2); + printf("%s (%s)\n", nca_id_str, titleGetNcmContentTypeName(cur_title_info->content_infos[i].content_type)); + } else + if (menu == 2) + { + printf("fs section #%u (%s)\n", i + 1, ncaGetFsSectionTypeName(nca_ctx->fs_contexts[i].section_type)); + } + } + + printf("\n"); + consoleUpdate(NULL); - u64 btn = 0; - - while(true) + u64 btn_down = 0, btn_held = 0; + while(!btn_down && !btn_held) { - btn = utilsReadInput(UtilsInputType_Down); - if (btn) break; + hidScanInput(); + btn_down = utilsHidKeysAllDown(); + btn_held = utilsHidKeysAllHeld(); + } + + if (btn_down & KEY_A) + { + bool error = false; - if (titleRefreshGameCardTitleInfo()) + if (menu == 0) { - free(app_metadata); - - app_metadata = titleGetApplicationMetadataEntries(false, &app_count); - if (!app_metadata) + title_idx = selected_idx; + } else + if (menu == 1) + { + nca_idx = selected_idx; + utilsGenerateHexStringFromData(nca_id_str, sizeof(nca_id_str), cur_title_info->content_infos[nca_idx].content_id.c, SHA256_HASH_SIZE / 2); + } + + menu++; + + if (menu == 1) + { + cur_title_info = titleGetInfoFromStorageByTitleId(NcmStorageId_BuiltInSystem, app_metadata[title_idx]->title_id); + if (!cur_title_info) { - consolePrint("\napp metadata failed\n"); - goto out2; + consolePrint("failed to get title info\n"); + error = true; } - - selected_idx = 0; - - break; - } - } - - if (btn & KEY_A) - { - if (!titleGetUserApplicationData(app_metadata[selected_idx]->title_id, &user_app_data) || !user_app_data.app_info || !user_app_data.patch_info) + } else + if (menu == 2) { - consolePrint("\nthe selected title either doesn't have available base content or update content.\n"); - utilsSleep(3); - continue; + if (!ncaInitializeContext(nca_ctx, cur_title_info->storage_id, 0, &(cur_title_info->content_infos[nca_idx]), &tik)) + { + consolePrint("nca initialize ctx failed\n"); + error = true; + } + } else + if (menu == 3) + { + consoleClear(); + dumpFsSection(cur_title_info, &(nca_ctx->fs_contexts[selected_idx])); } - break; - } else - if (btn & KEY_DOWN) - { - if ((selected_idx + 1) < app_count) + if (error || menu >= 3) { - selected_idx++; + utilsSleep(3); + menu--; } else { - selected_idx = 0; + selected_idx = cur_page = 0; } } else - if (btn & KEY_UP) + if ((btn_down & KEY_DDOWN) || (btn_held & (KEY_LSTICK_DOWN | KEY_RSTICK_DOWN))) { - if (selected_idx == 0) + selected_idx++; + + if (selected_idx >= max_val) { - selected_idx = (app_count - 1); - } else { - selected_idx--; + if (btn_down & KEY_DDOWN) + { + selected_idx = cur_page = 0; + } else { + selected_idx = (max_val - 1); + } + } else + if (selected_idx >= (cur_page + page_size)) + { + cur_page += page_size; } } else - if (btn & KEY_B) + if ((btn_down & KEY_DUP) || (btn_held & (KEY_LSTICK_UP | KEY_RSTICK_UP))) { - consolePrint("\nprocess cancelled.\n"); - goto out2; - } - } - - consoleClear(); - consolePrint("selected title:\n%s (%016lX)\n\n", app_metadata[selected_idx]->lang_entry.name, app_metadata[selected_idx]->title_id); - - if (!ncaInitializeContext(base_nca_ctx, user_app_data.app_info->storage_id, (user_app_data.app_info->storage_id == NcmStorageId_GameCard ? GameCardHashFileSystemPartitionType_Secure : 0), \ - titleGetContentInfoByTypeAndIdOffset(user_app_data.app_info, NcmContentType_Program, 0), &base_tik)) - { - consolePrint("nca initialize base ctx failed\n"); - goto out2; - } - - if (!ncaInitializeContext(update_nca_ctx, user_app_data.patch_info->storage_id, (user_app_data.patch_info->storage_id == NcmStorageId_GameCard ? GameCardHashFileSystemPartitionType_Secure : 0), \ - titleGetContentInfoByTypeAndIdOffset(user_app_data.patch_info, NcmContentType_Program, 0), &update_tik)) - { - consolePrint("nca initialize update ctx failed\n"); - goto out2; - } - - if (!bktrInitializeContext(&bktr_ctx, &(base_nca_ctx->fs_contexts[1]), &(update_nca_ctx->fs_contexts[1]))) - { - consolePrint("bktr initialize ctx failed\n"); - goto out2; - } - - consolePrint("bktr initialize ctx succeeded\n"); - - shared_data.bktr_ctx = &bktr_ctx; - shared_data.data = buf; - shared_data.data_size = 0; - shared_data.data_written = 0; - bktrGetTotalDataSize(&bktr_ctx, &(shared_data.total_size)); - - consolePrint("waiting for usb connection... "); - - time_t start = time(NULL); - bool usb_conn = false; - - while(true) - { - time_t now = time(NULL); - if ((now - start) >= 10) break; - consolePrint("%lu ", now - start); - - if ((usb_conn = usbIsReady())) break; - utilsSleep(1); - } - - consolePrint("\n"); - - if (!usb_conn) - { - consolePrint("usb connection failed\n"); - goto out2; - } - - consolePrint("creating threads\n"); - thrd_create(&read_thread, read_thread_func, &shared_data); - thrd_create(&write_thread, write_thread_func, &shared_data); - - u8 prev_time = 0; - u64 prev_size = 0; - u8 percent = 0; - - time_t btn_cancel_start_tmr = 0, btn_cancel_end_tmr = 0; - bool btn_cancel_cur_state = false, btn_cancel_prev_state = false; - - consolePrint("hold b to cancel\n\n"); - - start = time(NULL); - - while(shared_data.data_written < shared_data.total_size) - { - if (shared_data.read_error || shared_data.write_error) break; - - time_t now = time(NULL); - struct tm *ts = localtime(&now); - size_t size = shared_data.data_written; - - btn_cancel_cur_state = (utilsReadInput(UtilsInputType_Down) & KEY_B); - - if (btn_cancel_cur_state && btn_cancel_cur_state != btn_cancel_prev_state) - { - btn_cancel_start_tmr = now; - } else - if (btn_cancel_cur_state && btn_cancel_cur_state == btn_cancel_prev_state) - { - btn_cancel_end_tmr = now; - if ((btn_cancel_end_tmr - btn_cancel_start_tmr) >= 3) + selected_idx--; + + if (selected_idx == UINT32_MAX) + { + if (btn_down & KEY_DUP) + { + selected_idx = (max_val - 1); + cur_page = (max_val - (max_val % page_size)); + } else { + selected_idx = 0; + } + } else + if (selected_idx < cur_page) + { + cur_page -= page_size; + } + } else + if (btn_down & KEY_B) + { + menu--; + + if (menu == UINT32_MAX) { - mutexLock(&g_fileMutex); - shared_data.transfer_cancelled = true; - mutexUnlock(&g_fileMutex); break; + } else { + selected_idx = (menu == 0 ? title_idx : nca_idx); + cur_page = (selected_idx - (selected_idx % page_size)); } - } else { - btn_cancel_start_tmr = btn_cancel_end_tmr = 0; } - btn_cancel_prev_state = btn_cancel_cur_state; - - if (prev_time == ts->tm_sec || prev_size == size) continue; - - percent = (u8)((size * 100) / shared_data.total_size); - - prev_time = ts->tm_sec; - prev_size = size; - - printf("%lu / %lu (%u%%) | Time elapsed: %lu\n", size, shared_data.total_size, percent, (now - start)); - consoleUpdate(NULL); + if (btn_held & (KEY_LSTICK_DOWN | KEY_RSTICK_DOWN | KEY_LSTICK_UP | KEY_RSTICK_UP)) svcSleepThread(50000000); // 50 ms } - start = (time(NULL) - start); - - consolePrint("\nwaiting for threads to join\n"); - thrd_join(read_thread, NULL); - consolePrint("read_thread done: %lu\n", time(NULL)); - thrd_join(write_thread, NULL); - consolePrint("write_thread done: %lu\n", time(NULL)); - - if (shared_data.read_error || shared_data.write_error) - { - consolePrint("usb transfer error\n"); - goto out2; - } - - if (shared_data.transfer_cancelled) - { - consolePrint("process cancelled\n"); - goto out2; - } - - consolePrint("process completed in %lu seconds\n", start); - out2: - consolePrint("press any button to exit\n"); - utilsWaitForButtonPress(KEY_NONE); + if (menu != UINT32_MAX) + { + consolePrint("press any button to exit\n"); + utilsWaitForButtonPress(KEY_NONE); + } - bktrFreeContext(&bktr_ctx); - - if (update_nca_ctx) free(update_nca_ctx); - - if (base_nca_ctx) free(base_nca_ctx); + if (nca_ctx) free(nca_ctx); if (buf) free(buf); diff --git a/source/nca.c b/source/nca.c index d1a40f3..94c1df2 100644 --- a/source/nca.c +++ b/source/nca.c @@ -38,6 +38,14 @@ static const u8 g_nca0KeyAreaHash[SHA256_HASH_SIZE] = { 0xFF, 0x6B, 0x25, 0xEF, 0x9F, 0x96, 0x85, 0x28, 0x18, 0x9E, 0x76, 0xB0, 0x92, 0xF0, 0x6A, 0xCB }; +static const char *g_ncaFsSectionTypeNames[] = { + [NcaFsSectionType_PartitionFs] = "Partition FS", + [NcaFsSectionType_RomFs] = "RomFS", + [NcaFsSectionType_PatchRomFs] = "Patch RomFS [BKTR]", + [NcaFsSectionType_Nca0RomFs] = "NCA0 RomFS", + [NcaFsSectionType_Invalid] = "Invalid" +}; + /* Function prototypes. */ NX_INLINE bool ncaIsFsInfoEntryValid(NcaFsInfo *fs_info); @@ -158,15 +166,21 @@ bool ncaInitializeContext(NcaContext *out, u8 storage_id, u8 hfs_partition_type, /* Parse sections. */ for(u8 i = 0; i < NCA_FS_HEADER_COUNT; i++) { - /* Don't proceed if this NCA FS section isn't populated. */ - if (!ncaIsFsInfoEntryValid(&(out->header.fs_info[i]))) continue; - /* Fill section context. */ out->fs_contexts[i].nca_ctx = out; out->fs_contexts[i].section_num = i; + out->fs_contexts[i].section_type = NcaFsSectionType_Invalid; /* Placeholder. */ + + /* Don't proceed if this NCA FS section isn't populated. */ + if (!ncaIsFsInfoEntryValid(&(out->header.fs_info[i]))) continue; + + /* Calculate section offset and size. */ out->fs_contexts[i].section_offset = NCA_FS_SECTOR_OFFSET(out->header.fs_info[i].start_sector); out->fs_contexts[i].section_size = (NCA_FS_SECTOR_OFFSET(out->header.fs_info[i].end_sector) - out->fs_contexts[i].section_offset); - out->fs_contexts[i].section_type = NcaFsSectionType_Invalid; /* Placeholder. */ + + /* Check if we're dealing with an invalid offset/size. */ + if (out->fs_contexts[i].section_offset < sizeof(NcaHeader) || !out->fs_contexts[i].section_size || \ + (out->fs_contexts[i].section_offset + out->fs_contexts[i].section_size) > out->content_size) continue; /* Determine encryption type. */ out->fs_contexts[i].encryption_type = (out->format_version == NcaVersion_Nca0 ? NcaEncryptionType_AesXts : out->fs_contexts[i].header.encryption_type); @@ -187,11 +201,7 @@ bool ncaInitializeContext(NcaContext *out, u8 storage_id, u8 hfs_partition_type, } /* Check if we're dealing with an invalid encryption type value. */ - if (out->fs_contexts[i].encryption_type == NcaEncryptionType_Auto || out->fs_contexts[i].encryption_type > NcaEncryptionType_AesCtrEx) - { - memset(&(out->fs_contexts[i]), 0, sizeof(NcaFsSectionContext)); - continue; - } + if (out->fs_contexts[i].encryption_type == NcaEncryptionType_Auto || out->fs_contexts[i].encryption_type > NcaEncryptionType_AesCtrEx) continue; /* Determine FS section type. */ if (out->fs_contexts[i].header.fs_type == NcaFsType_PartitionFs && out->fs_contexts[i].header.hash_type == NcaHashType_HierarchicalSha256) @@ -208,11 +218,7 @@ bool ncaInitializeContext(NcaContext *out, u8 storage_id, u8 hfs_partition_type, } /* Check if we're dealing with an invalid section type value. */ - if (out->fs_contexts[i].section_type >= NcaFsSectionType_Invalid) - { - memset(&(out->fs_contexts[i]), 0, sizeof(NcaFsSectionContext)); - continue; - } + if (out->fs_contexts[i].section_type >= NcaFsSectionType_Invalid) continue; /* Initialize crypto data. */ if ((!out->rights_id_available || (out->rights_id_available && out->titlekey_retrieved)) && out->fs_contexts[i].encryption_type > NcaEncryptionType_None && \ @@ -317,6 +323,12 @@ void ncaWriteHierarchicalIntegrityPatchToMemoryBuffer(NcaContext *ctx, NcaHierar for(u32 i = 0; i < NCA_IVFC_LEVEL_COUNT; i++) ncaWriteHashDataPatchToMemoryBuffer(ctx, &(patch->hash_level_patch[i]), buf, buf_size, buf_offset); } +const char *ncaGetFsSectionTypeName(u8 section_type) +{ + u8 idx = (section_type > NcaFsSectionType_Invalid ? NcaFsSectionType_Invalid : section_type); + return g_ncaFsSectionTypeNames[idx]; +} + void ncaRemoveTitlekeyCrypto(NcaContext *ctx) { if (!ctx || ctx->content_size < NCA_FULL_HEADER_LENGTH || !ctx->rights_id_available || !ctx->titlekey_retrieved) return; diff --git a/source/nca.h b/source/nca.h index de31abe..6207a53 100644 --- a/source/nca.h +++ b/source/nca.h @@ -393,7 +393,8 @@ bool ncaGenerateHierarchicalIntegrityPatch(NcaFsSectionContext *ctx, const void /// 'buf_offset' must hold the raw NCA offset where the data stored in 'buf' was read from. void ncaWriteHierarchicalIntegrityPatchToMemoryBuffer(NcaContext *ctx, NcaHierarchicalIntegrityPatch *patch, void *buf, u64 buf_size, u64 buf_offset); - +/// Returns a pointer to a string holding the name of the provided NCA FS section type. +const char *ncaGetFsSectionTypeName(u8 section_type); diff --git a/source/pfs.c b/source/pfs.c index 7eafcdd..9be1633 100644 --- a/source/pfs.c +++ b/source/pfs.c @@ -167,7 +167,7 @@ bool pfsGetEntryIndexByName(PartitionFileSystemContext *ctx, const char *name, u } } - LOGFILE("Unable to find partition FS entry \"%s\"!", name); + //LOGFILE("Unable to find partition FS entry \"%s\"!", name); return false; } diff --git a/source/title.c b/source/title.c index 6382e3a..36be3bb 100644 --- a/source/title.c +++ b/source/title.c @@ -47,6 +47,17 @@ static NcmContentStorage g_ncmStorageGameCard = {0}, g_ncmStorageEmmcSystem = {0 static TitleInfo *g_titleInfo = NULL; static u32 g_titleInfoCount = 0, g_titleInfoGameCardStartIndex = 0, g_titleInfoGameCardCount = 0; +static const char *g_titleNcmContentTypeNames[] = { + [NcmContentType_Meta] = "Meta", + [NcmContentType_Program] = "Program", + [NcmContentType_Data] = "Data", + [NcmContentType_Control] = "Control", + [NcmContentType_HtmlDocument] = "HtmlDocument", + [NcmContentType_LegalInformation] = "LegalInformation", + [NcmContentType_DeltaFragment] = "DeltaFragment", + [NcmContentType_DeltaFragment + 1] = "Unknown" +}; + /* Info retrieved from https://switchbrew.org/wiki/Title_list. */ /* Titles bundled with the kernel are excluded. */ static const SystemTitleName g_systemTitles[] = { @@ -732,7 +743,11 @@ end: return success; } - +const char *titleGetNcmContentTypeName(u8 content_type) +{ + u8 idx = (content_type > NcmContentType_DeltaFragment ? (NcmContentType_DeltaFragment + 1) : content_type); + return g_titleNcmContentTypeNames[idx]; +} @@ -1153,7 +1168,7 @@ static bool titleRetrieveContentMetaKeysFromDatabase(u8 storage_id) /* Get a full list of all titles available in this storage. */ /* Meta type '0' means all title types will be retrieved. */ - rc = ncmContentMetaDatabaseList(ncm_db, (s32*)&total, (s32*)&written, meta_keys, 1, 0, 0, 0, -1, NcmContentInstallType_Full); + rc = ncmContentMetaDatabaseList(ncm_db, (s32*)&total, (s32*)&written, meta_keys, 1, 0, 0, 0, UINT64_MAX, NcmContentInstallType_Full); if (R_FAILED(rc)) { LOGFILE("ncmContentMetaDatabaseList failed! (0x%08X) (first entry).", rc); @@ -1186,7 +1201,7 @@ static bool titleRetrieveContentMetaKeysFromDatabase(u8 storage_id) meta_keys_tmp = NULL; /* Issue call again. */ - rc = ncmContentMetaDatabaseList(ncm_db, (s32*)&total, (s32*)&written, meta_keys, (s32)total, 0, 0, 0, -1, NcmContentInstallType_Full); + rc = ncmContentMetaDatabaseList(ncm_db, (s32*)&total, (s32*)&written, meta_keys, (s32)total, 0, 0, 0, UINT64_MAX, NcmContentInstallType_Full); if (R_FAILED(rc)) { LOGFILE("ncmContentMetaDatabaseList failed! (0x%08X) (%u %s).", rc, total, total > 1 ? "entries" : "entry"); diff --git a/source/title.h b/source/title.h index 2ebed3a..02261e0 100644 --- a/source/title.h +++ b/source/title.h @@ -87,12 +87,12 @@ NcmContentStorage *titleGetNcmStorageByStorageId(u8 storage_id); bool titleRefreshGameCardTitleInfo(void); /// Returns a pointer to a dynamically allocated buffer of pointers to TitleApplicationMetadata entries, as well as their count. The allocated buffer must be freed by the calling function. -/// If 'is_system' is true, TitleApplicationMetadata entries from available system titles will be returned. -/// Otherwise, TitleApplicationMetadata entries from user applications with available content data will be returned. +/// If 'is_system' is true, TitleApplicationMetadata entries from available system titles (NcmStorageId_BuiltInSystem) will be returned. +/// Otherwise, TitleApplicationMetadata entries from user applications with available content data (NcmStorageId_Any) will be returned. /// Returns NULL if an error occurs. TitleApplicationMetadata **titleGetApplicationMetadataEntries(bool is_system, u32 *out_count); -/// Retrieves a pointer to a TitleInfo entry with a matching storage ID and title ID. +/// Returns a pointer to a TitleInfo entry with a matching storage ID and title ID. /// If NcmStorageId_Any is used, the first entry with a matching title ID is returned. /// Returns NULL if an error occurs. TitleInfo *titleGetInfoFromStorageByTitleId(u8 storage_id, u64 title_id); @@ -100,6 +100,16 @@ TitleInfo *titleGetInfoFromStorageByTitleId(u8 storage_id, u64 title_id); /// Populates a TitleUserApplicationData element using an user application ID. bool titleGetUserApplicationData(u64 app_id, TitleUserApplicationData *out); +/// Returns a pointer to a string holding the name of the provided ncm content type. +const char *titleGetNcmContentTypeName(u8 content_type); + + + + + + + + diff --git a/source/usb.c b/source/usb.c index cf6ba93..23d6072 100644 --- a/source/usb.c +++ b/source/usb.c @@ -29,7 +29,7 @@ #define USB_CMD_HEADER_MAGIC 0x4E584454 /* "NXDT". */ #define USB_TRANSFER_ALIGNMENT 0x1000 /* 4 KiB. */ -#define USB_TRANSFER_TIMEOUT 3 /* 3 seconds. */ +#define USB_TRANSFER_TIMEOUT 6 /* 6 seconds. */ /* Type definitions. */ diff --git a/source/utils.c b/source/utils.c index f8263b9..9d47542 100644 --- a/source/utils.c +++ b/source/utils.c @@ -57,9 +57,6 @@ static const u32 g_sizeSuffixesCount = MAX_ELEMENTS(g_sizeSuffixes); /* Function prototypes. */ -static u64 utilsHidKeysAllDown(void); -static u64 utilsHidKeysAllHeld(void); - static bool utilsMountEmmcBisSystemPartitionStorage(void); static void utilsUnmountEmmcBisSystemPartitionStorage(void); @@ -200,13 +197,24 @@ void utilsCloseResources(void) mutexUnlock(&g_resourcesMutex); } -u64 utilsReadInput(u8 input_type) +u64 utilsHidKeysAllDown(void) { - if (input_type != UtilsInputType_Down && input_type != UtilsInputType_Held) return 0; + u8 controller; + u64 keys_down = 0; - hidScanInput(); + for(controller = 0; controller < (u8)CONTROLLER_P1_AUTO; controller++) keys_down |= hidKeysDown((HidControllerID)controller); - return (input_type == UtilsInputType_Down ? utilsHidKeysAllDown() : utilsHidKeysAllHeld()); + return keys_down; +} + +u64 utilsHidKeysAllHeld(void) +{ + u8 controller; + u64 keys_held = 0; + + for(controller = 0; controller < (u8)CONTROLLER_P1_AUTO; controller++) keys_held |= hidKeysHeld((HidControllerID)controller); + + return keys_held; } void utilsWaitForButtonPress(u64 flag) @@ -221,7 +229,8 @@ void utilsWaitForButtonPress(u64 flag) while(appletMainLoop()) { - keys_down = utilsReadInput(UtilsInputType_Down); + hidScanInput(); + keys_down = utilsHidKeysAllDown(); if (keys_down & flag) break; } } @@ -434,6 +443,29 @@ bool utilsCreateConcatenationFile(const char *path) return true; } +void utilsCreateDirectoryTree(const char *path, bool create_last_element) +{ + char *ptr = NULL, *tmp = NULL; + size_t path_len = 0; + + if (!path || !(path_len = strlen(path))) return; + + tmp = calloc(path_len + 1, sizeof(char)); + if (!tmp) return; + + ptr = strchr(path, '/'); + while(ptr) + { + sprintf(tmp, "%.*s", (int)(ptr - path), path); + mkdir(tmp, 0777); + ptr = strchr(++ptr, '/'); + } + + if (create_last_element) mkdir(path, 0777); + + free(tmp); +} + bool utilsAppletModeCheck(void) { return (g_programAppletType != AppletType_Application && g_programAppletType != AppletType_SystemApplication); @@ -476,26 +508,6 @@ void utilsOverclockSystem(bool overclock) servicesChangeHardwareClockRates(cpuClkRate, memClkRate); } -static u64 utilsHidKeysAllDown(void) -{ - u8 controller; - u64 keys_down = 0; - - for(controller = 0; controller < (u8)CONTROLLER_P1_AUTO; controller++) keys_down |= hidKeysDown((HidControllerID)controller); - - return keys_down; -} - -static u64 utilsHidKeysAllHeld(void) -{ - u8 controller; - u64 keys_held = 0; - - for(controller = 0; controller < (u8)CONTROLLER_P1_AUTO; controller++) keys_held |= hidKeysHeld((HidControllerID)controller); - - return keys_held; -} - static bool utilsMountEmmcBisSystemPartitionStorage(void) { Result rc = 0; diff --git a/source/utils.h b/source/utils.h index ac106ef..ecc2a5d 100644 --- a/source/utils.h +++ b/source/utils.h @@ -66,12 +66,6 @@ - -typedef enum { - UtilsInputType_Down = 0, - UtilsInputType_Held = 1 -} UtilsInputType; - typedef enum { UtilsCustomFirmwareType_Unknown = 0, UtilsCustomFirmwareType_Atmosphere = 1, @@ -82,7 +76,10 @@ typedef enum { bool utilsInitializeResources(void); void utilsCloseResources(void); -u64 utilsReadInput(u8 input_type); +/// hidScanInput() must be called before any of these functions. +u64 utilsHidKeysAllDown(void); +u64 utilsHidKeysAllHeld(void); + void utilsWaitForButtonPress(u64 flag); void utilsWriteMessageToLogFile(const char *func_name, const char *fmt, ...); @@ -105,6 +102,8 @@ bool utilsCheckIfFileExists(const char *path); bool utilsCreateConcatenationFile(const char *path); +void utilsCreateDirectoryTree(const char *path, bool create_last_element); + bool utilsAppletModeCheck(void); void utilsChangeHomeButtonBlockStatus(bool block);