From aab7e24778264c46e5ffcb0c9a5deca275f28251 Mon Sep 17 00:00:00 2001 From: Pablo Curiel Date: Wed, 5 Jun 2019 18:44:18 -0400 Subject: [PATCH] Update to v1.1.0. --- Makefile | 15 +- README.md | 76 +- icon.jpg | Bin 15764 -> 32465 bytes romfs/dir_highlight.jpg | Bin 0 -> 629 bytes romfs/dir_normal.jpg | Bin 0 -> 434 bytes romfs/file_highlight.jpg | Bin 0 -> 630 bytes romfs/file_normal.jpg | Bin 0 -> 435 bytes source/aes.c | 325 ---- source/aes.h | 42 - source/dumper.c | 3080 +++++++++++++++++++++++----------- source/dumper.h | 15 +- source/extkeys.c | 250 --- source/extkeys.h | 18 - source/{fsext.c => fs_ext.c} | 2 +- source/{fsext.h => fs_ext.h} | 4 +- source/keys.c | 400 +++++ source/keys.h | 44 + source/main.c | 334 ++-- source/nca.c | 2014 ++++++++++++++++++++++ source/nca.h | 362 ++++ source/rsa.c | 83 + source/rsa.h | 11 + source/rsa_keys.h | 55 + source/ui.c | 1939 ++++++++++++++++----- source/ui.h | 88 +- source/util.c | 1806 ++++++++++++-------- source/util.h | 233 +-- 27 files changed, 8091 insertions(+), 3105 deletions(-) create mode 100644 romfs/dir_highlight.jpg create mode 100644 romfs/dir_normal.jpg create mode 100644 romfs/file_highlight.jpg create mode 100644 romfs/file_normal.jpg delete mode 100644 source/aes.c delete mode 100644 source/aes.h delete mode 100644 source/extkeys.c delete mode 100644 source/extkeys.h rename source/{fsext.c => fs_ext.c} (99%) rename source/{fsext.h => fs_ext.h} (90%) create mode 100644 source/keys.c create mode 100644 source/keys.h create mode 100644 source/nca.c create mode 100644 source/nca.h create mode 100644 source/rsa.c create mode 100644 source/rsa.h create mode 100644 source/rsa_keys.h diff --git a/Makefile b/Makefile index e0ebc00..61f79af 100644 --- a/Makefile +++ b/Makefile @@ -32,8 +32,8 @@ include $(DEVKITPRO)/libnx/switch_rules #--------------------------------------------------------------------------------- VERSION_MAJOR := 1 -VERSION_MINOR := 0 -VERSION_MICRO := 8 +VERSION_MINOR := 1 +VERSION_MICRO := 0 APP_TITLE := gcdumptool APP_AUTHOR := MCMrARM, DarkMatterCore @@ -45,28 +45,29 @@ SOURCES := source DATA := data INCLUDES := include EXEFS_SRC := exefs_src -#ROMFS := romfs +ROMFS := romfs #--------------------------------------------------------------------------------- # options for code generation #--------------------------------------------------------------------------------- -ARCH := -march=armv8-a -mtune=cortex-a57 -mtp=soft -fPIE +ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE CFLAGS := -g -Wall -O2 -ffunction-sections \ $(ARCH) $(DEFINES) -CFLAGS += $(INCLUDE) -D__SWITCH__ -D__LINUX_ERRNO_EXTENSIONS__ +CFLAGS += $(INCLUDE) -D__SWITCH__ -D__LINUX_ERRNO_EXTENSIONS__ -DAPP_VERSION=\"${APP_VERSION}\" CFLAGS += `freetype-config --cflags` CFLAGS += `aarch64-none-elf-pkg-config zlib --cflags` CFLAGS += `aarch64-none-elf-pkg-config libxml-2.0 --cflags` CFLAGS += `aarch64-none-elf-pkg-config json-c --cflags` +CFLAGS += `aarch64-none-elf-pkg-config libturbojpeg --cflags` -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++11 +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions ASFLAGS := -g $(ARCH) LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) -LIBS := -lcurl -lmbedtls -lmbedx509 -lmbedcrypto -lxml2 -lz -lnx -ljson-c -lm `freetype-config --libs` +LIBS := -lcurl -lmbedtls -lmbedx509 -lmbedcrypto -lxml2 -lz -lnx -ljson-c -lm `freetype-config --libs` -lturbojpeg #--------------------------------------------------------------------------------- # list of directories containing libraries, this must be the top level containing diff --git a/README.md b/README.md index b173391..eb7245b 100644 --- a/README.md +++ b/README.md @@ -4,37 +4,95 @@ Nintendo Switch Game Card Dump Tool Main features -------------- -* Generates full cartridge image dumps (XCI) with optional certificate removal and optional trimming. -* Generates installable packages (NSP) from cartridge applications. - - You'll need to retrieve the full NCA keyset beforehand, using Lockpick. It must be stored in "sdmc:/switch/prod.keys". -* Supports multigame carts. +* Generates full Cartridge Image dumps (XCI) with optional certificate removal and optional trimming. +* Generates installable Nintendo Submission Packages (NSP) from base applications, updates and DLCs stored in the inserted game card. +* Compatible with multigame carts. * CRC32 checksum calculation for XCI/NSP dumps. * Full XCI dump verification using XML database from NSWDB.COM (NSWreleases.xml). * XML database and in-app update via libcurl. * Precise HFS0 raw partition dumping, using the root HFS0 header from the game card. -* Partition filesystem data dumping. -* Partition filesystem browser with manual file dump support. +* HFS0 partition file data dumping. +* HFS0 partition file browser with manual file dump support. +* RomFS section file data dumping. +* RomFS section file browser with manual file dump support. * Manual game card certificate dump. * Free SD card space checks in place. * File splitting support for all operations. * Game card metadata retrieval using NCM and NS services. -* Dump speed, ETA calculation and progress bar. +* Dump speed calculation, ETA calculation and progress bar. Thanks to -------------- * MCMrARM, for creating the original application. -* RSDuck, for their vba-next-switch port. It's UI menu code was taken as a basis for this application. +* RSDuck, for vba-next-switch port. It's UI menu code was taken as a basis for this application. * Foen, for giving me some pretty good hints about how to use the NCM service. * Yellows8, for helping me fix a silly bug in my implementation of some NCM service IPC calls. -* SciresM, for hactool. It's AES cipher handling and external keys file parsing code is used during the NSP dump process. +* SciresM, for hactool. It's NCA content handling procedure is reproduced during the NSP dump process. +* The-4n, for 4NXCI and hacPack. The NCA content patching procedure used in 4NXCI is replicated in the application, as well as the NACP XML generation from hacPack. +* shchmue, for Lockpick. It was used as a reference for the key-collection algorithm needed for the NSP dump and RomFS dump/browse procedures. * Björn Samuelsson, for his public domain CRC32 checksum calculation code for C (crc32_fast.c). * AnalogMan, for his constant support and ideas. +* RattletraPM, for the awesome icon used in the application. +* The GNOME project, from which the high contrast directory/file icons for the filebrowser modes were retrieved. * The folks from ReSwitched, for working towards the creation of a good homebrew ecosystem. Changelog -------------- +**v1.1.0:** +* Replaced the application icon with a new, stylish one made by RattletraPM. Thanks a lot! +* Gamecard base application icons are now retrieved and displayed in the menu. +* L/ZL/R/ZR buttons can now be used to change the displayed base application info if a multigame cart is inserted, instead of displaying everything right away. +* The Nintendo Extension shared font is now used to display bitmaps representing controller buttons and sticks instead of just using text to reference them. +* Replaced the mbedtls-based AES and SHA-256 implementations with functions from the hardware accelerated cryptography API from libnx. +* Added an option to generate split XCI dumps using a directory with the archive bit set, just like split NSP dumps. It will only appear if "Split output dump" is enabled. +* Fixed ETA calculation. +* Enabled ETA calculation in full HFS0 partition data dumps. +* Fixed CRC32 checksum calculation for gamecard certificate dumps. +* Added Program NCA RomFS section parser: + - Supports filesystem dumping, filesystem browsing, manual file dumping and file splitting. Enjoy datamining your gamecards! + - Compatible with multigame carts. You'll be able to choose which base application RomFS will be dumped/browsed from a submenu. + - Output files will be saved to: "sdmc:/[GameName] v[GameVersion] ([TitleID]) (RomFS)/". +* Added high contrast directory/file icons from GNOME project to file browsing modes (HFS0 / RomFS). +* Fixed the NSP generation code (based on 4NXCI / hacPack): + - Delta Fragment NCAs are now discarded. + - The SHA-256 checksum is recalculated for every NCA content after being modified, resulting in new NCA IDs. + - The ACID public key is replaced in the NPDM section from the Program NCA. All the related NCA/PFS0 Superblock SHA-256 hashes are recalculated. + - The NPDM signature in the Program NCA header is now replaced as well. + - The content records from the Application CNMT are updated with proper SHA-256 hashes and new NCA IDs. All the related NCA/PFS0 Superblock hashes are recalculated. + - NACP XMLs are now generated as well. + - Because of all these changes, the CRC32 checksum can't be calculated until the dump procedure is complete. + - If this option is enabled, the application will take extra time after the NSP dump has been completed to calculate the CRC32 checksum. Nonetheless, you'll be able to cancel this procedure. + - A warning message will appear in the NSP dump menu if CRC32 checksum calculation is enabled to inform the user about this extra step. + - Furthermore, the output CRC32 checksum will be different on each new dump. This is because the NPDM signature in the Program NCA header uses a random seed. + - This effectively makes the generated NSPs only need ES patches to work. ACID patches shouldn't be needed anymore. +* Added NSP dumping support for Patch and AddOnContent title types with gamecards that include bundled Updates/DLCs: + - The information displayed in the main menu now shows how many Updates/DLCs are bundled in the inserted gamecard (per application and in total). + - If a bundled gamecard update features a populated Rights ID bitfield, both its Ticket and Certificate will get added to the output NSP. + - Additionally, the NSP dump menu has been divided in three subcategories: base application, update and DLC. + - Each submenu will only appear if the inserted gamecard holds at least one title belonging to the category it represents. + - If only the base application is included, like most gamecards, choosing the NSP dump option in the main menu will take you right to the base application dump menu. + - Once you enter a submenu, you'll be able to choose exactly which title to dump belonging to that category. + - Output update NSPs will not be modified in any way. Thus, unlike NSPs from base applications and DLCs, their CRC32 checksums will always be the same. +* Fixed the minimum system version field size in the extended CNMT header struct. Thanks to @0Liam ! +* Changed the naming convention for output NSP dumps: + - Base application: "sdmc:/[GameName] v[GameVersion] ([TitleID]) (BASE).nsp". + - Update: "sdmc:/[GameName] v[UpdateVersion] ([UpdateTitleID]) (UPD).nsp". + - If a matching base application isn't found: "sdmc:/[UpdateTitleID] v[UpdateVersion] (UPD).nsp". + - DLC: "sdmc:/[GameName] v[DLCVersion] ([DLCTitleID]) (DLC).nsp". + - If a matching base application isn't found: "sdmc:/[DLCTitleID] v[DLCVersion] (DLC).nsp". +* The application is now able to retrieve the NCA header key and perform NCA key area decryption at runtime, using the SPL services. Thus, is isn't needed to run Lockpick beforehand anymore to dump NSPs (nor to dump/browse RomFS data). +* If the inserted gamecard includes a bundled update, its version number will now be used in the output filename for XCI, HFS0 and gamecard certificate dumps. +* Minor improvements to the file splitting code. + - Additionally, the filename for the current part will now be displayed and updated for all operations if file splitting is enabled. +* The application update feature will now use the launch path from argv if it's available. Otherwise, it defaults to "sdmc:/switch/gcdumptool.nro". +* Cosmetic fixes to the UI layout. +* NCM service resources are now properly closed. +* Removed unnecessary service (de)initializations. + +Big thanks to PatrickD85, unvaluablespace, wartutor and Slim45 for testing these changes! + **v1.0.8:** * Added proper metadata reading from multigame carts. diff --git a/icon.jpg b/icon.jpg index e945adae90fd909e1c9794eeacf07e37f1a2c0db..23195fe83c80d1498dd73f27b7e3a0ec38a8652a 100644 GIT binary patch literal 32465 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 V@&Dw7zVCnch5jF=$o}8!{{o)}*53dC literal 15764 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 diff --git a/romfs/dir_highlight.jpg b/romfs/dir_highlight.jpg new file mode 100644 index 0000000000000000000000000000000000000000..88d16ed8de9157358087bd64721ba100566d3c2b GIT binary patch literal 629 zcmex=O1eh5G8JGka znFSgDA7Kz@U|?osKmly*oInW$0R~1^24*B#7G_p9b`C}+PEj!xai9t@kdsh!GB7a% z#nl8E7@3&aSrKxK41!FGfy_c8K?@gMWKnWVyj1xA76T8^VkSXmK?Zw<;!U=R7ee!1 zT)BB~&)2FoCokTquCR8~(vi(e&N&(Ulrxmm*MC7E+bqTQOa-1v)p0vLwTonTraO4m z9$0ZWfF%E$Sr=LE@+&1C93URfs_l= z+o1EmV*l=%J>g=C`(n#@wl}=Tmwgp2d|k^Z*B&pn`tOX(aqcxR!jMe=`!g3P|CMNOMdu{HDOPA;+ctdn|C;XdbPJlo^H$7h;~_5%%4 zY!CRhn03Zm`Ie`D?}W_!{aR*CfzI1IcF)z!HvV%xykOCkLiK+PR}_x8*p>wImdV{# Qdr>#(kLiUE`~Tkr09p*W?EnA( literal 0 HcmV?d00001 diff --git a/romfs/dir_normal.jpg b/romfs/dir_normal.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ac9be2542351be14c0e73bf45a6355b3239cde6b GIT binary patch literal 434 zcmex=_mlt4QWMyDtM#!-+v$C;sFmW=9imCj+#lQj5X3vnF`t9VxY}u=;jPKUpW{sT^ zQN;S>qx8xRz59DV>jqDGy2aMozVXPc7t3r{OfZW4Tk}$F9cTHIOB1#-cNT7(DZ`Rv zY0{GYtm8%Z`oLY~4h~xEG436Y-tXFV$8WV=Jk zny zb9qPWqz!-VRs2ln9zQHrRgleT$=7e^$&QRcwC&XyBb{o Ge-i*9i;_10 literal 0 HcmV?d00001 diff --git a/romfs/file_highlight.jpg b/romfs/file_highlight.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0f51d1db8a5b5bd5e705be08923bfa965774e039 GIT binary patch literal 630 zcmex=O1eh5G8JGka znFSgDA7Ky!IgSAturqK1#T5h?7+FD5%rIG2W)?Pf4kkuUVG%KLkWP?`kTo&@{R|XW z7GwmnSXr3iazJrrMMLAlB0fg@**`u06JQ}bfqMfdh40_)Virbf$?d35V=8PJ~b1bI0ClHq-pjFx~DWs)v_wMoa0< zH#3=Bod3ghmG0sFu7@~QJ-^PWdOJSm>{;(QPv2Zh$*EZV(zmrXd&2uSbsNGCpS^l4 z|B!t+M?~>nC3~wDrsZMlbf>r|DKCB*@=Nc5naAYj9BbE0ddwSKcX6vcKhCxE@cQWN Ss%bxu-&phH&5!l}Zvp_`l*L5= literal 0 HcmV?d00001 diff --git a/romfs/file_normal.jpg b/romfs/file_normal.jpg new file mode 100644 index 0000000000000000000000000000000000000000..20c58d329d34da0048e1d1a184dea98f5dee5b45 GIT binary patch literal 435 zcmex=4;QMa2K#V&DL2vuC)vf6kVx&Fk+@ZaMMI?BcQo zmEQ$wmP*GsZeDx;&pwUQv-Kt1mj-=b;`&q4{2`Nz??%t-@4hVZDn6g3di{z2kxG}( zukw7;89GjWZtL2t^EN%&xA@r3%NFOJZGUpOCu`5z1x9n#Ze7~&`cPO>;5MB%XRg<| zHT6ANl5*jluRDKFAj^)rk5-@3!&m%yyJ7vB-b2&uEwmF~NSlAroS`{cbm`MJK0 z{;O6?9aj>1c!_0guekqgDbJnT|0rD*dw74*Lzd9u*DaIYu3wkGC`|P0x(dJJuUT_! z?#873asKMi+HQSG`SS_~t9jCAyd_rYfi+9?f=zxk1*>AL JzHa}2699v4o*MuF literal 0 HcmV?d00001 diff --git a/source/aes.c b/source/aes.c deleted file mode 100644 index 42edecf..0000000 --- a/source/aes.c +++ /dev/null @@ -1,325 +0,0 @@ -#include -#include -#include - -#include "aes.h" -#include "ui.h" -#include "util.h" - -/* Extern variables */ - -extern int breaks; -extern int font_height; -extern char strbuf[NAME_BUF_LEN * 4]; - -/* Allocate a new context. */ -aes_ctx_t *new_aes_ctx(const void *key, unsigned int key_size, aes_mode_t mode) -{ - int ret; - aes_ctx_t *ctx; - - if ((ctx = malloc(sizeof(*ctx))) == NULL) - { - uiDrawString("Error: failed to allocate aes_ctx_t!", 0, breaks * font_height, 255, 0, 0); - return NULL; - } - - mbedtls_cipher_init(&ctx->cipher_dec); - mbedtls_cipher_init(&ctx->cipher_enc); - - ret = mbedtls_cipher_setup(&ctx->cipher_dec, mbedtls_cipher_info_from_type(mode)); - if (ret) - { - free_aes_ctx(ctx); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to set up AES decryption context! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return NULL; - } - - ret = mbedtls_cipher_setup(&ctx->cipher_enc, mbedtls_cipher_info_from_type(mode)); - if (ret) - { - free_aes_ctx(ctx); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to set up AES encryption context! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return NULL; - } - - ret = mbedtls_cipher_setkey(&ctx->cipher_dec, key, key_size * 8, AES_DECRYPT); - if (ret) - { - free_aes_ctx(ctx); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to set key for AES decryption context! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return NULL; - } - - ret = mbedtls_cipher_setkey(&ctx->cipher_enc, key, key_size * 8, AES_ENCRYPT); - if (ret) - { - free_aes_ctx(ctx); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to set key for AES encryption context! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return NULL; - } - - return ctx; -} - -/* Free an allocated context. */ -void free_aes_ctx(aes_ctx_t *ctx) -{ - /* Explicitly allow NULL. */ - if (ctx == NULL) return; - mbedtls_cipher_free(&ctx->cipher_dec); - mbedtls_cipher_free(&ctx->cipher_enc); - free(ctx); -} - -/* Set AES CTR or IV for a context. */ -int aes_setiv(aes_ctx_t *ctx, const void *iv, size_t l) -{ - int ret; - - ret = mbedtls_cipher_set_iv(&ctx->cipher_dec, iv, l); - if (ret) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to set IV for AES decryption context! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return 0; - } - - ret = mbedtls_cipher_set_iv(&ctx->cipher_enc, iv, l); - if (ret) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to set IV for AES encryption context! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return 0; - } - - return 1; -} - -/* Calculate CMAC. */ -int aes_calculate_cmac(void *dst, void *src, size_t size, const void *key) -{ - int ret; - mbedtls_cipher_context_t m_ctx; - - mbedtls_cipher_init(&m_ctx); - - ret = mbedtls_cipher_setup(&m_ctx, mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_AES_128_ECB)); - if (ret) - { - mbedtls_cipher_free(&m_ctx); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to set up AES context for CMAC calculation! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return 0; - } - - ret = mbedtls_cipher_cmac_starts(&m_ctx, key, 0x80); - if (ret) - { - mbedtls_cipher_free(&m_ctx); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to start CMAC calculation! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return 0; - } - - ret = mbedtls_cipher_cmac_update(&m_ctx, src, size); - if (ret) - { - mbedtls_cipher_free(&m_ctx); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to update CMAC calculation! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return 0; - } - - ret = mbedtls_cipher_cmac_finish(&m_ctx, dst); - if (ret) - { - mbedtls_cipher_free(&m_ctx); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to finish CMAC calculation! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return 0; - } - - mbedtls_cipher_free(&m_ctx); - - return 1; -} - - -/* Encrypt with context. */ -int aes_encrypt(aes_ctx_t *ctx, void *dst, const void *src, size_t l) -{ - int ret; - size_t out_len = 0; - - /* Prepare context */ - mbedtls_cipher_reset(&ctx->cipher_enc); - - /* XTS doesn't need per-block updating */ - if (mbedtls_cipher_get_cipher_mode(&ctx->cipher_enc) == MBEDTLS_MODE_XTS) - { - ret = mbedtls_cipher_update(&ctx->cipher_enc, (const unsigned char *)src, l, (unsigned char *)dst, &out_len); - } else { - unsigned int blk_size = mbedtls_cipher_get_block_size(&ctx->cipher_enc); - - /* Do per-block updating */ - for (int offset = 0; (unsigned int)offset < l; offset += blk_size) - { - int len = (((unsigned int)(l - offset) > blk_size) ? blk_size : (unsigned int)(l - offset)); - ret = mbedtls_cipher_update(&ctx->cipher_enc, (const unsigned char *)src + offset, len, (unsigned char *)dst + offset, &out_len); - if (ret) break; - } - } - - if (ret) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: AES encryption failed! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return 0; - } - - /* Flush all data */ - size_t strbuf_size = sizeof(strbuf); - ret = mbedtls_cipher_finish(&ctx->cipher_enc, (unsigned char *)strbuf, &strbuf_size); // Looks ugly, but using NULL,NULL with mbedtls on Switch is a no-no - if (ret) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to finalize cipher for AES encryption! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return 0; - } - - return 1; -} - -/* Decrypt with context. */ -int aes_decrypt(aes_ctx_t *ctx, void *dst, const void *src, size_t l) -{ - int ret; - bool src_equals_dst = false; - - if (src == dst) - { - src_equals_dst = true; - - dst = malloc(l); - if (dst == NULL) - { - uiDrawString("Error: failed to allocate buffer for AES decryption!", 0, breaks * font_height, 255, 0, 0); - return 0; - } - } - - size_t out_len = 0; - - /* Prepare context */ - mbedtls_cipher_reset(&ctx->cipher_dec); - - /* XTS doesn't need per-block updating */ - if (mbedtls_cipher_get_cipher_mode(&ctx->cipher_dec) == MBEDTLS_MODE_XTS) - { - ret = mbedtls_cipher_update(&ctx->cipher_dec, (const unsigned char *)src, l, (unsigned char *)dst, &out_len); - } else { - unsigned int blk_size = mbedtls_cipher_get_block_size(&ctx->cipher_dec); - - /* Do per-block updating */ - for (int offset = 0; (unsigned int)offset < l; offset += blk_size) - { - int len = (((unsigned int)(l - offset) > blk_size) ? blk_size : (unsigned int)(l - offset)); - ret = mbedtls_cipher_update(&ctx->cipher_dec, (const unsigned char *)src + offset, len, (unsigned char *)dst + offset, &out_len); - if (ret) break; - } - } - - if (ret) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: AES decryption failed! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return 0; - } - - /* Flush all data */ - size_t strbuf_size = sizeof(strbuf); - ret = mbedtls_cipher_finish(&ctx->cipher_dec, (unsigned char *)strbuf, &strbuf_size); // Looks ugly, but using NULL,NULL with mbedtls on Switch is a no-no - if (ret) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to finalize cipher for AES decryption! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return 0; - } - - if (src_equals_dst) - { - memcpy((void*)src, dst, l); - free(dst); - } - - return 1; -} - -static void get_tweak(unsigned char *tweak, size_t sector) -{ - /* Nintendo LE custom tweak... */ - for (int i = 0xF; i >= 0; i--) - { - tweak[i] = (unsigned char)(sector & 0xFF); - sector >>= 8; - } -} - -/* Encrypt with context for XTS. */ -int aes_xts_encrypt(aes_ctx_t *ctx, void *dst, const void *src, size_t l, size_t sector, size_t sector_size) -{ - int ret = 0; - unsigned char tweak[0x10]; - - if ((l % sector_size) != 0) - { - uiDrawString("Error: length must be a multiple of sector size in AES-XTS.", 0, breaks * font_height, 255, 0, 0); - return 0; - } - - for (size_t i = 0; i < l; i += sector_size) - { - /* Workaround for Nintendo's custom sector...manually generate the tweak. */ - get_tweak(tweak, sector++); - - ret = aes_setiv(ctx, tweak, 16); - if (!ret) break; - - ret = aes_encrypt(ctx, (char *)dst + i, (const char *)src + i, sector_size); - if (!ret) break; - } - - return ret; -} - -/* Decrypt with context for XTS. */ -int aes_xts_decrypt(aes_ctx_t *ctx, void *dst, const void *src, size_t l, size_t sector, size_t sector_size) -{ - int ret = 0; - unsigned char tweak[0x10]; - - if ((l % sector_size) != 0) - { - uiDrawString("Error: length must be a multiple of sector size in AES-XTS.", 0, breaks * font_height, 255, 0, 0); - return 0; - } - - for (size_t i = 0; i < l; i += sector_size) - { - /* Workaround for Nintendo's custom sector...manually generate the tweak. */ - get_tweak(tweak, sector++); - - ret = aes_setiv(ctx, tweak, 16); - if (!ret) break; - - ret = aes_decrypt(ctx, (char *)dst + i, (const char *)src + i, sector_size); - if (!ret) break; - } - - return ret; -} diff --git a/source/aes.h b/source/aes.h deleted file mode 100644 index ee6e0d1..0000000 --- a/source/aes.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#ifndef __AES_H__ -#define __AES_H__ - -#include -#include -#include - -/* Enumerations. */ -typedef enum { - AES_MODE_ECB = MBEDTLS_CIPHER_AES_128_ECB, - AES_MODE_CTR = MBEDTLS_CIPHER_AES_128_CTR, - AES_MODE_XTS = MBEDTLS_CIPHER_AES_128_XTS -} aes_mode_t; - -typedef enum { - AES_DECRYPT = MBEDTLS_DECRYPT, - AES_ENCRYPT = MBEDTLS_ENCRYPT, -} aes_operation_t; - -/* Define structs. */ -typedef struct { - mbedtls_cipher_context_t cipher_enc; - mbedtls_cipher_context_t cipher_dec; -} aes_ctx_t; - -/* Function prototypes. */ -aes_ctx_t *new_aes_ctx(const void *key, unsigned int key_size, aes_mode_t mode); -void free_aes_ctx(aes_ctx_t *ctx); - -int aes_setiv(aes_ctx_t *ctx, const void *iv, size_t l); - -int aes_encrypt(aes_ctx_t *ctx, void *dst, const void *src, size_t l); -int aes_decrypt(aes_ctx_t *ctx, void *dst, const void *src, size_t l); - -int aes_calculate_cmac(void *dst, void *src, size_t size, const void *key); - -int aes_xts_encrypt(aes_ctx_t *ctx, void *dst, const void *src, size_t l, size_t sector, size_t sector_size); -int aes_xts_decrypt(aes_ctx_t *ctx, void *dst, const void *src, size_t l, size_t sector, size_t sector_size); - -#endif diff --git a/source/dumper.c b/source/dumper.c index 6dbc4ac..7402114 100644 --- a/source/dumper.c +++ b/source/dumper.c @@ -1,5 +1,5 @@ #include -#include +#include #include #include #include @@ -10,9 +10,9 @@ #include "crc32_fast.h" #include "dumper.h" -#include "fsext.h" +#include "fs_ext.h" #include "ui.h" -#include "util.h" +#include "nca.h" /* Extern variables */ @@ -24,11 +24,11 @@ extern int font_height; extern u64 trimmedCardSize; extern char trimmedCardSizeStr[32]; -extern char *hfs0_header; +extern u8 *hfs0_header; extern u64 hfs0_offset, hfs0_size; extern u32 hfs0_partition_cnt; -extern char *partitionHfs0Header; +extern u8 *partitionHfs0Header; extern u64 partitionHfs0HeaderOffset, partitionHfs0HeaderSize; extern u32 partitionHfs0FileCount, partitionHfs0StrTableSize; @@ -37,8 +37,16 @@ extern u64 *gameCardTitleID; extern u32 *gameCardVersion; extern char **fixedGameCardName; +extern u32 gameCardPatchCount; + +extern u32 gameCardAddOnCount; + extern AppletType programAppletType; +extern romfs_ctx_t romFsContext; + +extern char curRomFsPath[NAME_BUF_LEN]; + extern char strbuf[NAME_BUF_LEN * 4]; /* Statically allocated variables */ @@ -54,79 +62,78 @@ void workaroundPartitionZeroAccess(FsDeviceOperator* fsOperator) fsStorageClose(&gameCardStorage); } -bool dumpCartridgeImage(FsDeviceOperator* fsOperator, bool isFat32, bool dumpCert, bool trimDump, bool calcCrc) +bool dumpCartridgeImage(FsDeviceOperator* fsOperator, bool isFat32, bool setXciArchiveBit, bool dumpCert, bool trimDump, bool calcCrc) { - u64 partitionOffset = 0, fileOffset = 0, xciDataSize = 0, totalSize = 0, n; + u64 partitionOffset = 0, xciDataSize = 0, n; u64 partitionSizes[ISTORAGE_PARTITION_CNT]; - char partitionSizesStr[ISTORAGE_PARTITION_CNT][32] = {'\0'}, xciDataSizeStr[32] = {'\0'}, curSizeStr[32] = {'\0'}, totalSizeStr[32] = {'\0'}, filename[NAME_BUF_LEN * 2] = {'\0'}; + char partitionSizesStr[ISTORAGE_PARTITION_CNT][32] = {'\0'}, xciDataSizeStr[32] = {'\0'}, filename[NAME_BUF_LEN * 2] = {'\0'}; u32 partition; Result result; FsGameCardHandle handle; FsStorage gameCardStorage; bool proceed = true, success = false, fat32_error = false; FILE *outFile = NULL; - char *buf = NULL; + u8 *buf = NULL; u8 splitIndex = 0; - u8 progress = 0; u32 crc1 = 0, crc2 = 0; - u64 start, now, remainingTime; - struct tm *timeinfo; - char etaInfo[32] = {'\0'}; - double lastSpeed = 0.0, averageSpeed = 0.0; + progress_ctx_t progressCtx; + memset(&progressCtx, 0, sizeof(progress_ctx_t)); + + char tmp_idx[5]; size_t write_res; - char *dumpName = generateDumpName(); + char *dumpName = generateDumpFullName(); if (!dumpName) { - uiDrawString("Error: unable to generate output dump name!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: unable to generate output dump name!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); breaks += 2; return false; } for(partition = 0; partition < ISTORAGE_PARTITION_CNT; partition++) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Getting partition #%u size...", partition); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks++; + /*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Getting partition #%u size...", partition); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks++;*/ workaroundPartitionZeroAccess(fsOperator); if (R_SUCCEEDED(result = fsDeviceOperatorGetGameCardHandle(fsOperator, &handle))) { /*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "GetGameCardHandle succeeded: 0x%08X", handle.value); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++;*/ if (R_SUCCEEDED(result = fsOpenGameCardStorage(&gameCardStorage, &handle, partition))) { /*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "OpenGameCardStorage succeeded: 0x%08X", handle.value); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++;*/ if (R_SUCCEEDED(result = fsStorageGetSize(&gameCardStorage, &(partitionSizes[partition])))) { xciDataSize += partitionSizes[partition]; convertSize(partitionSizes[partition], partitionSizesStr[partition], sizeof(partitionSizesStr[partition]) / sizeof(partitionSizesStr[partition][0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Partition #%u size: %s (%lu bytes).", partition, partitionSizesStr[partition], partitionSizes[partition]); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks += 2; + /*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Partition #%u size: %s (%lu bytes).", partition, partitionSizesStr[partition], partitionSizes[partition]); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks += 2;*/ } else { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "StorageGetSize failed! (0x%08X)", result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); proceed = false; } fsStorageClose(&gameCardStorage); } else { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "OpenGameCardStorage failed! (0x%08X)", result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); proceed = false; } } else { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "GetGameCardHandle failed! (0x%08X)", result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); proceed = false; } @@ -136,14 +143,14 @@ bool dumpCartridgeImage(FsDeviceOperator* fsOperator, bool isFat32, bool dumpCer if (proceed) { convertSize(xciDataSize, xciDataSizeStr, sizeof(xciDataSizeStr) / sizeof(xciDataSizeStr[0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XCI data size: %s (%lu bytes).", xciDataSizeStr, xciDataSize); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks += 2; + /*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XCI data size: %s (%lu bytes).", xciDataSizeStr, xciDataSize); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks += 2;*/ if (trimDump) { - totalSize = trimmedCardSize; - snprintf(totalSizeStr, sizeof(totalSizeStr) / sizeof(totalSizeStr[0]), "%s", trimmedCardSizeStr); + progressCtx.totalSize = trimmedCardSize; + snprintf(progressCtx.totalSizeStr, sizeof(progressCtx.totalSizeStr) / sizeof(progressCtx.totalSizeStr[0]), "%s", trimmedCardSizeStr); // Change dump size for the last IStorage partition u64 partitionSizesSum = 0; @@ -151,21 +158,36 @@ bool dumpCartridgeImage(FsDeviceOperator* fsOperator, bool isFat32, bool dumpCer partitionSizes[ISTORAGE_PARTITION_CNT - 1] = (trimmedCardSize - partitionSizesSum); } else { - totalSize = xciDataSize; - snprintf(totalSizeStr, sizeof(totalSizeStr) / sizeof(totalSizeStr[0]), "%s", xciDataSizeStr); + progressCtx.totalSize = xciDataSize; + snprintf(progressCtx.totalSizeStr, sizeof(progressCtx.totalSizeStr) / sizeof(progressCtx.totalSizeStr[0]), "%s", xciDataSizeStr); } - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output dump size: %s (%lu bytes).", totalSizeStr, totalSize); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output dump size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++; - if (totalSize <= freeSpace) + if (progressCtx.totalSize <= freeSpace) { breaks++; - if (totalSize > FAT32_FILESIZE_LIMIT && isFat32) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32) { - snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s.xc%u", dumpName, splitIndex); + if (setXciArchiveBit) + { + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s.xci", dumpName); + + // Since we may actually be dealing with an existing directory with the archive bit set or unset, let's try both + // Better safe than sorry + unlink(filename); + removeDirectory(filename); + + mkdir(filename, 0744); + + sprintf(tmp_idx, "/%02u", splitIndex); + strcat(filename, tmp_idx); + } else { + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s.xc%u", dumpName, splitIndex); + } } else { snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s.xci", dumpName); } @@ -173,50 +195,54 @@ bool dumpCartridgeImage(FsDeviceOperator* fsOperator, bool isFat32, bool dumpCer outFile = fopen(filename, "wb"); if (outFile) { - buf = (char*)malloc(DUMP_BUFFER_SIZE); + buf = malloc(DUMP_BUFFER_SIZE); if (buf) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dump procedure started. Writing output to \"%s\". Hold B to cancel.", filename); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dump procedure started. Hold %s to cancel.", NINTENDO_FONT_B); + 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 HOME button. Doing so could corrupt the SD card filesystem.", 0, breaks * font_height, 255, 0, 0); + 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; } - timeGetCurrentTime(TimeType_LocalSystemClock, &start); + progressCtx.line_offset = (breaks + 4); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start)); for(partition = 0; partition < ISTORAGE_PARTITION_CNT; partition++) { n = DUMP_BUFFER_SIZE; - uiFill(0, breaks * font_height, FB_WIDTH, font_height * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping partition #%u...", partition); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - workaroundPartitionZeroAccess(fsOperator); if (R_SUCCEEDED(result = fsDeviceOperatorGetGameCardHandle(fsOperator, &handle))) { if (R_SUCCEEDED(result = fsOpenGameCardStorage(&gameCardStorage, &handle, partition))) { - for(partitionOffset = 0; partitionOffset < partitionSizes[partition]; partitionOffset += n, fileOffset += n) + for(partitionOffset = 0; partitionOffset < partitionSizes[partition]; partitionOffset += n, progressCtx.curOffset += n) { + uiFill(0, (breaks * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", filename); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping IStorage partition #%u...", partition); + uiDrawString(strbuf, 8, ((breaks + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + if (DUMP_BUFFER_SIZE > (partitionSizes[partition] - partitionOffset)) n = (partitionSizes[partition] - partitionOffset); if (R_FAILED(result = fsStorageRead(&gameCardStorage, partitionOffset, buf, n))) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "StorageRead failed (0x%08X) at offset 0x%016lX for partition #%u", result, partitionOffset, partition); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); proceed = false; break; } // Remove game card certificate - if (fileOffset == 0 && !dumpCert) memset(buf + CERT_OFFSET, 0xFF, CERT_SIZE); + if (progressCtx.curOffset == 0 && !dumpCert) memset(buf + CERT_OFFSET, 0xFF, CERT_SIZE); if (calcCrc) { @@ -224,7 +250,7 @@ bool dumpCartridgeImage(FsDeviceOperator* fsOperator, bool isFat32, bool dumpCer { if (dumpCert) { - if (fileOffset == 0) + if (progressCtx.curOffset == 0) { // Update CRC32 (with gamecard certificate) crc32(buf, n, &crc1); @@ -258,9 +284,9 @@ bool dumpCartridgeImage(FsDeviceOperator* fsOperator, bool isFat32, bool dumpCer } } - if (totalSize > FAT32_FILESIZE_LIMIT && isFat32 && (fileOffset + n) < totalSize && (fileOffset + n) >= ((splitIndex + 1) * SPLIT_FILE_XCI_PART_SIZE)) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32 && (progressCtx.curOffset + n) >= ((splitIndex + 1) * SPLIT_FILE_XCI_PART_SIZE)) { - u64 new_file_chunk_size = ((fileOffset + n) - ((splitIndex + 1) * SPLIT_FILE_XCI_PART_SIZE)); + u64 new_file_chunk_size = ((progressCtx.curOffset + n) - ((splitIndex + 1) * SPLIT_FILE_XCI_PART_SIZE)); u64 old_file_chunk_size = (n - new_file_chunk_size); if (old_file_chunk_size > 0) @@ -268,48 +294,58 @@ bool dumpCartridgeImage(FsDeviceOperator* fsOperator, bool isFat32, bool dumpCer write_res = fwrite(buf, 1, old_file_chunk_size, outFile); if (write_res != old_file_chunk_size) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", old_file_chunk_size, fileOffset, splitIndex, write_res); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", old_file_chunk_size, progressCtx.curOffset, splitIndex, write_res); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); proceed = false; break; } } fclose(outFile); + outFile = NULL; - splitIndex++; - snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s.xc%u", dumpName, splitIndex); - - outFile = fopen(filename, "wb"); - if (!outFile) + if (new_file_chunk_size > 0 || (progressCtx.curOffset + n) < progressCtx.totalSize) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file for part #%u!", splitIndex); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); - proceed = false; - break; - } - - if (new_file_chunk_size > 0) - { - write_res = fwrite(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); - if (write_res != new_file_chunk_size) + splitIndex++; + + if (setXciArchiveBit) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, fileOffset + old_file_chunk_size, splitIndex, write_res); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s.xci/%02u", dumpName, splitIndex); + } else { + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s.xc%u", dumpName, splitIndex); + } + + outFile = fopen(filename, "wb"); + if (!outFile) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file for part #%u!", splitIndex); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); proceed = false; break; } + + if (new_file_chunk_size > 0) + { + write_res = fwrite(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); + if (write_res != new_file_chunk_size) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, write_res); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + } } } else { write_res = fwrite(buf, 1, n, outFile); if (write_res != n) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", n, fileOffset, write_res); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", n, progressCtx.curOffset, write_res); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - if ((fileOffset + n) > FAT32_FILESIZE_LIMIT) + if ((progressCtx.curOffset + n) > FAT32_FILESIZE_LIMIT) { - uiDrawString("You're probably using a FAT32 partition. Make sure to enable the \"Split output dump\" option.", 0, (breaks + 7) * font_height, 255, 255, 255); + uiDrawString("You're probably using a FAT32 partition. Make sure to enable the \"Split output dump\" option.", 8, ((breaks + 8) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); fat32_error = true; } @@ -318,101 +354,75 @@ bool dumpCartridgeImage(FsDeviceOperator* fsOperator, bool isFat32, bool dumpCer } } - timeGetCurrentTime(TimeType_LocalSystemClock, &now); + printProgressBar(&progressCtx, true, n); - lastSpeed = (((double)(fileOffset + n) / (double)DUMP_BUFFER_SIZE) / difftime((time_t)now, (time_t)start)); - averageSpeed = ((SMOOTHING_FACTOR * lastSpeed) + ((1 - SMOOTHING_FACTOR) * averageSpeed)); - if (!isnormal(averageSpeed)) averageSpeed = 0.00; // Very low values - - remainingTime = (u64)(((double)(totalSize - (fileOffset + n)) / (double)DUMP_BUFFER_SIZE) / averageSpeed); - timeinfo = localtime((time_t*)&remainingTime); - strftime(etaInfo, sizeof(etaInfo) / sizeof(etaInfo[0]), "%HH%MM%SS", timeinfo); - - progress = (u8)(((fileOffset + n) * 100) / totalSize); - - uiFill(0, ((breaks + 2) * font_height) - 4, FB_WIDTH / 4, font_height + 8, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%.2lf MiB/s [ETA: %s]", averageSpeed, etaInfo); - uiDrawString(strbuf, font_height * 2, (breaks + 2) * font_height, 255, 255, 255); - - uiFill(FB_WIDTH / 4, ((breaks + 2) * font_height) + 2, FB_WIDTH / 2, font_height, 0, 0, 0); - uiFill(FB_WIDTH / 4, ((breaks + 2) * font_height) + 2, (((fileOffset + n) * (FB_WIDTH / 2)) / totalSize), font_height, 0, 255, 0); - - uiFill(FB_WIDTH - (FB_WIDTH / 4), ((breaks + 2) * font_height) - 4, FB_WIDTH / 4, font_height + 8, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - convertSize(fileOffset + n, curSizeStr, sizeof(curSizeStr) / sizeof(curSizeStr[0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%u%% [%s / %s]", progress, curSizeStr, totalSizeStr); - uiDrawString(strbuf, FB_WIDTH - (FB_WIDTH / 4) + (font_height * 2), (breaks + 2) * font_height, 255, 255, 255); - - uiRefreshDisplay(); - - if ((fileOffset + n) < totalSize && ((fileOffset / DUMP_BUFFER_SIZE) % 10) == 0) + if ((progressCtx.curOffset + n) < progressCtx.totalSize && ((progressCtx.curOffset / DUMP_BUFFER_SIZE) % 10) == 0) { hidScanInput(); u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO); if (keysDown & KEY_B) { - uiDrawString("Process canceled", 0, (breaks + 5) * font_height, 255, 0, 0); + uiDrawString("Process canceled.", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); proceed = false; break; } } } - if (fileOffset >= totalSize) success = true; + if (progressCtx.curOffset >= progressCtx.totalSize) success = true; // Support empty files if (!partitionSizes[partition]) { - uiFill(FB_WIDTH / 4, ((breaks + 2) * font_height) + 2, ((fileOffset * (FB_WIDTH / 2)) / totalSize), font_height, 0, 255, 0); + uiFill(0, (breaks * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - uiFill(FB_WIDTH - (FB_WIDTH / 4), ((breaks + 2) * font_height) - 4, FB_WIDTH / 4, font_height + 8, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - convertSize(fileOffset, curSizeStr, sizeof(curSizeStr) / sizeof(curSizeStr[0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%u%% [%s / %s]", progress, curSizeStr, totalSizeStr); - uiDrawString(strbuf, FB_WIDTH - (FB_WIDTH / 4) + (font_height * 2), (breaks + 2) * font_height, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", filename); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - uiRefreshDisplay(); - } - - if (!proceed) - { - uiFill(FB_WIDTH / 4, ((breaks + 2) * font_height) + 2, (((fileOffset + n) * (FB_WIDTH / 2)) / totalSize), font_height, 255, 0, 0); - breaks += 5; - if (fat32_error) breaks += 2; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping IStorage partition #%u...", partition); + uiDrawString(strbuf, 8, ((breaks + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + printProgressBar(&progressCtx, false, 0); } fsStorageClose(&gameCardStorage); } else { - breaks++; snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "OpenGameCardStorage failed for partition #%u! (0x%08X)", partition, result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); proceed = false; } } else { - breaks++; snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "GetGameCardHandle failed for partition #%u! (0x%08X)", partition, result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); proceed = false; } - if (!proceed) break; + if (!proceed) + { + setProgressBarError(&progressCtx); + if (fat32_error) breaks += 2; + break; + } } free(buf); } else { - uiDrawString("Failed to allocate memory for the dump process!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Failed to allocate memory for the dump process!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } + if (outFile) fclose(outFile); + + breaks += 6; + if (success) { - fclose(outFile); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now)); + progressCtx.now -= progressCtx.start; - breaks += 5; - - now -= start; - timeinfo = localtime((time_t*)&now); - strftime(etaInfo, sizeof(etaInfo) / sizeof(etaInfo[0]), "%HH%MM%SS", timeinfo); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", etaInfo); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); + formatETAString(progressCtx.now, progressCtx.etaInfo, sizeof(progressCtx.etaInfo) / sizeof(progressCtx.etaInfo[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", progressCtx.etaInfo); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); if (calcCrc) { @@ -423,18 +433,18 @@ bool dumpCartridgeImage(FsDeviceOperator* fsOperator, bool isFat32, bool dumpCer if (dumpCert) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XCI dump CRC32 checksum (with certificate): %08X", crc1); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); breaks++; snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XCI dump CRC32 checksum (without certificate): %08X", crc2); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); } else { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XCI dump CRC32 checksum: %08X", crc2); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); } breaks += 2; - uiDrawString("Starting verification process using XML database from NSWDB.COM...", 0, breaks * font_height, 255, 255, 255); + uiDrawString("Starting verification process using XML database from NSWDB.COM...", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++; uiRefreshDisplay(); @@ -442,32 +452,48 @@ bool dumpCartridgeImage(FsDeviceOperator* fsOperator, bool isFat32, bool dumpCer gameCardDumpNSWDBCheck(crc2); } else { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XCI dump CRC32 checksum: %08X", crc1); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); breaks++; - uiDrawString("Dump verification disabled (not compatible with trimmed dumps).", 0, breaks * font_height, 255, 255, 255); + uiDrawString("Dump verification disabled (not compatible with trimmed dumps).", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + } + } + + // Set archive bit (only for FAT32 and if the required option is enabled) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32 && setXciArchiveBit) + { + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s.xci", dumpName); + if (R_FAILED(result = fsdevSetArchiveBit(filename))) + { + breaks += 2; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Warning: failed to set archive bit on output directory! (0x%08X)", result); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } } else { - if (outFile) fclose(outFile); - - if (totalSize > FAT32_FILESIZE_LIMIT && isFat32) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32) { - for(u8 i = 0; i <= splitIndex; i++) + if (setXciArchiveBit) { - snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s.xc%u", dumpName, i); - remove(filename); + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s.xci", dumpName); + removeDirectory(filename); + } else { + for(u8 i = 0; i <= splitIndex; i++) + { + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s.xc%u", dumpName, i); + unlink(filename); + } } } else { - remove(filename); + unlink(filename); } } } else { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file \"%s\"!", filename); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } else { - uiDrawString("Error: not enough free space available in the SD card.", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: not enough free space available in the SD card.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } @@ -478,74 +504,117 @@ bool dumpCartridgeImage(FsDeviceOperator* fsOperator, bool isFat32, bool dumpCer return success; } -bool dumpApplicationNSP(FsDeviceOperator* fsOperator, bool isFat32, bool calcCrc, u32 appIndex) +bool dumpNintendoSubmissionPackage(FsDeviceOperator* fsOperator, nspDumpType selectedNspDumpType, u32 titleIndex, bool isFat32, bool calcCrc) { Result result; u32 i = 0, j = 0; u32 written = 0; u32 total = 0; - u32 appNcaCount = 0; + u32 titleNcaCount = 0; u32 partition = (hfs0_partition_cnt - 1); // Select the secure partition + FsGameCardHandle handle; + + FsStorage gameCardStorage; + memset(&gameCardStorage, 0, sizeof(FsStorage)); + NcmContentMetaDatabase ncmDb; + memset(&ncmDb, 0, sizeof(NcmContentMetaDatabase)); + NcmContentStorage ncmStorage; - NcmApplicationContentMetaKey *appList = NULL; - NcmContentRecord *appContentRecords = NULL; - size_t appListSize = (gameCardAppCount * sizeof(NcmApplicationContentMetaKey)); + memset(&ncmStorage, 0, sizeof(NcmContentStorage)); + + NcmApplicationContentMetaKey *titleList = NULL; + NcmContentRecord *titleContentRecords = NULL; + size_t titleListSize = sizeof(NcmApplicationContentMetaKey); + titleListSize *= (selectedNspDumpType == DUMP_APP_NSP ? gameCardAppCount : (selectedNspDumpType == DUMP_PATCH_NSP ? gameCardPatchCount : gameCardAddOnCount)); cnmt_xml_program_info xml_program_info; cnmt_xml_content_info *xml_content_info = NULL; NcmNcaId ncaId; - char ncaHeader[NCA_FULL_HEADER_LENGTH] = {'\0'}; + u8 ncaHeader[NCA_FULL_HEADER_LENGTH] = {0}; nca_header_t dec_nca_header; + nca_program_mod_data ncaProgramMod; + memset(&ncaProgramMod, 0, sizeof(nca_program_mod_data)); + + ncaProgramMod.hash_table = NULL; + ncaProgramMod.block_data[0] = NULL; + ncaProgramMod.block_data[1] = NULL; + + nca_cnmt_mod_data ncaCnmtMod; + memset(&ncaCnmtMod, 0, sizeof(nca_cnmt_mod_data)); + + title_rights_ctx rights_info; + memset(&rights_info, 0, sizeof(title_rights_ctx)); + + u8 *tikData = NULL; + u64 tikSize = 0; + + u8 *certData = NULL; + u64 certSize = 0; + u32 cnmtNcaIndex = 0; - char *cnmtNcaBuf = NULL; + u8 *cnmtNcaBuf = NULL; bool cnmtFound = false; + char *cnmtXml = NULL; - u64 cnmt_pfs0_offset; - u64 cnmt_pfs0_size; - pfs0_header cnmt_pfs0_header; - pfs0_entry_table *cnmt_pfs0_entries = NULL; - - u64 appCnmtOffset; - cnmt_header appCnmtHeader; - cnmt_application_header appCnmtAppHeader; - cnmt_content_record *appCnmtContentRecords = NULL; - - char *metadataXml = NULL; + u32 nacpNcaIndex = 0; + char *nacpXml = NULL; + u32 nspFileCount = 0; pfs0_header nspPfs0Header; pfs0_entry_table *nspPfs0EntryTable = NULL; char *nspPfs0StrTable = NULL; - u32 full_nsp_header_size = 0; + u64 nspPfs0StrTableSize = 0; + u64 full_nsp_header_size = 0; - u64 total_size = 0; - char dumpPath[NAME_BUF_LEN * 2] = {'\0'}, curSizeStr[32] = {'\0'}, totalSizeStr[32] = {'\0'}; + u64 hash_table_dump_buffer_start = 0; + u64 hash_table_dump_buffer_end = 0; + u64 block0_dump_buffer_start = 0; + u64 block0_dump_buffer_end = 0; + u64 block1_dump_buffer_start = 0; + u64 block1_dump_buffer_end = 0; - u64 n, nca_offset, nsp_file_offset = 0; + Sha256Context nca_hash_ctx; + + char dumpPath[NAME_BUF_LEN * 2] = {'\0'}; + + u64 n, nca_offset; FILE *outFile = NULL; - char *buf = NULL; + u8 *buf = NULL; u8 splitIndex = 0; - u8 progress = 0; u32 crc = 0; - bool proceed = true, success = false, fat32_error = false; + bool proceed = true, success = false, dumping = false, fat32_error = false; - u64 start, now, remainingTime; - struct tm *timeinfo; - char etaInfo[32] = {'\0'}; - double lastSpeed = 0.0, averageSpeed = 0.0; + progress_ctx_t progressCtx; + memset(&progressCtx, 0, sizeof(progress_ctx_t)); + + char tmp_idx[5]; size_t write_res; - // Generate filename for our required CNMT file - char cnmtFileName[40] = {'\0'}; - snprintf(cnmtFileName, sizeof(cnmtFileName) / sizeof(cnmtFileName[0]), "Application_%016lx.cnmt", gameCardTitleID[appIndex]); + int initial_breaks = breaks; - if (appIndex > (gameCardAppCount - 1)) + if ((selectedNspDumpType == DUMP_APP_NSP && !gameCardAppCount) || (selectedNspDumpType == DUMP_PATCH_NSP && !gameCardPatchCount) || (selectedNspDumpType == DUMP_ADDON_NSP && !gameCardAddOnCount)) { - uiDrawString("Error: invalid application index!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: invalid title type count!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; + return false; + } + + if ((selectedNspDumpType == DUMP_APP_NSP && titleIndex > (gameCardAppCount - 1)) || (selectedNspDumpType == DUMP_PATCH_NSP && titleIndex > (gameCardPatchCount - 1)) || (selectedNspDumpType == DUMP_ADDON_NSP && titleIndex > (gameCardAddOnCount - 1))) + { + uiDrawString("Error: invalid title index!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; + return false; + } + + char *dumpName = generateNSPDumpName(selectedNspDumpType, titleIndex); + if (!dumpName) + { + uiDrawString("Error: unable to generate output dump name!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); breaks += 2; return false; } @@ -556,352 +625,733 @@ bool dumpApplicationNSP(FsDeviceOperator* fsOperator, bool isFat32, bool calcCrc if (!partitionHfs0FileCount) { - uiDrawString("The Secure HFS0 partition is empty!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("The Secure HFS0 partition is empty!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } - uiDrawString("Retrieving information from encrypted NCA content files...", 0, breaks * font_height, 255, 255, 255); + if (R_FAILED(result = fsDeviceOperatorGetGameCardHandle(fsOperator, &handle))) + { + 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); + goto out; + } + + if (R_FAILED(result = fsOpenGameCardStorage(&gameCardStorage, &handle, HFS0_TO_ISTORAGE_IDX(hfs0_partition_cnt, partition)))) + { + 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); + goto out; + } + + uiDrawString("Retrieving information from encrypted NCA content files...", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); uiRefreshDisplay(); breaks++; - appList = (NcmApplicationContentMetaKey*)calloc(1, appListSize); - if (!appList) + titleList = calloc(1, titleListSize); + if (!titleList) { - uiDrawString("Error: unable to allocate memory for the ApplicationContentMetaKey struct!", 0, breaks * font_height, 255, 0, 0); + 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(FsStorageId_GameCard, &ncmDb))) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmOpenContentMetaDatabase failed! (0x%08X)", result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + 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))) + u8 filter = ((u8)selectedNspDumpType + META_DB_REGULAR_APPLICATION); + + if (R_FAILED(result = ncmContentMetaDatabaseListApplication(&ncmDb, filter, titleList, titleListSize, &written, &total))) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentMetaDatabaseListApplication failed! (0x%08X)", result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + 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!", 0, breaks * font_height, 255, 0, 0); + 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 || written != gameCardAppCount) + if (written != total || (selectedNspDumpType == DUMP_APP_NSP && written != gameCardAppCount) || (selectedNspDumpType == DUMP_PATCH_NSP && written != gameCardPatchCount) || (selectedNspDumpType == DUMP_ADDON_NSP && written != gameCardAddOnCount)) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: application count mismatch in ncmContentMetaDatabaseListApplication (%u != %u)", written, gameCardAppCount); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: title count mismatch in ncmContentMetaDatabaseListApplication (%u != %u)", written, (selectedNspDumpType == DUMP_APP_NSP ? gameCardAppCount : (selectedNspDumpType == DUMP_PATCH_NSP ? gameCardPatchCount : gameCardAddOnCount))); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } - appContentRecords = (NcmContentRecord*)calloc(partitionHfs0FileCount, sizeof(NcmContentRecord)); - if (!appContentRecords) + titleContentRecords = calloc(partitionHfs0FileCount, sizeof(NcmContentRecord)); + if (!titleContentRecords) { - uiDrawString("Error: unable to allocate memory for the ContentRecord struct!", 0, breaks * font_height, 255, 0, 0); + 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[appIndex].metaRecord), 0, appContentRecords, partitionHfs0FileCount * sizeof(NcmContentRecord), &written))) + if (R_FAILED(result = ncmContentMetaDatabaseListContentInfo(&ncmDb, &(titleList[titleIndex].metaRecord), 0, titleContentRecords, partitionHfs0FileCount * sizeof(NcmContentRecord), &written))) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentMetaDatabaseListContentInfo failed! (0x%08X)", result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } - appNcaCount = written; + titleNcaCount = written; - // Fill information for our XML + // Fill information for our CNMT XML memset(&xml_program_info, 0, sizeof(cnmt_xml_program_info)); - xml_program_info.type = appList[appIndex].metaRecord.type; - xml_program_info.title_id = appList[appIndex].metaRecord.titleId; - xml_program_info.version = appList[appIndex].metaRecord.version; - xml_program_info.nca_cnt = appNcaCount; + xml_program_info.type = titleList[titleIndex].metaRecord.type; + xml_program_info.title_id = titleList[titleIndex].metaRecord.titleId; + xml_program_info.version = titleList[titleIndex].metaRecord.version; + xml_program_info.nca_cnt = titleNcaCount; - xml_content_info = (cnmt_xml_content_info*)calloc(appNcaCount, sizeof(cnmt_xml_content_info)); + xml_content_info = calloc(titleNcaCount, sizeof(cnmt_xml_content_info)); if (!xml_content_info) { - uiDrawString("Error: unable to allocate memory for the CNMT XML content info struct!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: unable to allocate memory for the CNMT XML content info struct!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } if (R_FAILED(result = ncmOpenContentStorage(FsStorageId_GameCard, &ncmStorage))) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmOpenContentStorage failed! (0x%08X)", result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } - for(i = 0; i < appNcaCount; i++) + // Fill our CNMT XML content records, leaving the CNMT NCA at the end + u32 titleRecordIndex; + for(i = 0, titleRecordIndex = 0; titleRecordIndex < titleNcaCount; i++, titleRecordIndex++) { - // Fill information for our XML - xml_content_info[i].type = appContentRecords[i].type; - memcpy(xml_content_info[i].nca_id, appContentRecords[i].ncaId.c, 16); - convertDataToHexString(appContentRecords[i].ncaId.c, 16, xml_content_info[i].nca_id_str, 33); - convertNcaSizeToU64(appContentRecords[i].size, &(xml_content_info[i].size)); - - memcpy(&ncaId, &(appContentRecords[i].ncaId), sizeof(NcmNcaId)); - - if (!cnmtFound && appContentRecords[i].type == NcmContentType_CNMT) + if (!cnmtFound && titleContentRecords[titleRecordIndex].type == NcmContentType_CNMT) { cnmtFound = true; - cnmtNcaIndex = i; - - cnmtNcaBuf = (char*)calloc(xml_content_info[i].size, sizeof(char)); - if (!cnmtNcaBuf) + cnmtNcaIndex = titleRecordIndex; + i--; + continue; + } + + // Skip Delta Fragments or any other unknown content types + if (titleContentRecords[titleRecordIndex].type >= NCA_CONTENT_TYPE_DELTA) + { + xml_program_info.nca_cnt--; + i--; + continue; + } + + // Fill information for our CNMT XML + xml_content_info[i].type = titleContentRecords[titleRecordIndex].type; + memcpy(xml_content_info[i].nca_id, titleContentRecords[titleRecordIndex].ncaId.c, 16); // Temporary + convertDataToHexString(titleContentRecords[titleRecordIndex].ncaId.c, 16, xml_content_info[i].nca_id_str, 33); // Temporary + convertNcaSizeToU64(titleContentRecords[titleRecordIndex].size, &(xml_content_info[i].size)); + convertDataToHexString(xml_content_info[i].hash, 32, xml_content_info[i].hash_str, 65); // Temporary + + memcpy(&ncaId, &(titleContentRecords[titleRecordIndex].ncaId), sizeof(NcmNcaId)); + + if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, ncaHeader, NCA_FULL_HEADER_LENGTH))) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentStorageReadContentIdFile failed for NCA \"%s\"! (0x%08X)", xml_content_info[i].nca_id_str, result); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + + // Decrypt the NCA header + if (!decryptNcaHeader(ncaHeader, NCA_FULL_HEADER_LENGTH, &dec_nca_header, &rights_info, xml_content_info[i].decrypted_nca_keys)) + { + proceed = false; + break; + } + + bool has_rights_id = false; + + for(j = 0; j < 0x10; j++) + { + if (dec_nca_header.rights_id[j] != 0) { - uiDrawString("Error: unable to allocate memory for CNMT NCA data!", 0, breaks * font_height, 255, 0, 0); + has_rights_id = true; + break; + } + } + + if (selectedNspDumpType == DUMP_APP_NSP || selectedNspDumpType == DUMP_ADDON_NSP) + { + if (has_rights_id) + { + uiDrawString("Error: Rights ID field in NCA header not empty!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); proceed = false; break; } - if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, cnmtNcaBuf, xml_content_info[i].size))) + // Fill information for our CNMT XML + xml_content_info[i].keyblob = (dec_nca_header.crypto_type2 > dec_nca_header.crypto_type ? dec_nca_header.crypto_type2 : dec_nca_header.crypto_type); + + // Modify distribution type + dec_nca_header.distribution = 0; + + // Patch ACID pubkey and recreate NCA NPDM signature if we're dealing with the Program NCA + if (xml_content_info[i].type == NcmContentType_Program) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentStorageReadContentIdFile failed for NCA \"%s\"! (0x%08X)", xml_content_info[i].nca_id_str, result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - proceed = false; - break; + if (!processProgramNca(&ncmStorage, &ncaId, &dec_nca_header, &(xml_content_info[i]), &ncaProgramMod)) + { + proceed = false; + break; + } } - - // Calculate SHA-256 checksum for the CNMT NCA - if (!calculateSHA256((u8*)cnmtNcaBuf, (u32)xml_content_info[i].size, xml_content_info[i].hash)) + } else + if (selectedNspDumpType == DUMP_PATCH_NSP) + { + if (has_rights_id) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "SHA-256 checksum calculation for CNMT NCA \"%s\" failed!", xml_content_info[i].nca_id_str); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - proceed = false; - break; + // Fill information for our CNMT XML + xml_content_info[i].keyblob = dec_nca_header.rights_id[15]; + } else { + // Fill information for our CNMT XML + xml_content_info[i].keyblob = (dec_nca_header.crypto_type2 > dec_nca_header.crypto_type ? dec_nca_header.crypto_type2 : dec_nca_header.crypto_type); } + } + + // Generate NACP XML + if (!nacpXml && xml_content_info[i].type == NcmContentType_Icon) + { + nacpNcaIndex = i; - // Fill information for our XML - convertDataToHexString(xml_content_info[i].hash, NCA_CNMT_DIGEST_SIZE, xml_content_info[i].hash_str, 65); - - // Decrypt the CNMT NCA buffer in-place - if (!decryptCnmtNca(cnmtNcaBuf, xml_content_info[i].size)) - { - proceed = false; - break; - } - - memcpy(&dec_nca_header, cnmtNcaBuf, sizeof(nca_header_t)); - } else { - if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, ncaHeader, NCA_FULL_HEADER_LENGTH))) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentStorageReadContentIdFile failed for NCA \"%s\"! (0x%08X)", xml_content_info[i].nca_id_str, result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - proceed = false; - break; - } - - // Decrypt the NCA header in-place - if (!decryptNcaHeader(ncaHeader, NCA_FULL_HEADER_LENGTH, &dec_nca_header)) + if (!generateNacpXmlFromNca(&ncmStorage, &ncaId, &dec_nca_header, xml_content_info[i].decrypted_nca_keys, &nacpXml)) { proceed = false; break; } } - // Fill information for our XML - xml_content_info[i].keyblob = (dec_nca_header.crypto_type2 > dec_nca_header.crypto_type ? dec_nca_header.crypto_type2 : dec_nca_header.crypto_type); - - // Modify distribution type - dec_nca_header.distribution = 0; - // Reencrypt header if (!encryptNcaHeader(&dec_nca_header, xml_content_info[i].encrypted_header_mod, NCA_FULL_HEADER_LENGTH)) { proceed = false; break; } - - /*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "sdmc:/%s_header.nca", xml_content_info[i].nca_id_str); - FILE *mod_header = fopen(strbuf, "wb"); - if (mod_header) - { - fwrite(xml_content_info[i].encrypted_header_mod, 1, NCA_FULL_HEADER_LENGTH, mod_header); - fclose(mod_header); - }*/ } if (proceed && !cnmtFound) { - uiDrawString("Error: unable to find the NCA ID for the application's CNMT!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: unable to find CNMT NCA!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } if (!proceed) goto out; - // Fill information for our XML - xml_program_info.min_keyblob = xml_content_info[cnmtNcaIndex].keyblob; + // Update NCA counter just in case we found any delta fragments + titleNcaCount = xml_program_info.nca_cnt; - memcpy(&dec_nca_header, cnmtNcaBuf, sizeof(nca_header_t)); + // Fill information for our CNMT XML + xml_content_info[titleNcaCount - 1].type = titleContentRecords[cnmtNcaIndex].type; + memcpy(xml_content_info[titleNcaCount - 1].nca_id, titleContentRecords[cnmtNcaIndex].ncaId.c, 16); // Temporary + convertDataToHexString(titleContentRecords[cnmtNcaIndex].ncaId.c, 16, xml_content_info[titleNcaCount - 1].nca_id_str, 33); // Temporary + convertNcaSizeToU64(titleContentRecords[cnmtNcaIndex].size, &(xml_content_info[titleNcaCount - 1].size)); + convertDataToHexString(xml_content_info[titleNcaCount - 1].hash, 32, xml_content_info[titleNcaCount - 1].hash_str, 65); // Temporary - cnmt_pfs0_offset = ((dec_nca_header.section_entries[0].media_start_offset * MEDIA_UNIT_SIZE) + dec_nca_header.fs_headers[0].pfs0_superblock.hash_table_offset + dec_nca_header.fs_headers[0].pfs0_superblock.pfs0_offset); - cnmt_pfs0_size = dec_nca_header.fs_headers[0].pfs0_superblock.pfs0_size; + memcpy(&ncaId, &(titleContentRecords[cnmtNcaIndex].ncaId), sizeof(NcmNcaId)); - // Fill information for our XML - memcpy(xml_program_info.digest, cnmtNcaBuf + cnmt_pfs0_offset + cnmt_pfs0_size - NCA_CNMT_DIGEST_SIZE, NCA_CNMT_DIGEST_SIZE); - convertDataToHexString(xml_program_info.digest, NCA_CNMT_DIGEST_SIZE, xml_program_info.digest_str, 65); + // Update CNMT index + cnmtNcaIndex = (titleNcaCount - 1); - memcpy(&cnmt_pfs0_header, cnmtNcaBuf + cnmt_pfs0_offset, sizeof(pfs0_header)); - - cnmt_pfs0_entries = (pfs0_entry_table*)calloc(cnmt_pfs0_header.file_cnt, sizeof(pfs0_entry_table)); - if (!cnmt_pfs0_entries) + cnmtNcaBuf = malloc(xml_content_info[cnmtNcaIndex].size); + if (!cnmtNcaBuf) { - uiDrawString("Error: unable to allocate memory for the PFS0 File Entry Table from CNMT NCA section #0!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: unable to allocate memory for CNMT NCA data!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } - cnmtFound = false; - - // Extract the decrypted CNMT in order to retrieve the remaining information for our XML - // It's filename in the PFS0 partition must match the "Application_{lower-case hex titleID}.cnmt" format - for(i = 0; i < cnmt_pfs0_header.file_cnt; i++) + if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, 0, cnmtNcaBuf, xml_content_info[cnmtNcaIndex].size))) { - u32 filename_offset = (cnmt_pfs0_offset + sizeof(pfs0_header) + (cnmt_pfs0_header.file_cnt * sizeof(pfs0_entry_table)) + cnmt_pfs0_entries[i].filename_offset); - if (!strncasecmp(cnmtNcaBuf + filename_offset, cnmtFileName, strlen(cnmtFileName))) - { - cnmtFound = true; - appCnmtOffset = (cnmt_pfs0_offset + sizeof(pfs0_header) + (cnmt_pfs0_header.file_cnt * sizeof(pfs0_entry_table)) + cnmt_pfs0_header.str_table_size + cnmt_pfs0_entries[i].file_offset); - break; - } - } - - if (!cnmtFound) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unable to find file \"%s\" in PFS0 partition from CNMT NCA section #0!", cnmtFileName); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentStorageReadContentIdFile failed for CNMT NCA \"%s\"! (0x%08X)", xml_content_info[cnmtNcaIndex].nca_id_str, result); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } - memcpy(&appCnmtHeader, cnmtNcaBuf + appCnmtOffset, sizeof(cnmt_header)); - memcpy(&appCnmtAppHeader, cnmtNcaBuf + appCnmtOffset + sizeof(cnmt_header), sizeof(cnmt_application_header)); + // Retrieve CNMT NCA data + if (!retrieveCnmtNcaData(selectedNspDumpType, cnmtNcaBuf, &xml_program_info, &(xml_content_info[cnmtNcaIndex]), &ncaCnmtMod, &rights_info)) goto out; - // Fill information for our XML - xml_program_info.patch_tid = appCnmtAppHeader.patch_tid; - xml_program_info.min_sysver = (u32)appCnmtAppHeader.min_sysver; - - appCnmtContentRecords = (cnmt_content_record*)calloc(appCnmtHeader.content_records_cnt, sizeof(cnmt_content_record)); - if (!appCnmtContentRecords) - { - uiDrawString("Error: unable to allocate memory for the PFS0 File Entry Table from CNMT NCA section #0!", 0, breaks * font_height, 255, 0, 0); - goto out; - } - - memcpy(appCnmtContentRecords, cnmtNcaBuf + appCnmtOffset + sizeof(cnmt_header) + appCnmtHeader.table_offset, appCnmtHeader.content_records_cnt * sizeof(cnmt_content_record)); - - for(i = 0; i < appCnmtHeader.content_records_cnt; i++) - { - for(j = 0; j < appNcaCount; j++) - { - if (!memcmp(appCnmtContentRecords[i].nca_id, xml_content_info[j].nca_id, 16)) - { - // Fill information for our XML - memcpy(xml_content_info[j].hash, appCnmtContentRecords[i].hash, NCA_CNMT_DIGEST_SIZE); - convertDataToHexString(xml_content_info[j].hash, NCA_CNMT_DIGEST_SIZE, xml_content_info[j].hash_str, 65); - break; - } - } - } - - /*for(i = 0; i < appNcaCount; i++) - { - breaks++; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Content Record #%u:", i); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks++; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "NCA ID: %s", xml_content_info[i].nca_id_str); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks++; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Size: 0x%016lX", xml_content_info[i].size); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks++; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Type: 0x%02X", xml_content_info[i].type); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks++; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Hash: %s", xml_content_info[i].hash_str); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks++; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Keyblob: 0x%02X", xml_content_info[i].keyblob); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks++; - - uiRefreshDisplay(); - }*/ - - breaks++; - uiDrawString("Generating metadata XML...", 0, breaks * font_height, 255, 255, 255); + // Generate a placeholder CNMT XML. It's length will be used to calculate the final output dump size + /*breaks++; + uiDrawString("Generating placeholder CNMT XML...", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); uiRefreshDisplay(); - breaks++; + breaks++;*/ - // Generate our metadata XML, making sure that the output buffer is big enough - metadataXml = (char*)calloc(NAME_BUF_LEN * 4, sizeof(char)); - if (!metadataXml) + // Make sure that the output buffer for our CNMT XML is big enough + cnmtXml = calloc(NAME_BUF_LEN * 4, sizeof(char)); + if (!cnmtXml) { - uiDrawString("Error: unable to allocate memory for the metadata XML!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: unable to allocate memory for the CNMT XML!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } - generateCnmtMetadataXml(&xml_program_info, xml_content_info, metadataXml); + generateCnmtXml(&xml_program_info, xml_content_info, cnmtXml); - /*char cnmtXmlFileName[50] = {'\0'}; - sprintf(cnmtXmlFileName, "%s.cnmt.xml", xml_content_info[cnmtNcaIndex].nca_id_str); - FILE *metaxml = fopen(cnmtXmlFileName, "wb"); - if (metaxml) + if (rights_info.has_rights_id) { - fwrite(metadataXml, 1, strlen(metadataXml), metaxml); - fclose(metaxml); - }*/ + // Retrieve tik file + /*breaks++; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Retrieving %s file...", rights_info.tik_filename); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + uiRefreshDisplay(); + breaks++;*/ + + tikData = getPartitionHfs0FileByName(&gameCardStorage, rights_info.tik_filename, &tikSize); + if (!tikData) goto out; + + // Retrieve cert file + /*breaks++; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Retrieving %s file...", rights_info.cert_filename); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + uiRefreshDisplay(); + breaks++;*/ + + certData = getPartitionHfs0FileByName(&gameCardStorage, rights_info.cert_filename, &certSize); + if (!certData) goto out; + + // File count = NCA count + CNMT XML + tik + cert + nspFileCount = (titleNcaCount + 3); + + // Calculate PFS0 String Table size + nspPfs0StrTableSize = (((nspFileCount - 4) * NSP_NCA_FILENAME_LENGTH) + (NSP_CNMT_FILENAME_LENGTH * 2) + NSP_TIK_FILENAME_LENGTH + NSP_CERT_FILENAME_LENGTH); + } else { + // File count = NCA count + CNMT XML + nspFileCount = (titleNcaCount + 1); + + // Calculate PFS0 String Table size + nspPfs0StrTableSize = (((nspFileCount - 2) * NSP_NCA_FILENAME_LENGTH) + (NSP_CNMT_FILENAME_LENGTH * 2)); + } + + // Add our NACP XML if we created it + if (nacpXml) + { + nspFileCount++; + nspPfs0StrTableSize += NSP_NACP_FILENAME_LENGTH; + } // Start NSP creation + /*breaks++; + uiDrawString("Generating placeholder PFS0 header...", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + uiRefreshDisplay(); + breaks++;*/ + + memset(&nspPfs0Header, 0, sizeof(pfs0_header)); + nspPfs0Header.magic = bswap_32(PFS0_MAGIC); + nspPfs0Header.file_cnt = nspFileCount; + + nspPfs0EntryTable = calloc(nspFileCount, sizeof(pfs0_entry_table)); + if (!nspPfs0EntryTable) + { + uiDrawString("Unable to allocate memory for the PFS0 file entries!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + + // Make sure we have enough space + nspPfs0StrTable = calloc(nspPfs0StrTableSize * 2, sizeof(char)); + if (!nspPfs0StrTable) + { + uiDrawString("Unable to allocate memory for the PFS0 string table!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + + // Determine our full NSP header size + full_nsp_header_size = (sizeof(pfs0_header) + (nspFileCount * sizeof(pfs0_entry_table)) + nspPfs0StrTableSize); + full_nsp_header_size = round_up(full_nsp_header_size, 0x10); + + // Determine our String Table size + nspPfs0Header.str_table_size = (full_nsp_header_size - (sizeof(pfs0_header) + (nspFileCount * sizeof(pfs0_entry_table)))); + + // Calculate total dump size + progressCtx.totalSize = full_nsp_header_size; + progressCtx.totalSize += strlen(cnmtXml); + if (nacpXml) progressCtx.totalSize += strlen(nacpXml); + if (rights_info.has_rights_id) progressCtx.totalSize += (tikSize + certSize); + for(i = 0; i < titleNcaCount; i++) progressCtx.totalSize += xml_content_info[i].size; + breaks++; - uiDrawString("Generating PFS0 header...", 0, breaks * font_height, 255, 255, 255); + convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, sizeof(progressCtx.totalSizeStr) / sizeof(progressCtx.totalSizeStr[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Total NSP dump size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); uiRefreshDisplay(); breaks++; - memset(&nspPfs0Header, 0, sizeof(pfs0_header)); - nspPfs0Header.magic = bswap_32((u32)PFS0_MAGIC); - nspPfs0Header.file_cnt = (appNcaCount + 1); // Make sure to consider the metadata XML - - nspPfs0EntryTable = (pfs0_entry_table*)calloc(appNcaCount + 1, sizeof(pfs0_entry_table)); - if (!nspPfs0EntryTable) + if (progressCtx.totalSize > freeSpace) { - uiDrawString("Unable to allocate memory for the PFS0 file entries!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: not enough free space available in the SD card.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } - // Make sure we have enough memory for the PFS0 String Table - nspPfs0StrTable = (char*)calloc(NAME_BUF_LEN * 4, sizeof(char)); - if (!nspPfs0StrTable) + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s.nsp", dumpName); + + // Since we may actually be dealing with an existing directory with the archive bit set or unset, let's try both + // Better safe than sorry + unlink(dumpPath); + removeDirectory(dumpPath); + + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32) { - uiDrawString("Unable to allocate memory for the PFS0 string table!", 0, breaks * font_height, 255, 0, 0); + mkdir(dumpPath, 0744); + + sprintf(tmp_idx, "/%02u", splitIndex); + strcat(dumpPath, tmp_idx); + } + + outFile = fopen(dumpPath, "wb"); + if (!outFile) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file \"%s\"!", dumpPath); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } + buf = calloc(DUMP_BUFFER_SIZE, sizeof(u8)); + if (!buf) + { + uiDrawString("Failed to allocate memory for the dump process!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + + breaks++; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dump procedure started. Hold %s to cancel.", NINTENDO_FONT_B); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + uiRefreshDisplay(); + 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; + } + + // Write placeholder zeroes + write_res = fwrite(buf, 1, full_nsp_header_size + strlen(cnmtXml), outFile); + if (write_res != (full_nsp_header_size + strlen(cnmtXml))) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes placeholder data to file offset 0x%016lX! (wrote %lu bytes)", full_nsp_header_size + strlen(cnmtXml), (u64)0, write_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + + progressCtx.curOffset = (full_nsp_header_size + strlen(cnmtXml)); + + // Write our NACP XML + if (nacpXml) + { + write_res = fwrite(nacpXml, 1, strlen(nacpXml), outFile); + if (write_res != strlen(nacpXml)) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes NACP XML to file offset 0x%016lX! (wrote %lu bytes)", strlen(nacpXml), progressCtx.curOffset, write_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + + progressCtx.curOffset += strlen(nacpXml); + } + + if (rights_info.has_rights_id) + { + memcpy(buf, tikData, tikSize); + memcpy(buf + tikSize, certData, certSize); + + // Write tik / cert + write_res = fwrite(buf, 1, tikSize + certSize, outFile); + if (write_res != (tikSize + certSize)) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes tik + cert to file offset 0x%016lX! (wrote %lu bytes)", tikSize + certSize, progressCtx.curOffset, write_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + + progressCtx.curOffset += (tikSize + certSize); + } + + // Calculate DUMP_BUFFER_SIZE block numbers for the modified Program NCA data blocks + if (selectedNspDumpType != DUMP_PATCH_NSP && ncaProgramMod.block_mod_cnt > 0) + { + hash_table_dump_buffer_start = ((ncaProgramMod.hash_table_offset / DUMP_BUFFER_SIZE) * DUMP_BUFFER_SIZE); + hash_table_dump_buffer_end = (((ncaProgramMod.hash_table_offset + ncaProgramMod.hash_table_size) / DUMP_BUFFER_SIZE) * DUMP_BUFFER_SIZE); + + block0_dump_buffer_start = ((ncaProgramMod.block_offset[0] / DUMP_BUFFER_SIZE) * DUMP_BUFFER_SIZE); + block0_dump_buffer_end = (((ncaProgramMod.block_offset[0] + ncaProgramMod.block_size[0]) / DUMP_BUFFER_SIZE) * DUMP_BUFFER_SIZE); + + if (ncaProgramMod.block_mod_cnt == 2) + { + block1_dump_buffer_start = ((ncaProgramMod.block_offset[1] / DUMP_BUFFER_SIZE) * DUMP_BUFFER_SIZE); + block1_dump_buffer_end = (((ncaProgramMod.block_offset[1] + ncaProgramMod.block_size[1]) / DUMP_BUFFER_SIZE) * DUMP_BUFFER_SIZE); + } + } + + progressCtx.line_offset = (breaks + 4); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start)); + + dumping = true; + + // Dump all NCAs excluding the CNMT NCA + for(i = 0; i < (titleNcaCount - 1); i++) + { + n = DUMP_BUFFER_SIZE; + + memcpy(ncaId.c, xml_content_info[i].nca_id, 16); + + sha256ContextCreate(&nca_hash_ctx); + + for(nca_offset = 0; nca_offset < xml_content_info[i].size; nca_offset += n, progressCtx.curOffset += n) + { + uiFill(0, (breaks * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", dumpPath); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping NCA content \"%s\"...", xml_content_info[i].nca_id_str); + uiDrawString(strbuf, 8, ((breaks + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + if (DUMP_BUFFER_SIZE > (xml_content_info[i].size - nca_offset)) n = (xml_content_info[i].size - nca_offset); + + if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, nca_offset, buf, n))) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentStorageReadContentIdFile failed (0x%08X) at offset 0x%016lX for NCA \"%s\".", result, nca_offset, xml_content_info[i].nca_id_str); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + + // Replace NCA header with our modified one + if (nca_offset == 0) memcpy(buf, xml_content_info[i].encrypted_header_mod, NCA_FULL_HEADER_LENGTH); + + // Replace modified Program NCA data blocks + if (xml_content_info[i].type == NcmContentType_Program && selectedNspDumpType != DUMP_PATCH_NSP && ncaProgramMod.block_mod_cnt > 0) + { + u64 program_nca_prev_write; + u64 program_nca_next_write; + + if (nca_offset == hash_table_dump_buffer_start || nca_offset == hash_table_dump_buffer_end) + { + if (hash_table_dump_buffer_start == hash_table_dump_buffer_end) + { + memcpy(buf + (ncaProgramMod.hash_table_offset - hash_table_dump_buffer_start), ncaProgramMod.hash_table, ncaProgramMod.hash_table_size); + } else { + program_nca_prev_write = (DUMP_BUFFER_SIZE - (ncaProgramMod.hash_table_offset - hash_table_dump_buffer_start)); + program_nca_next_write = (ncaProgramMod.hash_table_size - program_nca_prev_write); + + if (nca_offset == hash_table_dump_buffer_start) + { + memcpy(buf + (ncaProgramMod.hash_table_offset - hash_table_dump_buffer_start), ncaProgramMod.hash_table, program_nca_prev_write); + } else { + memcpy(buf, ncaProgramMod.hash_table + program_nca_prev_write, program_nca_next_write); + } + } + } + + if (nca_offset == block0_dump_buffer_start || nca_offset == block0_dump_buffer_end) + { + if (block0_dump_buffer_start == block0_dump_buffer_end) + { + memcpy(buf + (ncaProgramMod.block_offset[0] - block0_dump_buffer_start), ncaProgramMod.block_data[0], ncaProgramMod.block_size[0]); + } else { + program_nca_prev_write = (DUMP_BUFFER_SIZE - (ncaProgramMod.block_offset[0] - block0_dump_buffer_start)); + program_nca_next_write = (ncaProgramMod.block_size[0] - program_nca_prev_write); + + if (nca_offset == block0_dump_buffer_start) + { + memcpy(buf + (ncaProgramMod.block_offset[0] - block0_dump_buffer_start), ncaProgramMod.block_data[0], program_nca_prev_write); + } else { + memcpy(buf, ncaProgramMod.block_data[0] + program_nca_prev_write, program_nca_next_write); + } + } + } + + if (ncaProgramMod.block_mod_cnt == 2 && (nca_offset == block1_dump_buffer_start || nca_offset == block1_dump_buffer_end)) + { + if (block1_dump_buffer_start == block1_dump_buffer_end) + { + memcpy(buf + (ncaProgramMod.block_offset[1] - block1_dump_buffer_start), ncaProgramMod.block_data[1], ncaProgramMod.block_size[1]); + } else { + program_nca_prev_write = (DUMP_BUFFER_SIZE - (ncaProgramMod.block_offset[1] - block1_dump_buffer_start)); + program_nca_next_write = (ncaProgramMod.block_size[1] - program_nca_prev_write); + + if (nca_offset == block1_dump_buffer_start) + { + memcpy(buf + (ncaProgramMod.block_offset[1] - block1_dump_buffer_start), ncaProgramMod.block_data[1], program_nca_prev_write); + } else { + memcpy(buf, ncaProgramMod.block_data[1] + program_nca_prev_write, program_nca_next_write); + } + } + } + } + + // Update SHA-256 calculation + sha256ContextUpdate(&nca_hash_ctx, buf, n); + + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32 && (progressCtx.curOffset + n) >= ((splitIndex + 1) * SPLIT_FILE_NSP_PART_SIZE)) + { + u64 new_file_chunk_size = ((progressCtx.curOffset + n) - ((splitIndex + 1) * SPLIT_FILE_NSP_PART_SIZE)); + u64 old_file_chunk_size = (n - new_file_chunk_size); + + if (old_file_chunk_size > 0) + { + write_res = fwrite(buf, 1, old_file_chunk_size, outFile); + if (write_res != old_file_chunk_size) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", old_file_chunk_size, progressCtx.curOffset, splitIndex, write_res); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + } + + fclose(outFile); + outFile = NULL; + + if (new_file_chunk_size > 0 || (progressCtx.curOffset + n) < progressCtx.totalSize) + { + splitIndex++; + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s.nsp/%02u", dumpName, splitIndex); + + outFile = fopen(dumpPath, "wb"); + if (!outFile) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file for part #%u!", splitIndex); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + + if (new_file_chunk_size > 0) + { + write_res = fwrite(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); + if (write_res != new_file_chunk_size) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, write_res); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + } + } + } else { + write_res = fwrite(buf, 1, n, outFile); + if (write_res != n) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", n, progressCtx.curOffset, write_res); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + + if ((progressCtx.curOffset + n) > FAT32_FILESIZE_LIMIT) + { + uiDrawString("You're probably using a FAT32 partition. Make sure to enable the \"Split output dump\" option.", 8, ((breaks + 8) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + fat32_error = true; + } + + proceed = false; + break; + } + } + + printProgressBar(&progressCtx, true, n); + + if ((progressCtx.curOffset + n) < progressCtx.totalSize && ((progressCtx.curOffset / DUMP_BUFFER_SIZE) % 10) == 0) + { + hidScanInput(); + + u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO); + if (keysDown & KEY_B) + { + uiDrawString("Process canceled.", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + } + } + + if (!proceed) + { + setProgressBarError(&progressCtx); + break; + } + + // Support empty files + if (!xml_content_info[i].size) + { + uiFill(0, (breaks * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", dumpPath); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping NCA content \"%s\"...", xml_content_info[i].nca_id_str); + uiDrawString(strbuf, 8, ((breaks + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + printProgressBar(&progressCtx, false, 0); + } + + // Update content info + sha256ContextGetHash(&nca_hash_ctx, xml_content_info[i].hash); + convertDataToHexString(xml_content_info[i].hash, 32, xml_content_info[i].hash_str, 65); + memcpy(xml_content_info[i].nca_id, xml_content_info[i].hash, 16); + convertDataToHexString(xml_content_info[i].nca_id, 16, xml_content_info[i].nca_id_str, 33); + } + + if (!proceed) goto out; + + dumping = false; + + breaks += 6; + + uiFill(0, ((progressCtx.line_offset - 4) * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", dumpPath); + uiDrawString(strbuf, 8, ((progressCtx.line_offset - 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + uiDrawString("Writing PFS0 header, CNMT XML and CNMT NCA...", 8, ((progressCtx.line_offset - 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + uiRefreshDisplay(); + + // Now we can patch our CNMT NCA and generate our proper CNMT XML + if (!patchCnmtNca(cnmtNcaBuf, xml_content_info[cnmtNcaIndex].size, &xml_program_info, xml_content_info, &ncaCnmtMod)) + { + setProgressBarError(&progressCtx); + goto out; + } + + generateCnmtXml(&xml_program_info, xml_content_info, cnmtXml); + // Fill our Entry and String Tables u64 file_offset = 0; u32 filename_offset = 0; - for(i = 0; i < (appNcaCount + 1); i++) + for(i = 0; i < nspFileCount; i++) { - char ncaFileName[50] = {'\0'}; + // If dealing with a title with rights ID, reserve the first four entries for the CNMT XML, NACP XML (if available), tik and cert + // Otherwise, just reserve the first entry for the CNMT XML - if (i == appNcaCount) + char ncaFileName[50] = {'\0'}; + u64 cur_file_size = 0; + + if (i == 0) { + // CNMT XML sprintf(ncaFileName, "%s.cnmt.xml", xml_content_info[cnmtNcaIndex].nca_id_str); - nspPfs0EntryTable[i].file_size = strlen(metadataXml); + cur_file_size = strlen(cnmtXml); } else { - sprintf(ncaFileName, "%s.%s", xml_content_info[i].nca_id_str, (i == cnmtNcaIndex ? "cnmt.nca" : "nca")); - nspPfs0EntryTable[i].file_size = xml_content_info[i].size; + if (nacpXml && i == 1) + { + // NACP XML + sprintf(ncaFileName, "%s.nacp.xml", xml_content_info[nacpNcaIndex].nca_id_str); + cur_file_size = strlen(nacpXml); + } else { + if (rights_info.has_rights_id && ((!nacpXml && (i == 1 || i == 2)) || (nacpXml && (i == 2 || i == 3)))) + { + // tik / cert + sprintf(ncaFileName, "%s", (((!nacpXml && i == 1) || (nacpXml && i == 2)) ? rights_info.tik_filename : rights_info.cert_filename)); + cur_file_size = (((!nacpXml && i == 1) || (nacpXml && i == 2)) ? tikSize : certSize); + } else { + u32 cnt_idx = (i - (rights_info.has_rights_id ? 3 : 1) - (nacpXml ? 1 : 0)); + sprintf(ncaFileName, "%s.%s", xml_content_info[cnt_idx].nca_id_str, (cnt_idx == cnmtNcaIndex ? "cnmt.nca" : "nca")); + cur_file_size = xml_content_info[cnt_idx].size; + } + } } + nspPfs0EntryTable[i].file_size = cur_file_size; nspPfs0EntryTable[i].file_offset = file_offset; nspPfs0EntryTable[i].filename_offset = filename_offset; @@ -911,309 +1361,331 @@ bool dumpApplicationNSP(FsDeviceOperator* fsOperator, bool isFat32, bool calcCrc filename_offset += (strlen(ncaFileName) + 1); } - filename_offset--; - - // Determine our full NSP header size - full_nsp_header_size = (sizeof(pfs0_header) + ((appNcaCount + 1) * sizeof(pfs0_entry_table)) + filename_offset); - full_nsp_header_size = round_up(full_nsp_header_size, 16); - - // Determine our String Table size - nspPfs0Header.str_table_size = (full_nsp_header_size - (sizeof(pfs0_header) + ((appNcaCount + 1) * sizeof(pfs0_entry_table)))); - - // Calculate total dump size - total_size = full_nsp_header_size; - for(i = 0; i < (appNcaCount + 1); i++) total_size += nspPfs0EntryTable[i].file_size; - - breaks++; - convertSize(total_size, totalSizeStr, sizeof(totalSizeStr) / sizeof(totalSizeStr[0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Total NSP dump size: %s (%lu bytes).", totalSizeStr, total_size); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - uiRefreshDisplay(); - breaks++; - - if (total_size > freeSpace) - { - uiDrawString("Error: not enough free space available in the SD card.", 0, breaks * font_height, 255, 0, 0); - goto out; - } - - if (total_size > FAT32_FILESIZE_LIMIT && isFat32) - { - snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s v%u (%016lX).nsp", fixedGameCardName[appIndex], gameCardVersion[appIndex], gameCardTitleID[appIndex]); - - mkdir(dumpPath, 0744); - - snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s v%u (%016lX).nsp/%02u", fixedGameCardName[appIndex], gameCardVersion[appIndex], gameCardTitleID[appIndex], splitIndex); - } else { - snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s v%u (%016lX).nsp", fixedGameCardName[appIndex], gameCardVersion[appIndex], gameCardTitleID[appIndex]); - } - - outFile = fopen(dumpPath, "wb"); - if (!outFile) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file \"%s\"!", dumpPath); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - goto out; - } - - buf = (char*)malloc(DUMP_BUFFER_SIZE); - if (!buf) - { - uiDrawString("Failed to allocate memory for the dump process!", 0, breaks * font_height, 255, 0, 0); - goto out; - } - - breaks++; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dump procedure started. Writing output to \"%.*s\". Hold B to cancel.", (int)((total_size > FAT32_FILESIZE_LIMIT && isFat32) ? (strlen(dumpPath) - 3) : strlen(dumpPath)), dumpPath); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - uiRefreshDisplay(); - breaks += 2; - - if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) - { - uiDrawString("Do not press the HOME button. Doing so could corrupt the SD card filesystem.", 0, breaks * font_height, 255, 0, 0); - breaks += 2; - } - - // Write our full PFS0 header + // Write our full PFS0 header + CNMT XML memcpy(buf, &nspPfs0Header, sizeof(pfs0_header)); - memcpy(buf + sizeof(pfs0_header), nspPfs0EntryTable, sizeof(pfs0_entry_table) * (appNcaCount + 1)); - memcpy(buf + sizeof(pfs0_header) + (sizeof(pfs0_entry_table) * (appNcaCount + 1)), nspPfs0StrTable, nspPfs0Header.str_table_size); + memcpy(buf + sizeof(pfs0_header), nspPfs0EntryTable, nspFileCount * sizeof(pfs0_entry_table)); + memcpy(buf + sizeof(pfs0_header) + (nspFileCount * sizeof(pfs0_entry_table)), nspPfs0StrTable, nspPfs0Header.str_table_size); + memcpy(buf + full_nsp_header_size, cnmtXml, strlen(cnmtXml)); - write_res = fwrite(buf, 1, full_nsp_header_size, outFile); - if (write_res != full_nsp_header_size) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %u bytes full PFS0 header to file offset 0x%016lX! (wrote %lu bytes)", full_nsp_header_size, (u64)0, write_res); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + if (outFile) + { + fclose(outFile); + outFile = NULL; + } + + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s.nsp/%02u", dumpName, 0); + + outFile = fopen(dumpPath, "rb+"); + if (!outFile) + { + setProgressBarError(&progressCtx); + uiDrawString("Failed to re-open output file for part #0!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + } else { + rewind(outFile); + } + + write_res = fwrite(buf, 1, full_nsp_header_size + strlen(cnmtXml), outFile); + if (write_res != (full_nsp_header_size + strlen(cnmtXml))) + { + setProgressBarError(&progressCtx); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes PFS0 header + CNMT XML to file offset 0x%016lX! (wrote %lu bytes)", full_nsp_header_size + strlen(cnmtXml), (u64)0, write_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } - nsp_file_offset = full_nsp_header_size; - - // Update CRC32 - if (calcCrc) crc32(buf, full_nsp_header_size, &crc); - - timeGetCurrentTime(TimeType_LocalSystemClock, &start); - - for(i = 0; i < appNcaCount; i++) + // Now let's write our modified CNMT NCA + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32) { - uiFill(0, breaks * font_height, FB_WIDTH, font_height * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping NCA content #%u...", i); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - - n = DUMP_BUFFER_SIZE; - memcpy(ncaId.c, xml_content_info[i].nca_id, 16); - - for(nca_offset = 0; nca_offset < xml_content_info[i].size; nca_offset += n, nsp_file_offset += n) + if (outFile) { - if (DUMP_BUFFER_SIZE > (xml_content_info[i].size - nca_offset)) n = (xml_content_info[i].size - nca_offset); + fclose(outFile); + outFile = NULL; + } + + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s.nsp/%02u", dumpName, splitIndex); + + outFile = fopen(dumpPath, "rb+"); + if (!outFile) + { + setProgressBarError(&progressCtx); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to re-open output file for part #%u!", splitIndex); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + + fseek(outFile, 0, SEEK_END); + + // This is a pain + u64 cur_file_size = (progressCtx.curOffset - (splitIndex * SPLIT_FILE_NSP_PART_SIZE)); + if ((cur_file_size + xml_content_info[cnmtNcaIndex].size) > SPLIT_FILE_NSP_PART_SIZE) + { + u64 new_file_chunk_size = ((progressCtx.curOffset + xml_content_info[cnmtNcaIndex].size) - ((splitIndex + 1) * SPLIT_FILE_NSP_PART_SIZE)); + u64 old_file_chunk_size = (xml_content_info[cnmtNcaIndex].size - new_file_chunk_size); - if (R_FAILED(result = ncmContentStorageReadContentIdFile(&ncmStorage, &ncaId, nca_offset, buf, n))) + if (old_file_chunk_size > 0) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentStorageReadContentIdFile failed (0x%08X) at offset 0x%016lX for NCA \"%s\".", result, nca_offset, xml_content_info[i].nca_id_str); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); - proceed = false; - break; + write_res = fwrite(cnmtNcaBuf, 1, old_file_chunk_size, outFile); + if (write_res != old_file_chunk_size) + { + setProgressBarError(&progressCtx); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes CNMT NCA chunk #1 from offset 0x%016lX to part #%02u! (wrote %lu bytes)", old_file_chunk_size, progressCtx.curOffset, splitIndex, write_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } } - // Replace NCA header with our modified one - if (nca_offset == 0) memcpy(buf, xml_content_info[i].encrypted_header_mod, NCA_FULL_HEADER_LENGTH); + fclose(outFile); + outFile = NULL; - // Update CRC32 - if (calcCrc) crc32(buf, n, &crc); - - if (total_size > FAT32_FILESIZE_LIMIT && isFat32 && (nsp_file_offset + n) < total_size && (nsp_file_offset + n) >= ((splitIndex + 1) * SPLIT_FILE_NSP_PART_SIZE)) + if (new_file_chunk_size > 0) { - u64 new_file_chunk_size = ((nsp_file_offset + n) - ((splitIndex + 1) * SPLIT_FILE_NSP_PART_SIZE)); - u64 old_file_chunk_size = (n - new_file_chunk_size); - - if (old_file_chunk_size > 0) - { - write_res = fwrite(buf, 1, old_file_chunk_size, outFile); - if (write_res != old_file_chunk_size) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", old_file_chunk_size, nsp_file_offset, splitIndex, write_res); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); - proceed = false; - break; - } - } - - fclose(outFile); - splitIndex++; - snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s v%u (%016lX).nsp/%02u", fixedGameCardName[appIndex], gameCardVersion[appIndex], gameCardTitleID[appIndex], splitIndex); + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s.nsp/%02u", dumpName, splitIndex); outFile = fopen(dumpPath, "wb"); if (!outFile) { + setProgressBarError(&progressCtx); snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file for part #%u!", splitIndex); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); - proceed = false; - break; + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; } - if (new_file_chunk_size > 0) + uiFill(0, ((progressCtx.line_offset - 4) * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)), BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", dumpPath); + uiDrawString(strbuf, 8, ((progressCtx.line_offset - 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + uiRefreshDisplay(); + + write_res = fwrite(cnmtNcaBuf + old_file_chunk_size, 1, new_file_chunk_size, outFile); + if (write_res != new_file_chunk_size) { - write_res = fwrite(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); - if (write_res != new_file_chunk_size) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, nsp_file_offset + old_file_chunk_size, splitIndex, write_res); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); - proceed = false; - break; - } - } - } else { - write_res = fwrite(buf, 1, n, outFile); - if (write_res != n) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", n, nsp_file_offset, write_res); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); - - if ((nsp_file_offset + n) > FAT32_FILESIZE_LIMIT) - { - uiDrawString("You're probably using a FAT32 partition. Make sure to enable the \"Split output dump\" option.", 0, (breaks + 7) * font_height, 255, 255, 255); - fat32_error = true; - } - - proceed = false; - break; + setProgressBarError(&progressCtx); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes CNMT NCA chunk #2 from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, write_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; } } - - timeGetCurrentTime(TimeType_LocalSystemClock, &now); - - lastSpeed = (((double)(nsp_file_offset + n) / (double)DUMP_BUFFER_SIZE) / difftime((time_t)now, (time_t)start)); - averageSpeed = ((SMOOTHING_FACTOR * lastSpeed) + ((1 - SMOOTHING_FACTOR) * averageSpeed)); - if (!isnormal(averageSpeed)) averageSpeed = 0.00; // Very low values - - remainingTime = (u64)(((double)(total_size - (nsp_file_offset + n)) / (double)DUMP_BUFFER_SIZE) / averageSpeed); - timeinfo = localtime((time_t*)&remainingTime); - strftime(etaInfo, sizeof(etaInfo) / sizeof(etaInfo[0]), "%HH%MM%SS", timeinfo); - - progress = (u8)(((nsp_file_offset + n) * 100) / total_size); - - uiFill(0, ((breaks + 2) * font_height) - 4, FB_WIDTH / 4, font_height + 8, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%.2lf MiB/s [ETA: %s]", averageSpeed, etaInfo); - uiDrawString(strbuf, font_height * 2, (breaks + 2) * font_height, 255, 255, 255); - - uiFill(FB_WIDTH / 4, ((breaks + 2) * font_height) + 2, FB_WIDTH / 2, font_height, 0, 0, 0); - uiFill(FB_WIDTH / 4, ((breaks + 2) * font_height) + 2, (((nsp_file_offset + n) * (FB_WIDTH / 2)) / total_size), font_height, 0, 255, 0); - - uiFill(FB_WIDTH - (FB_WIDTH / 4), ((breaks + 2) * font_height) - 4, FB_WIDTH / 4, font_height + 8, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - convertSize(nsp_file_offset + n, curSizeStr, sizeof(curSizeStr) / sizeof(curSizeStr[0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%u%% [%s / %s]", progress, curSizeStr, totalSizeStr); - uiDrawString(strbuf, FB_WIDTH - (FB_WIDTH / 4) + (font_height * 2), (breaks + 2) * font_height, 255, 255, 255); - - uiRefreshDisplay(); - - if ((nsp_file_offset + n) < total_size && ((nsp_file_offset / DUMP_BUFFER_SIZE) % 10) == 0) + } else { + write_res = fwrite(cnmtNcaBuf, 1, xml_content_info[cnmtNcaIndex].size, outFile); + if (write_res != xml_content_info[cnmtNcaIndex].size) { - hidScanInput(); - - u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO); - if (keysDown & KEY_B) - { - uiDrawString("Process canceled", 0, (breaks + 5) * font_height, 255, 0, 0); - proceed = false; - break; - } + setProgressBarError(&progressCtx); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes CNMT NCA to file offset 0x%016lX! (wrote %lu bytes)", xml_content_info[cnmtNcaIndex].size, progressCtx.curOffset, write_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; } } + } else { + fseek(outFile, 0, SEEK_END); - if (!proceed) + write_res = fwrite(cnmtNcaBuf, 1, xml_content_info[cnmtNcaIndex].size, outFile); + if (write_res != xml_content_info[cnmtNcaIndex].size) { - uiFill(FB_WIDTH / 4, ((breaks + 2) * font_height) + 2, (((nsp_file_offset + n) * (FB_WIDTH / 2)) / total_size), font_height, 255, 0, 0); - break; - } - - // Support empty files - if (!xml_content_info[i].size) - { - uiFill(FB_WIDTH / 4, ((breaks + 2) * font_height) + 2, ((nsp_file_offset * (FB_WIDTH / 2)) / total_size), font_height, 0, 255, 0); + setProgressBarError(&progressCtx); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes CNMT NCA to file offset 0x%016lX! (wrote %lu bytes)", xml_content_info[cnmtNcaIndex].size, progressCtx.curOffset, write_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - uiFill(FB_WIDTH - (FB_WIDTH / 4), ((breaks + 2) * font_height) - 4, FB_WIDTH / 4, font_height + 8, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - convertSize(nsp_file_offset, curSizeStr, sizeof(curSizeStr) / sizeof(curSizeStr[0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%u%% [%s / %s]", progress, curSizeStr, totalSizeStr); - uiDrawString(strbuf, FB_WIDTH - (FB_WIDTH / 4) + (font_height * 2), (breaks + 2) * font_height, 255, 255, 255); + if ((progressCtx.curOffset + xml_content_info[cnmtNcaIndex].size) > FAT32_FILESIZE_LIMIT) + { + breaks += 2; + uiDrawString("You're probably using a FAT32 partition. Make sure to enable the \"Split output dump\" option.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + } - uiRefreshDisplay(); + goto out; } } - if (!proceed) goto out; + progressCtx.curOffset += xml_content_info[cnmtNcaIndex].size; - // Write our metadata XML - uiFill(0, breaks * font_height, FB_WIDTH, font_height * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Writing metadata XML \"%s.cnmt.xml\"...", xml_content_info[cnmtNcaIndex].nca_id_str); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - - write_res = fwrite(metadataXml, 1, strlen(metadataXml), outFile); - if (write_res != strlen(metadataXml)) + if (progressCtx.curOffset < progressCtx.totalSize) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes metadata XML to file offset 0x%016lX! (wrote %lu bytes)", strlen(metadataXml), nsp_file_offset, write_res); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); - goto out; - } - - nsp_file_offset += strlen(metadataXml); - - if (nsp_file_offset < total_size) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Unexpected underdump error! Wrote %lu bytes, expected %lu bytes.", nsp_file_offset, total_size); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); + setProgressBarError(&progressCtx); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Unexpected underdump error! Wrote %lu bytes, expected %lu bytes.", progressCtx.curOffset, progressCtx.totalSize); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); goto out; } success = true; - // Update CRC32 - if (calcCrc) crc32(metadataXml, strlen(metadataXml), &crc); - - // Update progress - remainingTime = 0; - timeinfo = localtime((time_t*)&remainingTime); - strftime(etaInfo, sizeof(etaInfo) / sizeof(etaInfo[0]), "%HH%MM%SS", timeinfo); - - progress = 100; - - uiFill(FB_WIDTH / 4, ((breaks + 2) * font_height) + 2, FB_WIDTH / 2, font_height, 0, 255, 0); - - uiFill(FB_WIDTH - (FB_WIDTH / 4), ((breaks + 2) * font_height) - 4, FB_WIDTH / 4, font_height + 8, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - convertSize(nsp_file_offset, curSizeStr, sizeof(curSizeStr) / sizeof(curSizeStr[0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%u%% [%s / %s]", progress, curSizeStr, totalSizeStr); - uiDrawString(strbuf, FB_WIDTH - (FB_WIDTH / 4) + (font_height * 2), (breaks + 2) * font_height, 255, 255, 255); - uiRefreshDisplay(); - - breaks += 5; - // Finalize dump - now -= start; - timeinfo = localtime((time_t*)&now); - strftime(etaInfo, sizeof(etaInfo) / sizeof(etaInfo[0]), "%HH%MM%SS", timeinfo); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", etaInfo); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now)); + progressCtx.now -= progressCtx.start; + + progressCtx.progress = 100; + progressCtx.remainingTime = 0; + + printProgressBar(&progressCtx, false, 0); + + formatETAString(progressCtx.now, progressCtx.etaInfo, sizeof(progressCtx.etaInfo) / sizeof(progressCtx.etaInfo[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", progressCtx.etaInfo); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + + uiRefreshDisplay(); if (calcCrc) { - breaks++; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "NSP dump CRC32 checksum: %08X", crc); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); - breaks++; + breaks += 2; + uiDrawString("CRC32 checksum calculation will begin in 5 seconds...", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + uiRefreshDisplay(); + + delay(5); + + breaks = initial_breaks; + uiFill(0, (breaks * (font_height + (font_height / 4))) + 8, FB_WIDTH, FB_HEIGHT - (breaks * (font_height + (font_height / 4))), BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Calculating CRC32 checksum. Hold %s to cancel.", NINTENDO_FONT_B); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks += 2; + + if (outFile) + { + fclose(outFile); + outFile = NULL; + } + + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s.nsp", dumpName); + + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32) + { + splitIndex = 0; + sprintf(tmp_idx, "/%02u", splitIndex); + strcat(dumpPath, tmp_idx); + } + + outFile = fopen(dumpPath, "rb"); + if (outFile) + { + n = DUMP_BUFFER_SIZE; + progressCtx.start = progressCtx.now = progressCtx.remainingTime = 0; + progressCtx.lastSpeed = progressCtx.averageSpeed = 0.0; + + size_t read_res; + + progressCtx.line_offset = (breaks + 2); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start)); + + for(progressCtx.curOffset = 0; progressCtx.curOffset < progressCtx.totalSize; progressCtx.curOffset += n) + { + uiFill(0, (breaks * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 2, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "File: \"%s\".", dumpPath); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + if (DUMP_BUFFER_SIZE > (progressCtx.totalSize - progressCtx.curOffset)) n = (progressCtx.totalSize - progressCtx.curOffset); + + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32 && (progressCtx.curOffset + n) >= ((splitIndex + 1) * SPLIT_FILE_NSP_PART_SIZE)) + { + u64 new_file_chunk_size = ((progressCtx.curOffset + n) - ((splitIndex + 1) * SPLIT_FILE_NSP_PART_SIZE)); + u64 old_file_chunk_size = (n - new_file_chunk_size); + + if (old_file_chunk_size > 0) + { + read_res = fread(buf, 1, old_file_chunk_size, outFile); + if (read_res != old_file_chunk_size) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to read %lu bytes chunk from offset 0x%016lX from part #%02u! (read %lu bytes)", old_file_chunk_size, progressCtx.curOffset, splitIndex, read_res); + uiDrawString(strbuf, 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + } + + fclose(outFile); + outFile = NULL; + + if (new_file_chunk_size > 0 || (progressCtx.curOffset + n) < progressCtx.totalSize) + { + splitIndex++; + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s.nsp/%02u", dumpName, splitIndex); + + outFile = fopen(dumpPath, "rb"); + if (!outFile) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to re-open output file for part #%u!", splitIndex); + uiDrawString(strbuf, 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + + if (new_file_chunk_size > 0) + { + read_res = fread(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); + if (read_res != new_file_chunk_size) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to read %lu bytes chunk from offset 0x%016lX from part #%02u! (read %lu bytes)", new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, read_res); + uiDrawString(strbuf, 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + } + } + } else { + read_res = fread(buf, 1, n, outFile); + if (read_res != n) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to read %lu bytes chunk from offset 0x%016lX! (read %lu bytes)", n, progressCtx.curOffset, read_res); + uiDrawString(strbuf, 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + } + + // Update CRC32 + crc32(buf, n, &crc); + + printProgressBar(&progressCtx, true, n); + + if ((progressCtx.curOffset + n) < progressCtx.totalSize && ((progressCtx.curOffset / DUMP_BUFFER_SIZE) % 10) == 0) + { + hidScanInput(); + + u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO); + if (keysDown & KEY_B) + { + uiDrawString("Process canceled.", 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + proceed = false; + break; + } + } + } + + breaks += 4; + + if (proceed) + { + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now)); + progressCtx.now -= progressCtx.start; + + formatETAString(progressCtx.now, progressCtx.etaInfo, sizeof(progressCtx.etaInfo) / sizeof(progressCtx.etaInfo[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", progressCtx.etaInfo); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + breaks++; + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "NSP dump CRC32 checksum: %08X", crc); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + } else { + setProgressBarError(&progressCtx); + } + } else { + uiDrawString("Failed to re-open output file in read mode!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } } // Set archive bit (only for FAT32) - if (total_size > FAT32_FILESIZE_LIMIT && isFat32) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32) { - snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s v%u (%016lX).nsp", fixedGameCardName[appIndex], gameCardVersion[appIndex], gameCardTitleID[appIndex]); + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s.nsp", dumpName); if (R_FAILED(result = fsdevSetArchiveBit(dumpPath))) { - breaks++; + breaks += 2; snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Warning: failed to set archive bit on output directory! (0x%08X)", result); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); - breaks++; + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } @@ -1224,16 +1696,19 @@ out: if (!success) { - breaks += 5; - if (fat32_error) breaks += 2; + if (dumping) + { + breaks += 6; + if (fat32_error) breaks += 2; + } - snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s v%u (%016lX).nsp", fixedGameCardName[appIndex], gameCardVersion[appIndex], gameCardTitleID[appIndex]); + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s.nsp", dumpName); - if (total_size > FAT32_FILESIZE_LIMIT && isFat32) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && isFat32) { removeDirectory(dumpPath); } else { - remove(dumpPath); + unlink(dumpPath); } } @@ -1241,59 +1716,73 @@ out: if (nspPfs0EntryTable) free(nspPfs0EntryTable); - if (metadataXml) free(metadataXml); + if (certData) free(certData); - if (appCnmtContentRecords) free(appCnmtContentRecords); + if (tikData) free(tikData); - if (cnmt_pfs0_entries) free(cnmt_pfs0_entries); + if (cnmtXml) free(cnmtXml); if (cnmtNcaBuf) free(cnmtNcaBuf); + if (ncaProgramMod.block_mod_cnt == 2 && ncaProgramMod.block_data[1]) free(ncaProgramMod.block_data[1]); + + if (ncaProgramMod.block_data[0]) free(ncaProgramMod.block_data[0]); + + if (ncaProgramMod.hash_table) free(ncaProgramMod.hash_table); + + if (nacpXml) free(nacpXml); + + serviceClose(&(ncmStorage.s)); + if (xml_content_info) free(xml_content_info); - if (appContentRecords) free(appContentRecords); + if (titleContentRecords) free(titleContentRecords); - if (appList) free(appList); + serviceClose(&(ncmDb.s)); + + if (titleList) free(titleList); + + fsStorageClose(&gameCardStorage); if (partitionHfs0Header) { free(partitionHfs0Header); partitionHfs0Header = NULL; + partitionHfs0HeaderOffset = 0; partitionHfs0HeaderSize = 0; partitionHfs0FileCount = 0; partitionHfs0StrTableSize = 0; } + free(dumpName); + breaks += 2; return success; } -bool dumpRawPartition(FsDeviceOperator* fsOperator, u32 partition, bool doSplitting) +bool dumpRawHfs0Partition(FsDeviceOperator* fsOperator, u32 partition, bool doSplitting) { Result result; - u64 size, partitionOffset; + u64 partitionOffset; bool success = false, fat32_error = false; - char *buf; - u64 off, n = DUMP_BUFFER_SIZE; + u8 *buf = NULL; + u64 n = DUMP_BUFFER_SIZE; FsGameCardHandle handle; FsStorage gameCardStorage; - char totalSizeStr[32] = {'\0'}, curSizeStr[32] = {'\0'}, filename[NAME_BUF_LEN * 2] = {'\0'}; - u8 progress = 0; + char filename[NAME_BUF_LEN * 2] = {'\0'}; FILE *outFile = NULL; u8 splitIndex = 0; - u64 start, now, remainingTime; - struct tm *timeinfo; - char etaInfo[32] = {'\0'}; - double lastSpeed = 0.0, averageSpeed = 0.0; + progress_ctx_t progressCtx; + memset(&progressCtx, 0, sizeof(progress_ctx_t)); size_t write_res; - char *dumpName = generateDumpName(); + char *dumpName = generateDumpFullName(); if (!dumpName) { - uiDrawString("Error: unable to generate output dump name!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: unable to generate output dump name!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); breaks += 2; return false; } @@ -1303,7 +1792,7 @@ bool dumpRawPartition(FsDeviceOperator* fsOperator, u32 partition, bool doSplitt if (R_SUCCEEDED(result = fsDeviceOperatorGetGameCardHandle(fsOperator, &handle))) { /*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "GetGameCardHandle succeeded: 0x%08X", handle.value); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++;*/ // Ugly hack @@ -1318,23 +1807,23 @@ bool dumpRawPartition(FsDeviceOperator* fsOperator, u32 partition, bool doSplitt 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.value); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++;*/ - if (getHfs0EntryDetails(hfs0_header, hfs0_offset, hfs0_size, hfs0_partition_cnt, partition, true, 0, &partitionOffset, &size)) + if (getHfs0EntryDetails(hfs0_header, hfs0_offset, hfs0_size, hfs0_partition_cnt, partition, true, 0, &partitionOffset, &(progressCtx.totalSize))) { - convertSize(size, totalSizeStr, sizeof(totalSizeStr) / sizeof(totalSizeStr[0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Partition size: %s (%lu bytes).", totalSizeStr, size); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks++; + convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, sizeof(progressCtx.totalSizeStr) / sizeof(progressCtx.totalSizeStr[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "HFS0 partition size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks += 2; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Partition offset (relative to IStorage instance): 0x%016lX", partitionOffset); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks++; + /*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "HFS0 partition offset (relative to IStorage instance): 0x%016lX", partitionOffset); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks += 2;*/ - if (size <= freeSpace) + if (progressCtx.totalSize <= freeSpace) { - if (size > FAT32_FILESIZE_LIMIT && doSplitting) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting) { snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s - Partition %u (%s).hfs0.%02u", dumpName, partition, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, partition), splitIndex); } else { @@ -1344,37 +1833,43 @@ bool dumpRawPartition(FsDeviceOperator* fsOperator, u32 partition, bool doSplitt outFile = fopen(filename, "wb"); if (outFile) { - buf = (char*)malloc(DUMP_BUFFER_SIZE); + buf = malloc(DUMP_BUFFER_SIZE); if (buf) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping raw HFS0 partition #%u to \"%.*s\". Hold B to cancel.", partition, (int)((size > FAT32_FILESIZE_LIMIT && doSplitting) ? (strlen(filename) - 3) : strlen(filename)), filename); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping raw HFS0 partition #%u. Hold %s to cancel.", partition, NINTENDO_FONT_B); + 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 HOME button. Doing so could corrupt the SD card filesystem.", 0, breaks * font_height, 255, 0, 0); + 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(); - timeGetCurrentTime(TimeType_LocalSystemClock, &start); + progressCtx.line_offset = (breaks + 2); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start)); - for (off = 0; off < size; off += n) + for (progressCtx.curOffset = 0; progressCtx.curOffset < progressCtx.totalSize; progressCtx.curOffset += n) { - if (DUMP_BUFFER_SIZE > (size - off)) n = (size - off); + uiFill(0, (breaks * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 2, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - if (R_FAILED(result = fsStorageRead(&gameCardStorage, partitionOffset + off, buf, n))) + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", filename); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + if (DUMP_BUFFER_SIZE > (progressCtx.totalSize - progressCtx.curOffset)) n = (progressCtx.totalSize - progressCtx.curOffset); + + if (R_FAILED(result = fsStorageRead(&gameCardStorage, partitionOffset + progressCtx.curOffset, buf, n))) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "StorageRead failed (0x%08X) at offset 0x%016lX", result, partitionOffset + off); - uiDrawString(strbuf, 0, (breaks + 3) * font_height, 255, 0, 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "StorageRead failed (0x%08X) at offset 0x%016lX", result, partitionOffset + progressCtx.curOffset); + uiDrawString(strbuf, 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); break; } - if (size > FAT32_FILESIZE_LIMIT && doSplitting && (off + n) < size && (off + n) >= ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting && (progressCtx.curOffset + n) >= ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)) { - u64 new_file_chunk_size = ((off + n) - ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)); + u64 new_file_chunk_size = ((progressCtx.curOffset + n) - ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)); u64 old_file_chunk_size = (n - new_file_chunk_size); if (old_file_chunk_size > 0) @@ -1382,45 +1877,49 @@ bool dumpRawPartition(FsDeviceOperator* fsOperator, u32 partition, bool doSplitt write_res = fwrite(buf, 1, old_file_chunk_size, outFile); if (write_res != old_file_chunk_size) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", old_file_chunk_size, off, splitIndex, write_res); - uiDrawString(strbuf, 0, (breaks + 3) * font_height, 255, 0, 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", old_file_chunk_size, progressCtx.curOffset, splitIndex, write_res); + uiDrawString(strbuf, 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); break; } } fclose(outFile); + outFile = NULL; - splitIndex++; - snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s - Partition %u (%s).hfs0.%02u", dumpName, partition, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, partition), splitIndex); - - outFile = fopen(filename, "wb"); - if (!outFile) + if (new_file_chunk_size > 0 || (progressCtx.curOffset + n) < progressCtx.totalSize) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file for part #%u!", splitIndex); - uiDrawString(strbuf, 0, (breaks + 3) * font_height, 255, 0, 0); - break; - } - - if (new_file_chunk_size > 0) - { - write_res = fwrite(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); - if (write_res != new_file_chunk_size) + splitIndex++; + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s - Partition %u (%s).hfs0.%02u", dumpName, partition, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, partition), splitIndex); + + outFile = fopen(filename, "wb"); + if (!outFile) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, off + old_file_chunk_size, splitIndex, write_res); - uiDrawString(strbuf, 0, (breaks + 3) * font_height, 255, 0, 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file for part #%u!", splitIndex); + uiDrawString(strbuf, 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); break; } + + if (new_file_chunk_size > 0) + { + write_res = fwrite(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); + if (write_res != new_file_chunk_size) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, write_res); + uiDrawString(strbuf, 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + break; + } + } } } else { write_res = fwrite(buf, 1, n, outFile); if (write_res != n) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", n, off, write_res); - uiDrawString(strbuf, 0, (breaks + 3) * font_height, 255, 0, 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", n, progressCtx.curOffset, write_res); + uiDrawString(strbuf, 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); - if ((off + n) > FAT32_FILESIZE_LIMIT) + if ((progressCtx.curOffset + n) > FAT32_FILESIZE_LIMIT) { - uiDrawString("You're probably using a FAT32 partition. Make sure to enable file splitting.", 0, (breaks + 5) * font_height, 255, 255, 255); + uiDrawString("You're probably using a FAT32 partition. Make sure to enable file splitting.", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); fat32_error = true; } @@ -1428,111 +1927,90 @@ bool dumpRawPartition(FsDeviceOperator* fsOperator, u32 partition, bool doSplitt } } - timeGetCurrentTime(TimeType_LocalSystemClock, &now); + printProgressBar(&progressCtx, true, n); - lastSpeed = (((double)(off + n) / (double)DUMP_BUFFER_SIZE) / difftime((time_t)now, (time_t)start)); - averageSpeed = ((SMOOTHING_FACTOR * lastSpeed) + ((1 - SMOOTHING_FACTOR) * averageSpeed)); - if (!isnormal(averageSpeed)) averageSpeed = 0.00; // Very low values - - remainingTime = (u64)(((double)(size - (off + n)) / (double)DUMP_BUFFER_SIZE) / averageSpeed); - timeinfo = localtime((time_t*)&remainingTime); - strftime(etaInfo, sizeof(etaInfo) / sizeof(etaInfo[0]), "%HH%MM%SS", timeinfo); - - progress = (u8)(((off + n) * 100) / size); - - uiFill(0, (breaks * font_height) - 4, FB_WIDTH / 4, font_height + 8, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%.2lf MiB/s [ETA: %s]", averageSpeed, etaInfo); - uiDrawString(strbuf, font_height * 2, breaks * font_height, 255, 255, 255); - - uiFill(FB_WIDTH / 4, (breaks * font_height) + 2, FB_WIDTH / 2, font_height, 0, 0, 0); - uiFill(FB_WIDTH / 4, (breaks * font_height) + 2, (((off + n) * (FB_WIDTH / 2)) / size), font_height, 0, 255, 0); - - uiFill(FB_WIDTH - (FB_WIDTH / 4), (breaks * font_height) - 4, FB_WIDTH / 4, font_height + 8, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - convertSize(off + n, curSizeStr, sizeof(curSizeStr) / sizeof(curSizeStr[0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%u%% [%s / %s]", progress, curSizeStr, totalSizeStr); - uiDrawString(strbuf, FB_WIDTH - (FB_WIDTH / 4) + (font_height * 2), breaks * font_height, 255, 255, 255); - - uiRefreshDisplay(); - - if ((off + n) < size && ((off / DUMP_BUFFER_SIZE) % 10) == 0) + if ((progressCtx.curOffset + n) < progressCtx.totalSize && ((progressCtx.curOffset / DUMP_BUFFER_SIZE) % 10) == 0) { hidScanInput(); u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO); if (keysDown & KEY_B) { - uiDrawString("Process canceled", 0, (breaks + 3) * font_height, 255, 0, 0); + uiDrawString("Process canceled.", 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); break; } } } - if (off >= size) success = true; + if (progressCtx.curOffset >= progressCtx.totalSize) success = true; // Support empty files - if (!size) + if (!progressCtx.totalSize) { - uiFill(FB_WIDTH / 4, (breaks * font_height) + 2, FB_WIDTH / 2, font_height, 0, 255, 0); + uiFill(0, (breaks * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 2, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - uiFill(FB_WIDTH - (FB_WIDTH / 4), (breaks * font_height) - 4, FB_WIDTH / 4, font_height + 8, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - uiDrawString("100%% [0 B / 0 B]", FB_WIDTH - (FB_WIDTH / 4) + (font_height * 2), breaks * font_height, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", filename); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - uiRefreshDisplay(); + progressCtx.progress = 100; + + printProgressBar(&progressCtx, false, 0); } + breaks += 4; + if (success) { - now -= start; - timeinfo = localtime((time_t*)&now); - strftime(etaInfo, sizeof(etaInfo) / sizeof(etaInfo[0]), "%HH%MM%SS", timeinfo); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", etaInfo); - uiDrawString(strbuf, 0, (breaks + 3) * font_height, 0, 255, 0); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now)); + progressCtx.now -= progressCtx.start; + + formatETAString(progressCtx.now, progressCtx.etaInfo, sizeof(progressCtx.etaInfo) / sizeof(progressCtx.etaInfo[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", progressCtx.etaInfo); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); } else { - uiFill(FB_WIDTH / 4, (breaks * font_height) + 2, (((off + n) * (FB_WIDTH / 2)) / size), font_height, 255, 0, 0); + setProgressBarError(&progressCtx); + if (fat32_error) breaks += 2; } - breaks += 3; - if (fat32_error) breaks += 2; - free(buf); } else { - uiDrawString("Failed to allocate memory for the dump process!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Failed to allocate memory for the dump process!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } if (outFile) fclose(outFile); if (!success) { - if (size > FAT32_FILESIZE_LIMIT && doSplitting) + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting) { for(u8 i = 0; i <= splitIndex; i++) { snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s - Partition %u (%s).hfs0.%02u", dumpName, partition, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, partition), i); - remove(filename); + unlink(filename); } } else { - remove(filename); + unlink(filename); } } } else { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file \"%s\"!", filename); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } else { - uiDrawString("Error: not enough free space available in the SD card.", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: not enough free space available in the SD card.", 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!", 0, breaks * font_height, 255, 0, 0); + 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); } fsStorageClose(&gameCardStorage); } else { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "OpenGameCardStorage failed! (0x%08X)", result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + 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, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } breaks += 2; @@ -1542,74 +2020,69 @@ bool dumpRawPartition(FsDeviceOperator* fsOperator, u32 partition, bool doSplitt return success; } -bool copyFileFromHfs0(FsDeviceOperator* fsOperator, u32 partition, const char* source, const char* dest, const u64 file_offset, const u64 size, bool doSplitting, bool calcEta) +bool copyFileFromHfs0(FsDeviceOperator* fsOperator, u32 partition, const char* source, const char* dest, const u64 file_offset, const u64 size, progress_ctx_t *progressCtx, bool doSplitting) { Result result; bool success = false, fat32_error = false; char splitFilename[NAME_BUF_LEN] = {'\0'}; size_t destLen = strlen(dest); FILE *outFile = NULL; - char *buf = NULL; + u8 *buf = NULL; u64 off, n = DUMP_BUFFER_SIZE; u8 splitIndex = 0; - u8 progress = 0; - char totalSizeStr[32] = {'\0'}, curSizeStr[32] = {'\0'}; FsGameCardHandle handle; FsStorage gameCardStorage; - u64 start, now, remainingTime; - struct tm *timeinfo; - char etaInfo[32] = {'\0'}; - double lastSpeed = 0.0, averageSpeed = 0.0; - size_t write_res; - uiFill(0, breaks * font_height, FB_WIDTH, font_height * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + uiFill(0, (breaks * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Copying \"%s\"...", source); - uiDrawString(strbuf, 0, (breaks + 1) * font_height, 255, 255, 255); - uiRefreshDisplay(); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); if ((destLen + 1) < NAME_BUF_LEN) { if (R_SUCCEEDED(result = fsDeviceOperatorGetGameCardHandle(fsOperator, &handle))) { /*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "GetGameCardHandle succeeded: 0x%08X", handle.value); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++;*/ - // Same ugly hack from dumpRawPartition() + // 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.value); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++;*/ - convertSize(size, totalSizeStr, sizeof(totalSizeStr) / sizeof(totalSizeStr[0])); - if (size > FAT32_FILESIZE_LIMIT && doSplitting) snprintf(splitFilename, sizeof(splitFilename) / sizeof(splitFilename[0]), "%s.%02u", dest, splitIndex); outFile = fopen(((size > FAT32_FILESIZE_LIMIT && doSplitting) ? splitFilename : dest), "wb"); if (outFile) { - buf = (char*)malloc(DUMP_BUFFER_SIZE); + buf = malloc(DUMP_BUFFER_SIZE); if (buf) { - if (calcEta) timeGetCurrentTime(TimeType_LocalSystemClock, &start); - - for (off = 0; off < size; off += n) + for (off = 0; off < size; off += n, progressCtx->curOffset += n) { + uiFill(0, ((breaks + 2) * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 2, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", ((size > FAT32_FILESIZE_LIMIT && doSplitting) ? (strrchr(splitFilename, '/') + 1) : (strrchr(dest, '/') + 1))); + uiDrawString(strbuf, 8, ((breaks + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + uiRefreshDisplay(); + if (DUMP_BUFFER_SIZE > (size - off)) n = (size - off); if (R_FAILED(result = fsStorageRead(&gameCardStorage, file_offset + off, buf, n))) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "StorageRead failed (0x%08X) at offset 0x%016lX", result, file_offset + off); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); break; } - if (size > FAT32_FILESIZE_LIMIT && doSplitting && (off + n) < size && (off + n) >= ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)) + if (size > FAT32_FILESIZE_LIMIT && doSplitting && (off + n) >= ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)) { u64 new_file_chunk_size = ((off + n) - ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)); u64 old_file_chunk_size = (n - new_file_chunk_size); @@ -1620,44 +2093,48 @@ bool copyFileFromHfs0(FsDeviceOperator* fsOperator, u32 partition, const char* s if (write_res != old_file_chunk_size) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", old_file_chunk_size, off, splitIndex, write_res); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); break; } } fclose(outFile); + outFile = NULL; - splitIndex++; - snprintf(splitFilename, sizeof(splitFilename) / sizeof(splitFilename[0]), "%s.%02u", dest, splitIndex); - - outFile = fopen(splitFilename, "wb"); - if (!outFile) + if (new_file_chunk_size > 0 || (off + n) < size) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file for part #%u!", splitIndex); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); - break; - } - - if (new_file_chunk_size > 0) - { - write_res = fwrite(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); - if (write_res != new_file_chunk_size) + splitIndex++; + snprintf(splitFilename, sizeof(splitFilename) / sizeof(splitFilename[0]), "%s.%02u", dest, splitIndex); + + outFile = fopen(splitFilename, "wb"); + if (!outFile) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, off + old_file_chunk_size, splitIndex, write_res); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file for part #%u!", splitIndex); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); break; } + + if (new_file_chunk_size > 0) + { + write_res = fwrite(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); + if (write_res != new_file_chunk_size) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, off + old_file_chunk_size, splitIndex, write_res); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + break; + } + } } } else { write_res = fwrite(buf, 1, n, outFile); if (write_res != n) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", n, off, write_res); - uiDrawString(strbuf, 0, (breaks + 5) * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); if ((off + n) > FAT32_FILESIZE_LIMIT) { - uiDrawString("You're probably using a FAT32 partition. Make sure to enable file splitting.", 0, (breaks + 7) * font_height, 255, 255, 255); + uiDrawString("You're probably using a FAT32 partition. Make sure to enable file splitting.", 8, ((breaks + 8) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); fat32_error = true; } @@ -1665,34 +2142,7 @@ bool copyFileFromHfs0(FsDeviceOperator* fsOperator, u32 partition, const char* s } } - if (calcEta) - { - timeGetCurrentTime(TimeType_LocalSystemClock, &now); - - lastSpeed = (((double)(off + n) / (double)DUMP_BUFFER_SIZE) / difftime((time_t)now, (time_t)start)); - averageSpeed = ((SMOOTHING_FACTOR * lastSpeed) + ((1 - SMOOTHING_FACTOR) * averageSpeed)); - if (!isnormal(averageSpeed)) averageSpeed = 0.00; // Very low values - - remainingTime = (u64)(((double)(size - (off + n)) / (double)DUMP_BUFFER_SIZE) / averageSpeed); - timeinfo = localtime((time_t*)&remainingTime); - strftime(etaInfo, sizeof(etaInfo) / sizeof(etaInfo[0]), "%HH%MM%SS", timeinfo); - - uiFill(0, ((breaks + 3) * font_height) - 4, FB_WIDTH / 4, font_height + 8, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%.2lf MiB/s [ETA: %s]", averageSpeed, etaInfo); - uiDrawString(strbuf, font_height * 2, (breaks + 3) * font_height, 255, 255, 255); - } - - progress = (u8)(((off + n) * 100) / size); - - uiFill(FB_WIDTH / 4, ((breaks + 3) * font_height) + 2, FB_WIDTH / 2, font_height, 0, 0, 0); - uiFill(FB_WIDTH / 4, ((breaks + 3) * font_height) + 2, (((off + n) * (FB_WIDTH / 2)) / size), font_height, 0, 255, 0); - - uiFill(FB_WIDTH - (FB_WIDTH / 4), ((breaks + 3) * font_height) - 4, FB_WIDTH / 4, font_height + 8, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - convertSize(off + n, curSizeStr, sizeof(curSizeStr) / sizeof(curSizeStr[0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%u%% [%s / %s]", progress, curSizeStr, totalSizeStr); - uiDrawString(strbuf, FB_WIDTH - (FB_WIDTH / 4) + (font_height * 2), (breaks + 3) * font_height, 255, 255, 255); - - uiRefreshDisplay(); + printProgressBar(progressCtx, true, n); if ((off + n) < size && ((off / DUMP_BUFFER_SIZE) % 10) == 0) { @@ -1701,7 +2151,7 @@ bool copyFileFromHfs0(FsDeviceOperator* fsOperator, u32 partition, const char* s u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO); if (keysDown & KEY_B) { - uiDrawString("Process canceled", 0, (breaks + 5) * font_height, 255, 0, 0); + uiDrawString("Process canceled.", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); break; } } @@ -1712,89 +2162,80 @@ bool copyFileFromHfs0(FsDeviceOperator* fsOperator, u32 partition, const char* s // Support empty files if (!size) { - uiFill(FB_WIDTH / 4, ((breaks + 3) * font_height) + 2, FB_WIDTH / 2, font_height, 0, 255, 0); + uiFill(0, ((breaks + 2) * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 2, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - uiFill(FB_WIDTH - (FB_WIDTH / 4), ((breaks + 3) * font_height) - 4, FB_WIDTH / 4, font_height + 8, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - uiDrawString("100%% [0 B / 0 B]", FB_WIDTH - (FB_WIDTH / 4) + (font_height * 2), (breaks + 3) * font_height, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", ((size > FAT32_FILESIZE_LIMIT && doSplitting) ? (strrchr(splitFilename, '/') + 1) : (strrchr(dest, '/') + 1))); + uiDrawString(strbuf, 8, ((breaks + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); - uiRefreshDisplay(); + if (progressCtx->totalSize == size) progressCtx->progress = 100; + + printProgressBar(progressCtx, false, 0); } if (!success) { - uiFill(FB_WIDTH / 4, ((breaks + 3) * font_height) + 2, (((off + n) * (FB_WIDTH / 2)) / size), font_height, 255, 0, 0); - breaks += 5; + setProgressBarError(progressCtx); if (fat32_error) breaks += 2; } free(buf); } else { - breaks += 3; - uiDrawString("Failed to allocate memory for the dump process!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Failed to allocate memory for the dump process!", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } if (outFile) fclose(outFile); - if (success) + if (!success) { - if (calcEta) - { - breaks += 5; - - now -= start; - timeinfo = localtime((time_t*)&now); - strftime(etaInfo, sizeof(etaInfo) / sizeof(etaInfo[0]), "%HH%MM%SS", timeinfo); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", etaInfo); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); - } - } else { if (size > FAT32_FILESIZE_LIMIT && doSplitting) { for(u8 i = 0; i <= splitIndex; i++) { snprintf(splitFilename, sizeof(splitFilename) / sizeof(splitFilename[0]), "%s.%02u", dest, i); - remove(splitFilename); + unlink(splitFilename); } } else { - remove(dest); + unlink(dest); } } } else { - breaks += 3; - uiDrawString("Failed to open output file!", 0, breaks * font_height, 255, 255, 255); + uiDrawString("Failed to open output file!", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); } fsStorageClose(&gameCardStorage); } else { - breaks += 3; snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "OpenGameCardStorage failed! (0x%08X)", result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } else { - breaks += 3; snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "GetGameCardHandle failed! (0x%08X)", result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } else { - breaks += 3; snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Destination path is too long! (%lu bytes)", destLen); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } return success; } -bool copyHfs0Contents(FsDeviceOperator* fsOperator, u32 partition, hfs0_entry_table *partitionEntryTable, const char *dest, bool splitting) +bool copyHfs0Contents(FsDeviceOperator* fsOperator, u32 partition, hfs0_entry_table *partitionEntryTable, progress_ctx_t *progressCtx, const char *dest, bool splitting) { if (!dest || !*dest) { - uiDrawString("Error: destination directory is empty.", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: destination directory is empty.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); return false; } if (!partitionHfs0Header || !partitionEntryTable) { - uiDrawString("HFS0 partition header information unavailable!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("HFS0 partition header information unavailable!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (!progressCtx) + { + uiDrawString("Error: invalid progress context.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); return false; } @@ -1804,7 +2245,7 @@ bool copyHfs0Contents(FsDeviceOperator* fsOperator, u32 partition, hfs0_entry_ta if ((dest_len + 1) >= NAME_BUF_LEN) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Destination directory name is too long! (%lu bytes)", dest_len); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); return false; } @@ -1817,95 +2258,110 @@ bool copyHfs0Contents(FsDeviceOperator* fsOperator, u32 partition, hfs0_entry_ta u32 i; bool success; + progressCtx->line_offset = (breaks + 4); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx->start)); + for(i = 0; i < partitionHfs0FileCount; i++) { u32 filename_offset = (HFS0_ENTRY_TABLE_ADDR + (sizeof(hfs0_entry_table) * partitionHfs0FileCount) + partitionEntryTable[i].filename_offset); - char *filename = (partitionHfs0Header + filename_offset); + char *filename = ((char*)partitionHfs0Header + filename_offset); strcpy(dbuf + dest_len, filename); + removeIllegalCharacters(dbuf + dest_len); + u64 file_offset = (partitionHfs0HeaderSize + partitionEntryTable[i].file_offset); if (HFS0_TO_ISTORAGE_IDX(hfs0_partition_cnt, partition) == 0) file_offset += partitionHfs0HeaderOffset; - success = copyFileFromHfs0(fsOperator, partition, filename, dbuf, file_offset, partitionEntryTable[i].file_size, splitting, false); + success = copyFileFromHfs0(fsOperator, partition, filename, dbuf, file_offset, partitionEntryTable[i].file_size, progressCtx, splitting); if (!success) break; } return success; } -bool dumpPartitionData(FsDeviceOperator* fsOperator, u32 partition) +bool dumpHfs0PartitionData(FsDeviceOperator* fsOperator, u32 partition) { bool success = false; - u64 total_size = 0; u32 i; hfs0_entry_table *entryTable = NULL; - char dumpPath[NAME_BUF_LEN * 2] = {'\0'}, totalSizeStr[32] = {'\0'}; + char dumpPath[NAME_BUF_LEN * 2] = {'\0'}; - char *dumpName = generateDumpName(); + progress_ctx_t progressCtx; + memset(&progressCtx, 0, sizeof(progress_ctx_t)); + + char *dumpName = generateDumpFullName(); if (!dumpName) { - uiDrawString("Error: unable to generate output dump name!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: unable to generate output dump name!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); breaks += 2; return false; } - workaroundPartitionZeroAccess(fsOperator); + workaroundPartitionZeroAccess(fsOperator); if (getPartitionHfs0Header(partition)) { if (partitionHfs0FileCount) { - entryTable = (hfs0_entry_table*)malloc(sizeof(hfs0_entry_table) * partitionHfs0FileCount); + entryTable = calloc(partitionHfs0FileCount, sizeof(hfs0_entry_table)); if (entryTable) { memcpy(entryTable, partitionHfs0Header + HFS0_ENTRY_TABLE_ADDR, sizeof(hfs0_entry_table) * partitionHfs0FileCount); // Calculate total size - for(i = 0; i < partitionHfs0FileCount; i++) total_size += entryTable[i].file_size; + for(i = 0; i < partitionHfs0FileCount; i++) progressCtx.totalSize += entryTable[i].file_size; - convertSize(total_size, totalSizeStr, sizeof(totalSizeStr) / sizeof(totalSizeStr[0])); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Total partition data size: %s (%lu bytes).", totalSizeStr, total_size); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks++; + convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, sizeof(progressCtx.totalSizeStr) / sizeof(progressCtx.totalSizeStr[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Total partition data size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks += 2; - if (total_size <= freeSpace) + if (progressCtx.totalSize <= freeSpace) { snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s - Partition %u (%s)", dumpName, partition, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, partition)); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Copying partition #%u data to \"%s/\". Hold B to cancel.", partition, dumpPath); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Copying partition #%u data to \"%s/\". Hold %s to cancel.", partition, dumpPath, NINTENDO_FONT_B); + 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 HOME button. Doing so could corrupt the SD card filesystem.", 0, breaks * font_height, 255, 0, 0); + 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(); - success = copyHfs0Contents(fsOperator, partition, entryTable, dumpPath, true); + success = copyHfs0Contents(fsOperator, partition, entryTable, &progressCtx, dumpPath, true); + + breaks += 6; + if (success) { - breaks += 5; - uiDrawString("Process successfully completed!", 0, breaks * font_height, 0, 255, 0); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now)); + progressCtx.now -= progressCtx.start; + + formatETAString(progressCtx.now, progressCtx.etaInfo, sizeof(progressCtx.etaInfo) / sizeof(progressCtx.etaInfo[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", progressCtx.etaInfo); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); } else { removeDirectory(dumpPath); } } else { - uiDrawString("Error: not enough free space available in the SD card.", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: not enough free space available in the SD card.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } free(entryTable); } else { - uiDrawString("Unable to allocate memory for the HFS0 file entries!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Unable to allocate memory for the HFS0 file entries!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } else { - uiDrawString("The selected partition is empty!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("The selected partition is empty!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } free(partitionHfs0Header); partitionHfs0Header = NULL; + partitionHfs0HeaderOffset = 0; partitionHfs0HeaderSize = 0; partitionHfs0FileCount = 0; partitionHfs0StrTableSize = 0; @@ -1918,24 +2374,30 @@ bool dumpPartitionData(FsDeviceOperator* fsOperator, u32 partition) return success; } -bool dumpFileFromPartition(FsDeviceOperator* fsOperator, u32 partition, u32 file, char *filename) +bool dumpFileFromHfs0Partition(FsDeviceOperator* fsOperator, u32 partition, u32 file, char *filename) { if (!partitionHfs0Header) { - uiDrawString("HFS0 partition header information unavailable!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("HFS0 partition header information unavailable!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; return false; } if (!filename || !*filename) { - uiDrawString("Filename unavailable!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Filename unavailable!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; return false; } - char *dumpName = generateDumpName(); + progress_ctx_t progressCtx; + memset(&progressCtx, 0, sizeof(progress_ctx_t)); + + char *dumpName = generateDumpFullName(); if (!dumpName) { - uiDrawString("Error: unable to generate output dump name!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: unable to generate output dump name!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; return false; } @@ -1945,61 +2407,771 @@ bool dumpFileFromPartition(FsDeviceOperator* fsOperator, u32 partition, u32 file if (getHfs0EntryDetails(partitionHfs0Header, partitionHfs0HeaderOffset, partitionHfs0HeaderSize, partitionHfs0FileCount, file, false, partition, &file_offset, &file_size)) { + progressCtx.totalSize = file_size; + convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, sizeof(progressCtx.totalSizeStr) / sizeof(progressCtx.totalSizeStr[0])); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "File size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks++; + if (file_size <= freeSpace) { char destCopyPath[NAME_BUF_LEN * 2] = {'\0'}; + char fixedFilename[NAME_BUF_LEN] = {'\0'}; + + sprintf(fixedFilename, filename); + removeIllegalCharacters(fixedFilename); + snprintf(destCopyPath, sizeof(destCopyPath) / sizeof(destCopyPath[0]), "sdmc:/%s - Partition %u (%s)", dumpName, partition, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, partition)); if ((strlen(destCopyPath) + 1 + strlen(filename)) < NAME_BUF_LEN) { mkdir(destCopyPath, 0744); - snprintf(destCopyPath, sizeof(destCopyPath) / sizeof(destCopyPath[0]), "sdmc:/%s - Partition %u (%s)/%s", dumpName, partition, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, partition), filename); - uiDrawString("Hold B to cancel.", 0, breaks * font_height, 255, 255, 255); + snprintf(destCopyPath, sizeof(destCopyPath) / sizeof(destCopyPath[0]), "sdmc:/%s - Partition %u (%s)/%s", dumpName, partition, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, partition), fixedFilename); + + breaks++; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Hold %s to cancel.", NINTENDO_FONT_B); + 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 HOME button. Doing so could corrupt the SD card filesystem.", 0, breaks * font_height, 255, 0, 0); + 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(); - success = copyFileFromHfs0(fsOperator, partition, filename, destCopyPath, file_offset, file_size, true, true); + progressCtx.line_offset = (breaks + 4); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start)); + + success = copyFileFromHfs0(fsOperator, partition, filename, destCopyPath, file_offset, file_size, &progressCtx, true); + + breaks += 6; + + if (success) + { + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now)); + progressCtx.now -= progressCtx.start; + + formatETAString(progressCtx.now, progressCtx.etaInfo, sizeof(progressCtx.etaInfo) / sizeof(progressCtx.etaInfo[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", progressCtx.etaInfo); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + } } else { + breaks++; snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Destination path is too long! (%lu bytes)", strlen(destCopyPath) + 1 + strlen(filename)); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: not enough free space available in the SD card (%lu bytes required).", file_size); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: not enough free space available in the SD card.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } else { - uiDrawString("Error: unable to get file details from the partition HFS0 header!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: unable to get file details from the partition HFS0 header!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } free(dumpName); + breaks += 2; + + return success; +} + +bool recursiveDumpRomFsFile(u32 file_offset, char *romfs_path, char *output_path, progress_ctx_t *progressCtx, bool doSplitting) +{ + if (!romFsContext.romfs_filetable_size || file_offset > romFsContext.romfs_filetable_size || !romFsContext.romfs_file_entries || !romfs_path || !output_path || !progressCtx) + { + uiDrawString("Error: invalid parameters to parse file entry from RomFS section!", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + size_t orig_romfs_path_len = strlen(romfs_path); + size_t orig_output_path_len = strlen(output_path); + + u64 n = DUMP_BUFFER_SIZE; + FILE *outFile = NULL; + u8 *buf = NULL; + u8 splitIndex = 0; + bool proceed = true, success = false, fat32_error = false; + + u64 off = 0; + + size_t write_res; + + char tmp_idx[5]; + + romfs_file *entry = (romfs_file*)((u8*)romFsContext.romfs_file_entries + file_offset); + + // Check if we're dealing with a nameless file + if (!entry->nameLen) + { + uiDrawString("Error: file entry without name in RomFS section!", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if ((orig_romfs_path_len + 1 + entry->nameLen) >= (NAME_BUF_LEN * 2) || (orig_output_path_len + 1 + entry->nameLen) >= (NAME_BUF_LEN * 2)) + { + uiDrawString("Error: RomFS section file path is too long!", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + // Generate current path + strcat(romfs_path, "/"); + strncat(romfs_path, (char*)entry->name, entry->nameLen); + + strcat(output_path, "/"); + strncat(output_path, (char*)entry->name, entry->nameLen); + removeIllegalCharacters(output_path + orig_output_path_len + 1); + + // Start dump process + uiFill(0, (breaks * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 4, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Copying \"romfs:%s\"...", romfs_path); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + if (entry->dataSize > FAT32_FILESIZE_LIMIT && doSplitting) + { + sprintf(tmp_idx, ".%02u", splitIndex); + strcat(output_path, tmp_idx); + } + + outFile = fopen(output_path, "wb"); + if (!outFile) + { + uiDrawString("Failed to open output file!", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + goto out; + } + + buf = malloc(DUMP_BUFFER_SIZE); + if (!buf) + { + uiDrawString("Failed to allocate memory for the dump process!", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + goto out; + } + + for(off = 0; off < entry->dataSize; off += n, progressCtx->curOffset += n) + { + uiFill(0, ((breaks + 2) * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 2, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", strrchr(output_path, '/') + 1); + uiDrawString(strbuf, 8, ((breaks + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + uiRefreshDisplay(); + + if (DUMP_BUFFER_SIZE > (entry->dataSize - off)) n = (entry->dataSize - off); + + breaks += 6; + proceed = processNcaCtrSectionBlock(&(romFsContext.ncmStorage), &(romFsContext.ncaId), romFsContext.romfs_filedata_offset + entry->dataOff + off, buf, n, &(romFsContext.aes_ctx), false); + breaks -= 6; + + if (!proceed) break; + + if (entry->dataSize > FAT32_FILESIZE_LIMIT && doSplitting && (off + n) >= ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)) + { + u64 new_file_chunk_size = ((off + n) - ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)); + u64 old_file_chunk_size = (n - new_file_chunk_size); + + if (old_file_chunk_size > 0) + { + write_res = fwrite(buf, 1, old_file_chunk_size, outFile); + if (write_res != old_file_chunk_size) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", old_file_chunk_size, off, splitIndex, write_res); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + break; + } + } + + fclose(outFile); + outFile = NULL; + + if (new_file_chunk_size > 0 || (off + n) < entry->dataSize) + { + char *tmp = strrchr(output_path, '.'); + if (tmp != NULL) *tmp = '\0'; + + splitIndex++; + sprintf(tmp_idx, ".%02u", splitIndex); + strcat(output_path, tmp_idx); + + outFile = fopen(output_path, "wb"); + if (!outFile) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file for part #%u!", splitIndex); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + break; + } + + if (new_file_chunk_size > 0) + { + write_res = fwrite(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); + if (write_res != new_file_chunk_size) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, off + old_file_chunk_size, splitIndex, write_res); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + break; + } + } + } + } else { + write_res = fwrite(buf, 1, n, outFile); + if (write_res != n) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", n, off, write_res); + uiDrawString(strbuf, 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + + if ((off + n) > FAT32_FILESIZE_LIMIT) + { + uiDrawString("You're probably using a FAT32 partition. Make sure to enable file splitting.", 8, ((breaks + 8) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + fat32_error = true; + } + + break; + } + } + + printProgressBar(progressCtx, true, n); + + if ((off + n) < entry->dataSize && ((off / DUMP_BUFFER_SIZE) % 10) == 0) + { + hidScanInput(); + + u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO); + if (keysDown & KEY_B) + { + uiDrawString("Process canceled.", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + break; + } + } + } + + if (off >= entry->dataSize) success = true; + + // Support empty files + if (!entry->dataSize) + { + uiFill(0, ((breaks + 2) * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 2, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", strrchr(output_path, '/') + 1); + uiDrawString(strbuf, 8, ((breaks + 2) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + if (progressCtx->totalSize == entry->dataSize) progressCtx->progress = 100; + + printProgressBar(progressCtx, false, 0); + } + +out: + if (buf) free(buf); + + if (outFile) fclose(outFile); + + if (!success) + { + if (fat32_error) breaks += 2; + + if (entry->dataSize > FAT32_FILESIZE_LIMIT && doSplitting) + { + for(u8 i = 0; i <= splitIndex; i++) + { + char *tmp = strrchr(output_path, '.'); + if (tmp != NULL) *tmp = '\0'; + + sprintf(tmp_idx, ".%02u", splitIndex); + strcat(output_path, tmp_idx); + unlink(output_path); + } + } else { + unlink(output_path); + } + } + + romfs_path[orig_romfs_path_len] = '\0'; + output_path[orig_output_path_len] = '\0'; + + if (success) + { + if (entry->sibling != ROMFS_ENTRY_EMPTY) success = recursiveDumpRomFsFile(entry->sibling, romfs_path, output_path, progressCtx, true); + } + + return success; +} + +bool recursiveDumpRomFsDir(u32 dir_offset, char *romfs_path, char *output_path, progress_ctx_t *progressCtx) +{ + if (!romFsContext.romfs_dirtable_size || dir_offset > romFsContext.romfs_dirtable_size || !romFsContext.romfs_dir_entries || !romFsContext.romfs_filetable_size || !romFsContext.romfs_file_entries || !romfs_path || !output_path || !progressCtx) + { + uiDrawString("Error: invalid parameters to parse directory entry from RomFS section!", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + size_t orig_romfs_path_len = strlen(romfs_path); + size_t orig_output_path_len = strlen(output_path); + + romfs_dir *entry = (romfs_dir*)((u8*)romFsContext.romfs_dir_entries + dir_offset); + + // Check if we're dealing with a nameless directory that's not the root directory + if (!entry->nameLen && dir_offset > 0) + { + uiDrawString("Error: directory entry without name in RomFS section!", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if ((orig_romfs_path_len + 1 + entry->nameLen) >= (NAME_BUF_LEN * 2) || (orig_output_path_len + 1 + entry->nameLen) >= (NAME_BUF_LEN * 2)) + { + uiDrawString("Error: RomFS section directory path is too long!", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + // Generate current path + if (entry->nameLen) + { + strcat(romfs_path, "/"); + strncat(romfs_path, (char*)entry->name, entry->nameLen); + + strcat(output_path, "/"); + strncat(output_path, (char*)entry->name, entry->nameLen); + removeIllegalCharacters(output_path + orig_output_path_len + 1); + mkdir(output_path, 0744); + } + + if (entry->childFile != ROMFS_ENTRY_EMPTY) + { + if (!recursiveDumpRomFsFile(entry->childFile, romfs_path, output_path, progressCtx, true)) + { + romfs_path[orig_romfs_path_len] = '\0'; + output_path[orig_output_path_len] = '\0'; + return false; + } + } + + if (entry->childDir != ROMFS_ENTRY_EMPTY) + { + if (!recursiveDumpRomFsDir(entry->childDir, romfs_path, output_path, progressCtx)) + { + romfs_path[orig_romfs_path_len] = '\0'; + output_path[orig_output_path_len] = '\0'; + return false; + } + } + + romfs_path[orig_romfs_path_len] = '\0'; + output_path[orig_output_path_len] = '\0'; + + if (entry->sibling != ROMFS_ENTRY_EMPTY) + { + if (!recursiveDumpRomFsDir(entry->sibling, romfs_path, output_path, progressCtx)) return false; + } + + return true; +} + +bool dumpRomFsSectionData(FsDeviceOperator* fsOperator, u32 appIndex) +{ + progress_ctx_t progressCtx; + memset(&progressCtx, 0, sizeof(progress_ctx_t)); + + char romFsPath[NAME_BUF_LEN * 2] = {'\0'}, dumpPath[NAME_BUF_LEN * 2] = {'\0'}; + + bool success = false; + + initRomFsContext(); + + if (!gameCardAppCount) + { + uiDrawString("Error: invalid gamecard application count!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; + return false; + } + + if (appIndex > (gameCardAppCount - 1)) + { + uiDrawString("Error: invalid gamecard application index!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; + return false; + } + + char *dumpName = generateNSPDumpName(DUMP_APP_NSP, appIndex); + if (!dumpName) + { + uiDrawString("Error: unable to generate output dump name!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; + return false; + } + + // Replace "(BASE)" with "(RomFS)" + dumpName[strlen(dumpName) - 6] = '\0'; + strcat(dumpName, "(RomFS)"); + + // Retrieve RomFS from Program NCA + if (!readProgramNcaRomFs(appIndex)) goto out; + + // Calculate total dump size + if (!calculateRomFsExtractedDataSize(&(progressCtx.totalSize))) goto out; + + breaks++; + convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, sizeof(progressCtx.totalSizeStr) / sizeof(progressCtx.totalSizeStr[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Extracted RomFS dump size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + uiRefreshDisplay(); + breaks++; + + if (progressCtx.totalSize > freeSpace) + { + uiDrawString("Error: not enough free space available in the SD card.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + + // Prepare output dump path + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s", dumpName); + mkdir(dumpPath, 0744); + + // Start dump process + breaks++; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dump procedure started. Hold %s to cancel.", NINTENDO_FONT_B); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + uiRefreshDisplay(); + 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; + } + + progressCtx.line_offset = (breaks + 4); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start)); + + success = recursiveDumpRomFsDir(0, romFsPath, dumpPath, &progressCtx); + + breaks += 6; + + if (success) + { + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now)); + progressCtx.now -= progressCtx.start; + + formatETAString(progressCtx.now, progressCtx.etaInfo, sizeof(progressCtx.etaInfo) / sizeof(progressCtx.etaInfo[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", progressCtx.etaInfo); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + } else { + setProgressBarError(&progressCtx); + removeDirectory(dumpPath); + } + +out: + freeRomFsContext(); + + free(dumpName); + + breaks += 2; + + return success; +} + +bool dumpFileFromRomFsSection(u32 appIndex, u32 file_offset, bool doSplitting) +{ + if (!romFsContext.romfs_filetable_size || file_offset > romFsContext.romfs_filetable_size || !romFsContext.romfs_file_entries || appIndex > (gameCardAppCount - 1)) + { + uiDrawString("Error: invalid parameters to parse file entry from RomFS section!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; + return false; + } + + u64 n = DUMP_BUFFER_SIZE; + FILE *outFile = NULL; + u8 *buf = NULL; + u8 splitIndex = 0; + bool proceed = true, success = false, fat32_error = false; + + char dumpPath[NAME_BUF_LEN * 2] = {'\0'}; + char tmp_idx[5]; + + progress_ctx_t progressCtx; + memset(&progressCtx, 0, sizeof(progress_ctx_t)); + + size_t write_res; + + romfs_file *entry = (romfs_file*)((u8*)romFsContext.romfs_file_entries + file_offset); + + // Check if we're dealing with a nameless file + 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); + breaks += 2; + return false; + } + + char *dumpName = generateNSPDumpName(DUMP_APP_NSP, appIndex); + if (!dumpName) + { + uiDrawString("Error: unable to generate output dump name!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; + return false; + } + + // Replace "(BASE)" with "(RomFS)" + dumpName[strlen(dumpName) - 6] = '\0'; + strcat(dumpName, "(RomFS)"); + + // Generate output path + snprintf(dumpPath, sizeof(dumpPath) / sizeof(dumpPath[0]), "sdmc:/%s", dumpName); + mkdir(dumpPath, 0744); + + // Create subdirectories + char *tmp1 = NULL; + char *tmp2 = NULL; + + tmp1 = strchr(curRomFsPath, '/'); + + while(tmp1 != NULL) + { + tmp1++; + + if (!strlen(tmp1)) break; + + strcat(dumpPath, "/"); + + size_t cur_len = strlen(dumpPath); + + tmp2 = strchr(tmp1, '/'); + if (tmp2 != NULL) + { + strncat(dumpPath, tmp1, tmp2 - tmp1); + + tmp1 = tmp2; + } else { + strcat(dumpPath, tmp1); + + tmp1 = NULL; + } + + removeIllegalCharacters(dumpPath + cur_len); + + mkdir(dumpPath, 0744); + } + + strcat(dumpPath, "/"); + strncat(dumpPath, (char*)entry->name, entry->nameLen); + + progressCtx.totalSize = entry->dataSize; + convertSize(progressCtx.totalSize, progressCtx.totalSizeStr, sizeof(progressCtx.totalSizeStr) / sizeof(progressCtx.totalSizeStr[0])); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "File size: %s (%lu bytes).", progressCtx.totalSizeStr, progressCtx.totalSize); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks += 2; + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Hold %s to cancel.", NINTENDO_FONT_B); + 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; + } + + // Start dump process + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Copying \"romfs:%s/%.*s\"...", curRomFsPath, entry->nameLen, entry->name); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks += 2; + + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting) + { + sprintf(tmp_idx, ".%02u", splitIndex); + strcat(dumpPath, tmp_idx); + } + + outFile = fopen(dumpPath, "wb"); + if (!outFile) + { + uiDrawString("Failed to open output file!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + goto out; + } + + buf = malloc(DUMP_BUFFER_SIZE); + if (!buf) + { + uiDrawString("Failed to allocate memory for the dump process!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + goto out; + } + + progressCtx.line_offset = (breaks + 2); + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.start)); + + for(progressCtx.curOffset = 0; progressCtx.curOffset < progressCtx.totalSize; progressCtx.curOffset += n) + { + uiFill(0, (breaks * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 2, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", strrchr(dumpPath, '/') + 1); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + uiRefreshDisplay(); + + if (DUMP_BUFFER_SIZE > (progressCtx.totalSize - progressCtx.curOffset)) n = (progressCtx.totalSize - progressCtx.curOffset); + + breaks += 4; + proceed = processNcaCtrSectionBlock(&(romFsContext.ncmStorage), &(romFsContext.ncaId), romFsContext.romfs_filedata_offset + entry->dataOff + progressCtx.curOffset, buf, n, &(romFsContext.aes_ctx), false); + breaks -= 4; + + if (!proceed) break; + + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting && (progressCtx.curOffset + n) >= ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)) + { + u64 new_file_chunk_size = ((progressCtx.curOffset + n) - ((splitIndex + 1) * SPLIT_FILE_GENERIC_PART_SIZE)); + u64 old_file_chunk_size = (n - new_file_chunk_size); + + if (old_file_chunk_size > 0) + { + write_res = fwrite(buf, 1, old_file_chunk_size, outFile); + if (write_res != old_file_chunk_size) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", old_file_chunk_size, progressCtx.curOffset, splitIndex, write_res); + uiDrawString(strbuf, 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + break; + } + } + + fclose(outFile); + outFile = NULL; + + if (new_file_chunk_size > 0 || (progressCtx.curOffset + n) < progressCtx.totalSize) + { + char *tmp = strrchr(dumpPath, '.'); + if (tmp != NULL) *tmp = '\0'; + + splitIndex++; + sprintf(tmp_idx, ".%02u", splitIndex); + strcat(dumpPath, tmp_idx); + + outFile = fopen(dumpPath, "wb"); + if (!outFile) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file for part #%u!", splitIndex); + uiDrawString(strbuf, 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + break; + } + + if (new_file_chunk_size > 0) + { + write_res = fwrite(buf + old_file_chunk_size, 1, new_file_chunk_size, outFile); + if (write_res != new_file_chunk_size) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX to part #%02u! (wrote %lu bytes)", new_file_chunk_size, progressCtx.curOffset + old_file_chunk_size, splitIndex, write_res); + uiDrawString(strbuf, 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + break; + } + } + } + } else { + write_res = fwrite(buf, 1, n, outFile); + if (write_res != n) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %lu bytes chunk from offset 0x%016lX! (wrote %lu bytes)", n, progressCtx.curOffset, write_res); + uiDrawString(strbuf, 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + + if ((progressCtx.curOffset + n) > FAT32_FILESIZE_LIMIT) + { + uiDrawString("You're probably using a FAT32 partition. Make sure to enable file splitting.", 8, ((breaks + 6) * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + fat32_error = true; + } + + break; + } + } + + printProgressBar(&progressCtx, true, n); + + if ((progressCtx.curOffset + n) < progressCtx.totalSize && ((progressCtx.curOffset / DUMP_BUFFER_SIZE) % 10) == 0) + { + hidScanInput(); + + u32 keysDown = hidKeysDown(CONTROLLER_P1_AUTO); + if (keysDown & KEY_B) + { + uiDrawString("Process canceled.", 8, ((breaks + 4) * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + break; + } + } + } + + if (progressCtx.curOffset >= progressCtx.totalSize) success = true; + + // Support empty files + if (!progressCtx.totalSize) + { + uiFill(0, (breaks * (font_height + (font_height / 4))) + 8, FB_WIDTH, (font_height + (font_height / 4)) * 2, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Output file: \"%s\".", strrchr(dumpPath, '/') + 1); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + progressCtx.progress = 100; + + printProgressBar(&progressCtx, false, 0); + } + + breaks += 4; + + if (success) + { + timeGetCurrentTime(TimeType_LocalSystemClock, &(progressCtx.now)); + progressCtx.now -= progressCtx.start; + + formatETAString(progressCtx.now, progressCtx.etaInfo, sizeof(progressCtx.etaInfo) / sizeof(progressCtx.etaInfo[0])); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Process successfully completed after %s!", progressCtx.etaInfo); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + } else { + setProgressBarError(&progressCtx); + } + +out: + if (buf) free(buf); + + if (outFile) fclose(outFile); + + if (!success) + { + if (fat32_error) breaks += 2; + + if (progressCtx.totalSize > FAT32_FILESIZE_LIMIT && doSplitting) + { + for(u8 i = 0; i <= splitIndex; i++) + { + char *tmp = strrchr(dumpPath, '.'); + if (tmp != NULL) *tmp = '\0'; + + sprintf(tmp_idx, ".%02u", splitIndex); + strcat(dumpPath, tmp_idx); + unlink(dumpPath); + } + } else { + unlink(dumpPath); + } + } + + free(dumpName); + + breaks += 2; + return success; } bool dumpGameCertificate(FsDeviceOperator* fsOperator) { - u32 crc; + u32 crc = 0; Result result; FsGameCardHandle handle; FsStorage gameCardStorage; bool success = false; FILE *outFile = NULL; char filename[NAME_BUF_LEN * 2] = {'\0'}; - char *buf = NULL; + u8 buf[CERT_SIZE]; size_t write_res; - char *dumpName = generateDumpName(); + char *dumpName = generateDumpFullName(); if (!dumpName) { - uiDrawString("Error: unable to generate output dump name!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: unable to generate output dump name!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); breaks += 2; return false; } @@ -2009,79 +3181,71 @@ bool dumpGameCertificate(FsDeviceOperator* fsOperator) if (R_SUCCEEDED(result = fsDeviceOperatorGetGameCardHandle(fsOperator, &handle))) { /*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "GetGameCardHandle succeeded: 0x%08X", handle.value); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++;*/ if (R_SUCCEEDED(result = fsOpenGameCardStorage(&gameCardStorage, &handle, 0))) { /*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "OpenGameCardStorage succeeded: 0x%08X", handle.value); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++;*/ if (CERT_SIZE <= freeSpace) { - buf = (char*)malloc(DUMP_BUFFER_SIZE); - if (buf) + if (R_SUCCEEDED(result = fsStorageRead(&gameCardStorage, CERT_OFFSET, buf, CERT_SIZE))) { - if (R_SUCCEEDED(result = fsStorageRead(&gameCardStorage, CERT_OFFSET, buf, CERT_SIZE))) + // Calculate CRC32 + crc32(buf, CERT_SIZE, &crc); + + snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s - Certificate (%08X).bin", dumpName, crc); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping game card certificate to \"%s\"...", filename); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + breaks += 2; + + if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) { - // Calculate CRC32 - crc32(buf, CERT_SIZE, &crc); - - snprintf(filename, sizeof(filename) / sizeof(filename[0]), "sdmc:/%s - Certificate (%08X).bin", dumpName, crc); - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Dumping game card certificate to \"%s\"...", filename); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + 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; - - if (programAppletType != AppletType_Application && programAppletType != AppletType_SystemApplication) - { - uiDrawString("Do not press the HOME button. Doing so could corrupt the SD card filesystem.", 0, breaks * font_height, 255, 0, 0); - breaks += 2; - } - - uiRefreshDisplay(); - - outFile = fopen(filename, "wb"); - if (outFile) - { - write_res = fwrite(buf, 1, CERT_SIZE, outFile); - if (write_res == CERT_SIZE) - { - success = true; - uiDrawString("Process successfully completed!", 0, breaks * font_height, 0, 255, 0); - } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %u bytes certificate data! (wrote %lu bytes)", CERT_SIZE, write_res); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - } - - fclose(outFile); - if (!success) remove(filename); - } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file \"%s\"!", filename); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - } - } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "StorageRead failed (0x%08X) at offset 0x%08X", result, CERT_OFFSET); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); } - free(buf); + uiRefreshDisplay(); + + outFile = fopen(filename, "wb"); + if (outFile) + { + write_res = fwrite(buf, 1, CERT_SIZE, outFile); + if (write_res == CERT_SIZE) + { + success = true; + uiDrawString("Process successfully completed!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to write %u bytes certificate data! (wrote %lu bytes)", CERT_SIZE, write_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } + + fclose(outFile); + if (!success) unlink(filename); + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open output file \"%s\"!", filename); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } } else { - uiDrawString("Failed to allocate memory for the dump process!", 0, breaks * font_height, 255, 0, 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "StorageRead failed (0x%08X) at offset 0x%08X", result, CERT_OFFSET); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } else { - uiDrawString("Error: not enough free space available in the SD card.", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: not enough free space available in the SD card.", 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, 0, breaks * font_height, 255, 0, 0); + 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, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } breaks += 2; diff --git a/source/dumper.h b/source/dumper.h index 8bf4767..fa717a1 100644 --- a/source/dumper.h +++ b/source/dumper.h @@ -4,6 +4,7 @@ #define __DUMPER_H__ #include +#include "util.h" #define DUMP_BUFFER_SIZE (u64)0x100000 // 1 MiB (1048576 bytes) #define ISTORAGE_PARTITION_CNT 2 @@ -17,14 +18,16 @@ #define CERT_OFFSET 0x7000 #define CERT_SIZE 0x200 -#define SMOOTHING_FACTOR (double)0.01 +#define SMOOTHING_FACTOR (double)0.05 void workaroundPartitionZeroAccess(FsDeviceOperator* fsOperator); -bool dumpCartridgeImage(FsDeviceOperator* fsOperator, bool isFat32, bool dumpCert, bool trimDump, bool calcCrc); -bool dumpApplicationNSP(FsDeviceOperator* fsOperator, bool isFat32, bool calcCrc, u32 appIndex); -bool dumpRawPartition(FsDeviceOperator* fsOperator, u32 partition, bool doSplitting); -bool dumpPartitionData(FsDeviceOperator* fsOperator, u32 partition); -bool dumpFileFromPartition(FsDeviceOperator* fsOperator, u32 partition, u32 file, char *filename); +bool dumpCartridgeImage(FsDeviceOperator* fsOperator, bool isFat32, bool setXciArchiveBit, bool dumpCert, bool trimDump, bool calcCrc); +bool dumpNintendoSubmissionPackage(FsDeviceOperator* fsOperator, nspDumpType selectedNspDumpType, u32 titleIndex, bool isFat32, bool calcCrc); +bool dumpRawHfs0Partition(FsDeviceOperator* fsOperator, u32 partition, bool doSplitting); +bool dumpHfs0PartitionData(FsDeviceOperator* fsOperator, u32 partition); +bool dumpFileFromHfs0Partition(FsDeviceOperator* fsOperator, u32 partition, u32 file, char *filename); +bool dumpRomFsSectionData(FsDeviceOperator* fsOperator, u32 appIndex); +bool dumpFileFromRomFsSection(u32 appIndex, u32 file_offset, bool doSplitting); bool dumpGameCertificate(FsDeviceOperator *fsOperator); #endif diff --git a/source/extkeys.c b/source/extkeys.c deleted file mode 100644 index 1cb1ff8..0000000 --- a/source/extkeys.c +++ /dev/null @@ -1,250 +0,0 @@ -#include -#include -#include - -#include "extkeys.h" -#include "ui.h" -#include "util.h" - -/* Extern variables */ - -extern int breaks; -extern int font_height; -extern char strbuf[NAME_BUF_LEN * 4]; - -/** - * Reads a line from file f and parses out the key and value from it. - * The format of a line must match /^ *[A-Za-z0-9_] *[,=] *.+$/. - * If a line ends in \r, the final \r is stripped. - * The input file is assumed to have been opened with the 'b' flag. - * The input file is assumed to contain only ASCII. - * - * A line cannot exceed 512 bytes in length. - * Lines that are excessively long will be silently truncated. - * - * On success, *key and *value will be set to point to the key and value in - * the input line, respectively. - * *key and *value may also be NULL in case of empty lines. - * On failure, *key and *value will be set to NULL. - * End of file is considered failure. - * - * Because *key and *value will point to a static buffer, their contents must be - * copied before calling this function again. - * For the same reason, this function is not thread-safe. - * - * The key will be converted to lowercase. - * An empty key is considered a parse error, but an empty value is returned as - * success. - * - * This function assumes that the file can be trusted not to contain any NUL in - * the contents. - * - * Whitespace (' ', ASCII 0x20, as well as '\t', ASCII 0x09) at the beginning of - * the line, at the end of the line as well as around = (or ,) will be ignored. - * - * @param f the file to read - * @param key pointer to change to point to the key - * @param value pointer to change to point to the value - * @return 0 on success, - * 1 on end of file, - * -1 on parse error (line too long, line malformed) - * -2 on I/O error - */ -static int get_kv(FILE *f, char **key, char **value) -{ -#define SKIP_SPACE(p) do {\ - for (; (*p == ' ' || *p == '\t'); ++p);\ -} while(0); - - static char line[512]; - char *k, *v, *p, *end; - - *key = *value = NULL; - - errno = 0; - - if (fgets(line, (int)sizeof(line), f) == NULL) - { - if (feof(f)) - { - return 1; - } else { - return -2; - } - } - - if (errno != 0) return -2; - - if (*line == '\n' || *line == '\r' || *line == '\0') return 0; - - /* Not finding \r or \n is not a problem. - * The line might just be exactly 512 characters long, we have no way to - * tell. - * Additionally, it's possible that the last line of a file is not actually - * a line (i.e., does not end in '\n'); we do want to handle those. - */ - if ((p = strchr(line, '\r')) != NULL || (p = strchr(line, '\n')) != NULL) - { - end = p; - *p = '\0'; - } else { - end = (line + strlen(line) + 1); - } - - p = line; - SKIP_SPACE(p); - k = p; - - /* Validate key and convert to lower case. */ - for (; *p != ' ' && *p != ',' && *p != '\t' && *p != '='; ++p) - { - if (*p == '\0') return -1; - - if (*p >= 'A' && *p <= 'Z') - { - *p = 'a' + (*p - 'A'); - continue; - } - - if (*p != '_' && (*p < '0' && *p > '9') && (*p < 'a' && *p > 'z')) return -1; - } - - /* Bail if the final ++p put us at the end of string */ - if (*p == '\0') return -1; - - /* We should be at the end of key now and either whitespace or [,=] - * follows. - */ - if (*p == '=' || *p == ',') - { - *p++ = '\0'; - } else { - *p++ = '\0'; - SKIP_SPACE(p); - if (*p != '=' && *p != ',') return -1; - *p++ = '\0'; - } - - /* Empty key is an error. */ - if (*k == '\0') return -1; - - SKIP_SPACE(p); - v = p; - - /* Skip trailing whitespace */ - for (p = end - 1; *p == '\t' || *p == ' '; --p); - - *(p + 1) = '\0'; - - *key = k; - *value = v; - - return 0; - -#undef SKIP_SPACE -} - -static int ishex(char c) -{ - if ('a' <= c && c <= 'f') return 1; - if ('A' <= c && c <= 'F') return 1; - if ('0' <= c && c <= '9') return 1; - return 0; -} - -static char hextoi(char c) -{ - if ('a' <= c && c <= 'f') return (c - 'a' + 0xA); - if ('A' <= c && c <= 'F') return (c - 'A' + 0xA); - if ('0' <= c && c <= '9') return (c - '0'); - return 0; -} - -int parse_hex_key(unsigned char *key, const char *hex, unsigned int len) -{ - if (strlen(hex) != (2 * len)) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: key (%s) must be %u hex digits!", hex, 2 * len); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return 0; - } - - u32 i; - for(i = 0; i < (2 * len); i++) - { - if (!ishex(hex[i])) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: key (%s) must be %u hex digits!", hex, 2 * len); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return 0; - } - } - - memset(key, 0, len); - - for(i = 0; i < (2 * len); i++) - { - char val = hextoi(hex[i]); - if ((i & 1) == 0) val <<= 4; - key[i >> 1] |= val; - } - - return 1; -} - -int extkeys_initialize_keyset(nca_keyset_t *keyset, FILE *f) -{ - u32 i; - int ret; - char *key, *value; - char test_name[0x100]; - - memset(keyset, 0, sizeof(nca_keyset_t)); - - while((ret = get_kv(f, &key, &value)) != 1 && ret != -2) - { - if (ret == 0) - { - if (key == NULL || value == NULL) continue; - - if (strcmp(key, "header_key") == 0) - { - if (!parse_hex_key(keyset->header_key, value, sizeof(keyset->header_key))) return 0; - keyset->key_cnt++; - } else { - memset(test_name, 0, sizeof(test_name)); - - for(i = 0; i < 0x20; i++) - { - snprintf(test_name, sizeof(test_name), "key_area_key_application_%02x", i); - if (strcmp(key, test_name) == 0) - { - if (!parse_hex_key(keyset->key_area_keys[i][0], value, sizeof(keyset->key_area_keys[i][0]))) return 0; - keyset->key_cnt++; - break; - } - - snprintf(test_name, sizeof(test_name), "key_area_key_ocean_%02x", i); - if (strcmp(key, test_name) == 0) - { - if (!parse_hex_key(keyset->key_area_keys[i][1], value, sizeof(keyset->key_area_keys[i][1]))) return 0; - keyset->key_cnt++; - break; - } - - snprintf(test_name, sizeof(test_name), "key_area_key_system_%02x", i); - if (strcmp(key, test_name) == 0) - { - if (!parse_hex_key(keyset->key_area_keys[i][2], value, sizeof(keyset->key_area_keys[i][2]))) return 0; - keyset->key_cnt++; - break; - } - } - } - } - } - - if (!keyset->key_cnt) return -1; - - return 1; -} diff --git a/source/extkeys.h b/source/extkeys.h deleted file mode 100644 index 116739f..0000000 --- a/source/extkeys.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#ifndef __EXTKEYS_H__ -#define __EXTKEYS_H__ - -#include -#include - -typedef struct { - u32 key_cnt; - unsigned char header_key[0x20]; /* NCA header key. */ - unsigned char key_area_keys[0x20][3][0x10]; /* Key area encryption keys. */ -} nca_keyset_t; - -int parse_hex_key(unsigned char *key, const char *hex, unsigned int len); -int extkeys_initialize_keyset(nca_keyset_t *keyset, FILE *f); - -#endif \ No newline at end of file diff --git a/source/fsext.c b/source/fs_ext.c similarity index 99% rename from source/fsext.c rename to source/fs_ext.c index 82df76c..beaff3d 100644 --- a/source/fsext.c +++ b/source/fs_ext.c @@ -2,7 +2,7 @@ #include #include -#include "fsext.h" +#include "fs_ext.h" // IFileSystemProxy Result fsOpenGameCardStorage(FsStorage* out, const FsGameCardHandle* handle, u32 partition) diff --git a/source/fsext.h b/source/fs_ext.h similarity index 90% rename from source/fsext.h rename to source/fs_ext.h index ea30b38..523e792 100644 --- a/source/fsext.h +++ b/source/fs_ext.h @@ -1,7 +1,7 @@ #pragma once -#ifndef __FSEXT_H__ -#define __FSEXT_H__ +#ifndef __FS_EXT_H__ +#define __FS_EXT_H__ #include #include diff --git a/source/keys.c b/source/keys.c new file mode 100644 index 0000000..01cc786 --- /dev/null +++ b/source/keys.c @@ -0,0 +1,400 @@ +#include +#include +#include + +#include "keys.h" +#include "util.h" +#include "ui.h" + +/* Extern variables */ + +extern int breaks; +extern int font_height; + +extern char strbuf[NAME_BUF_LEN * 4]; + +/* Statically allocated variables */ + +nca_keyset_t nca_keyset; + +static keyLocation FSRodata = { + FS_TID, + SEG_RODATA, + NULL, + 0 +}; + +static keyLocation FSData = { + FS_TID, + SEG_DATA, + NULL, + 0 +}; + +static const keyInfo header_kek_source = { + "header_kek_source", + { 0x18, 0x88, 0xCA, 0xED, 0x55, 0x51, 0xB3, 0xED, 0xE0, 0x14, 0x99, 0xE8, 0x7C, 0xE0, 0xD8, 0x68, + 0x27, 0xF8, 0x08, 0x20, 0xEF, 0xB2, 0x75, 0x92, 0x10, 0x55, 0xAA, 0x4E, 0x2A, 0xBD, 0xFF, 0xC2 }, + 0x10 +}; + +static const keyInfo header_key_source = { + "header_key_source", + { 0x8F, 0x78, 0x3E, 0x46, 0x85, 0x2D, 0xF6, 0xBE, 0x0B, 0xA4, 0xE1, 0x92, 0x73, 0xC4, 0xAD, 0xBA, + 0xEE, 0x16, 0x38, 0x00, 0x43, 0xE1, 0xB8, 0xC4, 0x18, 0xC4, 0x08, 0x9A, 0x8B, 0xD6, 0x4A, 0xA6 }, + 0x20 +}; + +static const keyInfo key_area_key_application_source = { + "key_area_key_application_source", + { 0x04, 0xAD, 0x66, 0x14, 0x3C, 0x72, 0x6B, 0x2A, 0x13, 0x9F, 0xB6, 0xB2, 0x11, 0x28, 0xB4, 0x6F, + 0x56, 0xC5, 0x53, 0xB2, 0xB3, 0x88, 0x71, 0x10, 0x30, 0x42, 0x98, 0xD8, 0xD0, 0x09, 0x2D, 0x9E }, + 0x10 +}; + +static const keyInfo key_area_key_ocean_source = { + "key_area_key_ocean_source", + { 0xFD, 0x43, 0x40, 0x00, 0xC8, 0xFF, 0x2B, 0x26, 0xF8, 0xE9, 0xA9, 0xD2, 0xD2, 0xC1, 0x2F, 0x6B, + 0xE5, 0x77, 0x3C, 0xBB, 0x9D, 0xC8, 0x63, 0x00, 0xE1, 0xBD, 0x99, 0xF8, 0xEA, 0x33, 0xA4, 0x17 }, + 0x10 +}; + +static const keyInfo key_area_key_system_source = { + "key_area_key_system_source", + { 0x1F, 0x17, 0xB1, 0xFD, 0x51, 0xAD, 0x1C, 0x23, 0x79, 0xB5, 0x8F, 0x15, 0x2C, 0xA4, 0x91, 0x2E, + 0xC2, 0x10, 0x64, 0x41, 0xE5, 0x17, 0x22, 0xF3, 0x87, 0x00, 0xD5, 0x93, 0x7A, 0x11, 0x62, 0xF7 }, + 0x10 +}; + +void freeProcessMemory(keyLocation *location) +{ + if (location && location->data) + { + free(location->data); + location->data = NULL; + } +} + +bool retrieveProcessMemory(keyLocation *location) +{ + if (!location || !location->titleID || !location->mask) + { + uiDrawString("Error: invalid parameters to retrieve keys from process memory.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + Result result; + Handle debug_handle = INVALID_HANDLE; + + u64 d[8]; + memset(d, 0, 8 * sizeof(u64)); + + if ((location->titleID > 0x0100000000000005) && (location->titleID != 0x0100000000000028)) + { + // If not a kernel process, get PID from pm:dmnt + u64 pid; + + if (R_FAILED(result = pmdmntGetTitlePid(&pid, location->titleID))) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: pmdmntGetTitlePid failed! (0x%08X)", result); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (R_FAILED(result = svcDebugActiveProcess(&debug_handle, pid))) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: svcDebugActiveProcess failed! (0x%08X)", result); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (R_FAILED(result = svcGetDebugEvent((u8*)&d, debug_handle))) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: svcGetDebugEvent failed! (0x%08X)", result); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + } else { + // Otherwise, query svc for the process list + u64 pids[300]; + u32 num_processes; + + if (R_FAILED(result = svcGetProcessList(&num_processes, pids, 300))) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: svcGetProcessList failed! (0x%08X)", result); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + u32 i; + + for(i = 0; i < (num_processes - 1); i++) + { + if (R_SUCCEEDED(svcDebugActiveProcess(&debug_handle, pids[i])) && R_SUCCEEDED(svcGetDebugEvent((u8*)&d, debug_handle)) && (d[2] == location->titleID)) break; + + if (debug_handle) svcCloseHandle(debug_handle); + } + + if (i == (num_processes - 1)) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unable to retrieve debug handle for process with Title ID %016lX!", location->titleID); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + if (debug_handle) svcCloseHandle(debug_handle); + return false; + } + } + + MemoryInfo mem_info; + memset(&mem_info, 0, sizeof(MemoryInfo)); + + u32 page_info; + u64 addr = 0; + u8 segment; + + u8 *dataTmp = NULL; + + bool success = true; + + for(segment = 1; segment < BIT(3);) + { + if (R_FAILED(result = svcQueryDebugProcessMemory(&mem_info, &page_info, debug_handle, addr))) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: svcQueryDebugProcessMemory failed! (0x%08X)", result); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + success = false; + break; + } + + // Weird code to allow for bitmasking segments + if ((mem_info.perm & Perm_R) && ((mem_info.type & 0xff) >= MemType_CodeStatic) && ((mem_info.type & 0xff) < MemType_Heap) && ((segment <<= 1) >> 1 & location->mask) > 0) + { + // If location->data == NULL, realloc will essentially act as a malloc + dataTmp = realloc(location->data, location->dataSize + mem_info.size); + if (!dataTmp) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to resize key location data buffer to %lu bytes.", location->dataSize + mem_info.size); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + success = false; + break; + } + + location->data = dataTmp; + dataTmp = NULL; + + memset(location->data + location->dataSize, 0, mem_info.size); + + if (R_FAILED(result = svcReadDebugProcessMemory(location->data + location->dataSize, debug_handle, mem_info.addr, mem_info.size))) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: svcReadDebugProcessMemory failed! (0x%08X)", result); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + success = false; + break; + } + + location->dataSize += mem_info.size; + } + + addr = (mem_info.addr + mem_info.size); + if (addr == 0) break; + } + + svcCloseHandle(debug_handle); + + if (success) + { + if (!location->data || !location->dataSize) success = false; + } + + return success; +} + +bool findKeyInProcessMemory(const keyLocation *location, const keyInfo *findKey, u8 *out) +{ + if (!location || !location->data || !location->dataSize || !findKey || !strlen(findKey->name) || !findKey->size) + { + uiDrawString("Error: invalid parameters to locate key in process memory.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + u64 i; + u8 temp_hash[SHA256_HASH_LENGTH]; + bool found = false; + + // Hash every key-length-sized byte chunk in data until it matches a key hash + for(i = 0; i < location->dataSize; i++) + { + if (!found && (location->dataSize - i) < findKey->size) break; + + sha256CalculateHash(temp_hash, location->data + i, findKey->size); + + if (!memcmp(temp_hash, findKey->hash, SHA256_HASH_LENGTH)) + { + // Jackpot + memcpy(out, location->data + i, findKey->size); + found = true; + break; + } + } + + if (!found) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unable to locate key \"%s\" in process memory!", findKey->name); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } + + return found; +} + +bool findFSRodataKeys(keyLocation *location) +{ + if (!location || location->titleID != FS_TID || location->mask != SEG_RODATA || !location->data || !location->dataSize) + { + uiDrawString("Error: invalid parameters to locate keys in FS RODATA segment.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (!findKeyInProcessMemory(location, &header_kek_source, nca_keyset.header_kek_source)) return false; + nca_keyset.key_cnt++; + + if (!findKeyInProcessMemory(location, &key_area_key_application_source, nca_keyset.key_area_key_application_source)) return false; + nca_keyset.key_cnt++; + + if (!findKeyInProcessMemory(location, &key_area_key_ocean_source, nca_keyset.key_area_key_ocean_source)) return false; + nca_keyset.key_cnt++; + + if (!findKeyInProcessMemory(location, &key_area_key_system_source, nca_keyset.key_area_key_system_source)) return false; + nca_keyset.key_cnt++; + + return true; +} + +bool getNcaKeys() +{ + Result result; + u8 nca_header_kek[0x10]; + + if (!retrieveProcessMemory(&FSRodata)) return false; + + if (!retrieveProcessMemory(&FSData)) + { + freeProcessMemory(&FSRodata); + return false; + } + + if (!findFSRodataKeys(&FSRodata)) + { + freeProcessMemory(&FSData); + freeProcessMemory(&FSRodata); + return false; + } + + if (!findKeyInProcessMemory(&FSData, &header_key_source, nca_keyset.header_key_source)) + { + freeProcessMemory(&FSData); + freeProcessMemory(&FSRodata); + return false; + } + + nca_keyset.key_cnt++; + + // Derive NCA header key + if (R_FAILED(result = splCryptoInitialize())) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to initialize the spl:crypto service! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); + freeProcessMemory(&FSData); + freeProcessMemory(&FSRodata); + return false; + } + + if (R_FAILED(result = splCryptoGenerateAesKek(nca_keyset.header_kek_source, 0, 0, nca_header_kek))) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: splCryptoGenerateAesKek(header_kek_source) failed! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); + splCryptoExit(); + freeProcessMemory(&FSData); + freeProcessMemory(&FSRodata); + return false; + } + + if (R_FAILED(result = splCryptoGenerateAesKey(nca_header_kek, nca_keyset.header_key_source + 0x00, nca_keyset.header_key + 0x00))) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: splCryptoGenerateAesKey(header_key_source + 0x00) failed! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); + splCryptoExit(); + freeProcessMemory(&FSData); + freeProcessMemory(&FSRodata); + return false; + } + + if (R_FAILED(result = splCryptoGenerateAesKey(nca_header_kek, nca_keyset.header_key_source + 0x10, nca_keyset.header_key + 0x10))) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: splCryptoGenerateAesKey(header_key_source + 0x10) failed! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); + splCryptoExit(); + freeProcessMemory(&FSData); + freeProcessMemory(&FSRodata); + return false; + } + + nca_keyset.key_cnt++; + + splCryptoExit(); + + freeProcessMemory(&FSData); + freeProcessMemory(&FSRodata); + + return true; +} + +bool decryptNcaKeyArea(nca_header_t *dec_nca_header, u8 *out) +{ + if (!dec_nca_header || dec_nca_header->kaek_ind > 2) + { + uiDrawString("Error: invalid parameters to decrypt NCA key area.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + Result result; + + u8 i; + u8 tmp_kek[0x10]; + + u8 crypto_type = (dec_nca_header->crypto_type2 > dec_nca_header->crypto_type ? dec_nca_header->crypto_type2 : dec_nca_header->crypto_type); + u8 *kek_source = (dec_nca_header->kaek_ind == 0 ? nca_keyset.key_area_key_application_source : (dec_nca_header->kaek_ind == 1 ? nca_keyset.key_area_key_ocean_source : nca_keyset.key_area_key_system_source)); + + if (R_FAILED(result = splCryptoInitialize())) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to initialize the spl:crypto service! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); + return false; + } + + if (R_FAILED(result = splCryptoGenerateAesKek(kek_source, crypto_type, 0, tmp_kek))) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: splCryptoGenerateAesKek(kek_source) failed! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); + splCryptoExit(); + return false; + } + + bool success = true; + u8 decrypted_nca_keys[NCA_KEY_AREA_KEY_CNT][NCA_KEY_AREA_KEY_SIZE]; + + for(i = 0; i < NCA_KEY_AREA_KEY_CNT; i++) + { + if (R_FAILED(result = splCryptoGenerateAesKey(tmp_kek, dec_nca_header->nca_keys[i], decrypted_nca_keys[i]))) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: splCryptoGenerateAesKey(nca_kaek_%02u) failed! (0x%08X)", i, result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); + success = false; + break; + } + } + + splCryptoExit(); + + memcpy(out, decrypted_nca_keys, NCA_KEY_AREA_SIZE); + + return success; +} diff --git a/source/keys.h b/source/keys.h new file mode 100644 index 0000000..e3ae982 --- /dev/null +++ b/source/keys.h @@ -0,0 +1,44 @@ +#pragma once + +#ifndef __KEYS_H__ +#define __KEYS_H__ + +#include + +#include "nca.h" + +#define FS_TID (u64)0x0100000000000000 + +#define SEG_TEXT BIT(0) +#define SEG_RODATA BIT(1) +#define SEG_DATA BIT(2) + +#define SHA256_HASH_LENGTH 0x20 + +typedef struct { + u64 titleID; + u8 mask; + u8 *data; + u64 dataSize; +} PACKED keyLocation; + +typedef struct { + char name[128]; + u8 hash[SHA256_HASH_LENGTH]; + u64 size; +} PACKED keyInfo; + +typedef struct { + u32 key_cnt; /* Should be equal to 6. */ + u8 key_area_key_application_source[0x10]; /* Seed for kaek 0. */ + u8 key_area_key_ocean_source[0x10]; /* Seed for kaek 1. */ + u8 key_area_key_system_source[0x10]; /* Seed for kaek 2. */ + u8 header_kek_source[0x10]; /* Seed for header kek. */ + u8 header_key_source[0x20]; /* Seed for NCA header key. */ + u8 header_key[0x20]; /* NCA header key. */ +} PACKED nca_keyset_t; + +bool getNcaKeys(); +bool decryptNcaKeyArea(nca_header_t *dec_nca_header, u8 *out); + +#endif diff --git a/source/main.c b/source/main.c index 534aafa..b814c82 100644 --- a/source/main.c +++ b/source/main.c @@ -7,8 +7,8 @@ #include "dumper.h" #include "ui.h" #include "util.h" -#include "fsext.h" -#include "extkeys.h" +#include "fs_ext.h" +#include "keys.h" /* Extern variables */ @@ -23,218 +23,290 @@ extern bool gameCardInserted; extern char strbuf[NAME_BUF_LEN * 4]; +extern char appLaunchPath[NAME_BUF_LEN]; + extern nca_keyset_t nca_keyset; int main(int argc, char *argv[]) { + /* Copy launch path */ + if (!envIsNso() && argc > 0) + { + int i; + for(i = 0; i < argc; i++) + { + if (strlen(argv[i]) > 10 && !strncasecmp(argv[i], "sdmc:/", 6) && !strncasecmp(argv[i] + strlen(argv[i]) - 4, ".nro", 4)) + { + snprintf(appLaunchPath, sizeof(appLaunchPath) / sizeof(appLaunchPath[0]), argv[i]); + break; + } + } + } + /* Initialize UI */ if (!uiInit()) return -1; int ret = 0; Result result; - /* Initialize the fsp-srv service */ - result = fsInitialize(); + /* Initialize the ncm service */ + result = ncmInitialize(); if (R_SUCCEEDED(result)) { - /* Open device operator */ - result = fsOpenDeviceOperator(&fsOperatorInstance); + /* Initialize the ns service */ + result = nsInitialize(); if (R_SUCCEEDED(result)) { - /* Initialize the ncm service */ - result = ncmInitialize(); + /* Initialize the csrng service */ + result = csrngInitialize(); if (R_SUCCEEDED(result)) { - /* Initialize the ns service */ - result = nsInitialize(); + /* Initialize the spl service */ + result = splInitialize(); if (R_SUCCEEDED(result)) { - /* Initialize the time service */ - result = timeInitialize(); + /* Initialize the pm:dmnt service */ + result = pmdmntInitialize(); if (R_SUCCEEDED(result)) { - /* Open gamecard detection event notifier */ - result = fsOpenGameCardDetectionEventNotifier(&fsGameCardEventNotifier); + /* Open device operator */ + result = fsOpenDeviceOperator(&fsOperatorInstance); if (R_SUCCEEDED(result)) { - /* Retrieve gamecard detection event handle */ - result = fsEventNotifierGetEventHandle(&fsGameCardEventNotifier, &fsGameCardEventHandle); + /* Open gamecard detection event notifier */ + result = fsOpenGameCardDetectionEventNotifier(&fsGameCardEventNotifier); if (R_SUCCEEDED(result)) { - /* Retrieve initial gamecard status */ - gameCardInserted = isGameCardInserted(); - - /* Load gamecard detection kernel event */ - eventLoadRemote(&fsGameCardKernelEvent, fsGameCardEventHandle, false); - - /* Create usermode exit event */ - ueventCreate(&exitEvent, false); - - /* Create gamecard detection thread */ - Thread thread; - result = threadCreate(&thread, fsGameCardDetectionThreadFunc, NULL, 0x10000, 0x2C, -2); + /* Retrieve gamecard detection event handle */ + result = fsEventNotifierGetEventHandle(&fsGameCardEventNotifier, &fsGameCardEventHandle); if (R_SUCCEEDED(result)) { - /* Start gamecard detection thread */ - result = threadStart(&thread); + /* Retrieve initial gamecard status */ + gameCardInserted = isGameCardInserted(); + + /* Load gamecard detection kernel event */ + eventLoadRemote(&fsGameCardKernelEvent, fsGameCardEventHandle, false); + + /* Create usermode exit event */ + ueventCreate(&exitEvent, false); + + /* Create gamecard detection thread */ + Thread thread; + result = threadCreate(&thread, fsGameCardDetectionThreadFunc, NULL, 0x10000, 0x2C, -2); if (R_SUCCEEDED(result)) { - /* Zero out NCA keyset */ - memset(&nca_keyset, 0, sizeof(nca_keyset_t)); - - /* Main application loop */ - bool exitLoop = false; - while(appletMainLoop()) + /* Start gamecard detection thread */ + result = threadStart(&thread); + if (R_SUCCEEDED(result)) { - UIResult result = uiProcess(); - switch(result) + /* Zero out NCA keyset */ + memset(&nca_keyset, 0, sizeof(nca_keyset_t)); + + /* Init RomFS context */ + initRomFsContext(); + + /* Main application loop */ + bool exitLoop = false; + while(appletMainLoop()) { - case resultShowMainMenu: - uiSetState(stateMainMenu); - break; - case resultShowXciDumpMenu: - uiSetState(stateXciDumpMenu); - break; - case resultDumpXci: - uiSetState(stateDumpXci); - break; - case resultShowNspDumpMenu: - uiSetState(stateNspDumpMenu); - break; - case resultDumpNsp: - uiSetState(stateDumpNsp); - break; - case resultShowRawPartitionDumpMenu: - uiSetState(stateRawPartitionDumpMenu); - break; - case resultDumpRawPartition: - uiSetState(stateDumpRawPartition); - break; - case resultShowPartitionDataDumpMenu: - uiSetState(statePartitionDataDumpMenu); - break; - case resultDumpPartitionData: - uiSetState(stateDumpPartitionData); - break; - case resultShowViewGameCardFsMenu: - uiSetState(stateViewGameCardFsMenu); - break; - case resultShowViewGameCardFsGetList: - uiSetState(stateViewGameCardFsGetList); - break; - case resultShowViewGameCardFsBrowser: - uiSetState(stateViewGameCardFsBrowser); - break; - case resultViewGameCardFsBrowserCopyFile: - uiSetState(stateViewGameCardFsBrowserCopyFile); - break; - case resultDumpGameCardCertificate: - uiSetState(stateDumpGameCardCertificate); - break; - case resultUpdateNSWDBXml: - uiSetState(stateUpdateNSWDBXml); - break; - case resultUpdateApplication: - uiSetState(stateUpdateApplication); - break; - case resultExit: - exitLoop = true; - break; - default: - break; + UIResult result = uiProcess(); + switch(result) + { + case resultShowMainMenu: + uiSetState(stateMainMenu); + break; + case resultShowXciDumpMenu: + uiSetState(stateXciDumpMenu); + break; + case resultDumpXci: + uiSetState(stateDumpXci); + break; + case resultShowNspDumpMenu: + uiSetState(stateNspDumpMenu); + break; + case resultShowNspAppDumpMenu: + uiSetState(stateNspAppDumpMenu); + break; + case resultShowNspPatchDumpMenu: + uiSetState(stateNspPatchDumpMenu); + break; + case resultShowNspAddOnDumpMenu: + uiSetState(stateNspAddOnDumpMenu); + break; + case resultDumpNsp: + uiSetState(stateDumpNsp); + break; + case resultShowHfs0Menu: + uiSetState(stateHfs0Menu); + break; + case resultShowRawHfs0PartitionDumpMenu: + uiSetState(stateRawHfs0PartitionDumpMenu); + break; + case resultDumpRawHfs0Partition: + uiSetState(stateDumpRawHfs0Partition); + break; + case resultShowHfs0PartitionDataDumpMenu: + uiSetState(stateHfs0PartitionDataDumpMenu); + break; + case resultDumpHfs0PartitionData: + uiSetState(stateDumpHfs0PartitionData); + break; + case resultShowHfs0BrowserMenu: + uiSetState(stateHfs0BrowserMenu); + break; + case resultHfs0BrowserGetList: + uiSetState(stateHfs0BrowserGetList); + break; + case resultShowHfs0Browser: + uiSetState(stateHfs0Browser); + break; + case resultHfs0BrowserCopyFile: + uiSetState(stateHfs0BrowserCopyFile); + break; + case resultShowRomFsMenu: + uiSetState(stateRomFsMenu); + break; + case resultShowRomFsSectionDataDumpMenu: + uiSetState(stateRomFsSectionDataDumpMenu); + break; + case resultDumpRomFsSectionData: + uiSetState(stateDumpRomFsSectionData); + break; + case resultShowRomFsSectionBrowserMenu: + uiSetState(stateRomFsSectionBrowserMenu); + break; + case resultRomFsSectionBrowserGetEntries: + uiSetState(stateRomFsSectionBrowserGetEntries); + break; + case resultShowRomFsSectionBrowser: + uiSetState(stateRomFsSectionBrowser); + break; + case resultRomFsSectionBrowserChangeDir: + uiSetState(stateRomFsSectionBrowserChangeDir); + break; + case resultRomFsSectionBrowserCopyFile: + uiSetState(stateRomFsSectionBrowserCopyFile); + break; + case resultDumpGameCardCertificate: + uiSetState(stateDumpGameCardCertificate); + break; + case resultShowUpdateMenu: + uiSetState(stateUpdateMenu); + break; + case resultUpdateNSWDBXml: + uiSetState(stateUpdateNSWDBXml); + break; + case resultUpdateApplication: + uiSetState(stateUpdateApplication); + break; + case resultExit: + exitLoop = true; + break; + default: + break; + } + + if (exitLoop) break; } - if (exitLoop) break; + /* Signal the exit event to terminate the gamecard detection thread */ + ueventSignal(&exitEvent); + + /* Wait for the gamecard detection thread to exit */ + threadWaitForExit(&thread); + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to start gamecard detection thread! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); + uiRefreshDisplay(); + delay(5); + ret = -11; } - /* Signal the exit event to terminate the gamecard detection thread */ - ueventSignal(&exitEvent); - - /* Wait for the gamecard detection thread to exit */ - threadWaitForExit(&thread); + /* Close gamecard detection thread */ + threadClose(&thread); } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to start gamecard detection thread! (0x%08X)", result); - uiDrawString(strbuf, 0, 0, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to create gamecard detection thread! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); uiRefreshDisplay(); delay(5); ret = -10; } - /* Close gamecard detection thread */ - threadClose(&thread); + /* Close gamecard detection kernel event */ + eventClose(&fsGameCardKernelEvent); } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to create gamecard detection thread! (0x%08X)", result); - uiDrawString(strbuf, 0, 0, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to retrieve gamecard detection event handle! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); uiRefreshDisplay(); delay(5); ret = -9; } - /* Close gamecard detection kernel event */ - eventClose(&fsGameCardKernelEvent); + /* Close gamecard detection event notifier */ + fsEventNotifierClose(&fsGameCardEventNotifier); } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to retrieve gamecard detection event handle! (0x%08X)", result); - uiDrawString(strbuf, 0, 0, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open gamecard detection event notifier! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); uiRefreshDisplay(); delay(5); ret = -8; } - /* Close gamecard detection event notifier */ - fsEventNotifierClose(&fsGameCardEventNotifier); + /* Close device operator */ + fsDeviceOperatorClose(&fsOperatorInstance); } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open gamecard detection event notifier! (0x%08X)", result); - uiDrawString(strbuf, 0, 0, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open device operator! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); uiRefreshDisplay(); delay(5); ret = -7; } - /* Denitialize the time service */ - timeExit(); + /* Denitialize the pm:dmnt service */ + pmdmntExit(); } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to initialize the time service! (0x%08X)", result); - uiDrawString(strbuf, 0, 0, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to initialize the pm:dmnt service! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); uiRefreshDisplay(); delay(5); ret = -6; } - /* Denitialize the ns service */ - nsExit(); + /* Denitialize the spl service */ + splExit(); } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to initialize the ns service! (0x%08X)", result); - uiDrawString(strbuf, 0, 0, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to initialize the spl service! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); uiRefreshDisplay(); delay(5); ret = -5; } - /* Denitialize the ncm service */ - ncmExit(); + /* Denitialize the csrng service */ + csrngExit(); } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to initialize the ncm service! (0x%08X)", result); - uiDrawString(strbuf, 0, 0, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to initialize the csrng service! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); uiRefreshDisplay(); delay(5); ret = -4; } - /* Close device operator */ - fsDeviceOperatorClose(&fsOperatorInstance); + /* Denitialize the ns service */ + nsExit(); } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to open device operator! (0x%08X)", result); - uiDrawString(strbuf, 0, 0, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to initialize the ns service! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); uiRefreshDisplay(); delay(5); ret = -3; } - /* Denitialize the fs-srv service */ - fsExit(); + /* Denitialize the ncm service */ + ncmExit(); } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to initialize the fsp-srv service! (0x%08X)", result); - uiDrawString(strbuf, 0, 0, 255, 255, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to initialize the ncm service! (0x%08X)", result); + uiDrawString(strbuf, 8, 8, 255, 255, 255); uiRefreshDisplay(); delay(5); ret = -2; diff --git a/source/nca.c b/source/nca.c new file mode 100644 index 0000000..47835c9 --- /dev/null +++ b/source/nca.c @@ -0,0 +1,2014 @@ +#include +#include +#include + +#include "nca.h" +#include "util.h" +#include "ui.h" +#include "keys.h" +#include "rsa.h" + +/* Extern variables */ + +extern int breaks; +extern int font_height; + +extern romfs_ctx_t romFsContext; + +extern char strbuf[NAME_BUF_LEN * 4]; + +extern nca_keyset_t nca_keyset; + +char *getTitleType(u8 type) +{ + char *out = NULL; + + switch(type) + { + case META_DB_REGULAR_APPLICATION: + out = "Application"; + break; + case META_DB_PATCH: + out = "Patch"; + break; + case META_DB_ADDON: + out = "AddOnContent"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getContentType(u8 type) +{ + char *out = NULL; + + switch(type) + { + case NcmContentType_CNMT: + out = "Meta"; + break; + case NcmContentType_Program: + out = "Program"; + break; + case NcmContentType_Data: + out = "Data"; + break; + case NcmContentType_Icon: + out = "Control"; + break; + case NcmContentType_Doc: + out = "HtmlDocument"; + break; + case NcmContentType_Info: + out = "LegalInformation"; + break; + case NCA_CONTENT_TYPE_DELTA: + out = "DeltaFragment"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getRequiredMinTitleType(u8 type) +{ + char *out = NULL; + + switch(type) + { + case META_DB_REGULAR_APPLICATION: + case META_DB_PATCH: + out = "RequiredSystemVersion"; + break; + case META_DB_ADDON: + out = "RequiredApplicationVersion"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getReferenceTitleIDType(u8 type) +{ + char *out = NULL; + + switch(type) + { + case META_DB_REGULAR_APPLICATION: + out = "PatchId"; + break; + case META_DB_PATCH: + out = "OriginalId"; + break; + case META_DB_ADDON: + out = "ApplicationId"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +void generateCnmtXml(cnmt_xml_program_info *xml_program_info, cnmt_xml_content_info *xml_content_info, char *out) +{ + if (!xml_program_info || !xml_content_info || !xml_program_info->nca_cnt || !out) return; + + u32 i; + char tmp[NAME_BUF_LEN] = {'\0'}; + + sprintf(out, "\xEF\xBB\xBF\r\n" \ + "\r\n" \ + " %s\r\n" \ + " 0x%016lx\r\n" \ + " %u\r\n" \ + " %u\r\n", \ + getTitleType(xml_program_info->type), \ + xml_program_info->title_id, \ + xml_program_info->version, \ + xml_program_info->required_dl_sysver); + + for(i = 0; i < xml_program_info->nca_cnt; i++) + { + sprintf(tmp, " \r\n" \ + " %s\r\n" \ + " %s\r\n" \ + " %lu\r\n" \ + " %s\r\n" \ + " %u\r\n" \ + " \r\n", \ + getContentType(xml_content_info[i].type), \ + xml_content_info[i].nca_id_str, \ + xml_content_info[i].size, \ + xml_content_info[i].hash_str, \ + xml_content_info[i].keyblob); \ + + strcat(out, tmp); + } + + sprintf(tmp, " %s\r\n" \ + " %u\r\n" \ + " <%s>%u\r\n" \ + " <%s>0x%016lx\r\n" \ + "\r\n", \ + xml_program_info->digest_str, \ + xml_program_info->min_keyblob, \ + getRequiredMinTitleType(xml_program_info->type), \ + xml_program_info->min_sysver, \ + getRequiredMinTitleType(xml_program_info->type), \ + getReferenceTitleIDType(xml_program_info->type), \ + xml_program_info->patch_tid, \ + getReferenceTitleIDType(xml_program_info->type)); + + strcat(out, tmp); +} + +void convertNcaSizeToU64(const u8 size[0x6], u64 *out) +{ + if (!size || !out) return; + + u64 tmp = 0; + + tmp |= (((u64)size[5] << 40) & (u64)0xFF0000000000); + tmp |= (((u64)size[4] << 32) & (u64)0x00FF00000000); + tmp |= (((u64)size[3] << 24) & (u64)0x0000FF000000); + tmp |= (((u64)size[2] << 16) & (u64)0x000000FF0000); + tmp |= (((u64)size[1] << 8) & (u64)0x00000000FF00); + tmp |= ((u64)size[0] & (u64)0x0000000000FF); + + *out = tmp; +} + +void convertU64ToNcaSize(const u64 size, u8 out[0x6]) +{ + if (!size || !out) return; + + u8 tmp[0x6]; + + tmp[5] = (u8)(size >> 40); + tmp[4] = (u8)(size >> 32); + tmp[3] = (u8)(size >> 24); + tmp[2] = (u8)(size >> 16); + tmp[1] = (u8)(size >> 8); + tmp[0] = (u8)size; + + memcpy(out, tmp, 6); +} + +bool loadNcaKeyset() +{ + // Keyset already loaded + if (nca_keyset.key_cnt > 0) return true; + + if (!(envIsSyscallHinted(0x60) && // svcDebugActiveProcess + envIsSyscallHinted(0x63) && // svcGetDebugEvent + envIsSyscallHinted(0x65) && // svcGetProcessList + envIsSyscallHinted(0x69) && // svcQueryDebugProcessMemory + envIsSyscallHinted(0x6a))) // svcReadDebugProcessMemory + { + uiDrawString("Error: please run the application with debug svc permissions!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + return getNcaKeys(); +} + +size_t aes128XtsNintendoCrypt(Aes128XtsContext *ctx, void *dst, const void *src, size_t size, u32 sector, bool encrypt) +{ + if (!ctx || !dst || !src || !size || (size % NCA_AES_XTS_SECTOR_SIZE) != 0) return 0; + + u32 i; + size_t crypt_res = 0, out = 0; + u32 cur_sector = sector; + + for(i = 0; i < size; i += NCA_AES_XTS_SECTOR_SIZE, cur_sector++) + { + // We have to force a sector reset on each new sector to actually enable Nintendo AES-XTS cipher tweak + aes128XtsContextResetSector(ctx, cur_sector, true); + + if (encrypt) + { + crypt_res = aes128XtsEncrypt(ctx, (u8*)dst + i, (const u8*)src + i, NCA_AES_XTS_SECTOR_SIZE); + } else { + crypt_res = aes128XtsDecrypt(ctx, (u8*)dst + i, (const u8*)src + i, NCA_AES_XTS_SECTOR_SIZE); + } + + if (crypt_res != NCA_AES_XTS_SECTOR_SIZE) break; + + out += crypt_res; + } + + return out; +} + +/* Updates the CTR for an offset. */ +static void nca_update_ctr(unsigned char *ctr, u64 ofs) +{ + ofs >>= 4; + unsigned int i; + + for(i = 0; i < 0x8; i++) + { + ctr[0x10 - i - 1] = (unsigned char)(ofs & 0xFF); + ofs >>= 8; + } +} + +bool processNcaCtrSectionBlock(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, u64 offset, void *outBuf, size_t bufSize, Aes128CtrContext *ctx, bool encrypt) +{ + if (!ncmStorage || !ncaId || !outBuf || !bufSize || !ctx) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid parameters to process %s NCA section block!", encrypt ? "decrypted" : "encrypted"); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (!loadNcaKeyset()) return false; + + Result result; + unsigned char ctr[0x10]; + u8 *tmp_buf = NULL; + + char nca_id[33] = {'\0'}; + convertDataToHexString(ncaId->c, 16, nca_id, 33); + + u64 block_start_offset = (offset - (offset % 0x10)); + u64 block_end_offset = (u64)round_up(offset + bufSize, 0x10); + u64 block_size = (block_end_offset - block_start_offset); + + tmp_buf = malloc(block_size); + if (!tmp_buf) + { + uiDrawString("Error: unable to allocate memory for the temporary NCA section block read buffer!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (R_FAILED(result = ncmContentStorageReadContentIdFile(ncmStorage, ncaId, block_start_offset, tmp_buf, block_size))) + { + free(tmp_buf); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: ncmContentStorageReadContentIdFile failed for %lu bytes block at offset 0x%016lX from NCA \"%s\"! (0x%08X)", block_size, block_start_offset, nca_id, result); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + memcpy(ctr, ctx->ctr, 0x10); + nca_update_ctr(ctr, block_start_offset); + + // Decrypt + aes128CtrContextResetCtr(ctx, ctr); + aes128CtrCrypt(ctx, tmp_buf, tmp_buf, block_size); + + if (encrypt) + { + memcpy(tmp_buf + (offset - block_start_offset), outBuf, bufSize); + + // Encrypt + aes128CtrContextResetCtr(ctx, ctr); + aes128CtrCrypt(ctx, tmp_buf, tmp_buf, block_size); + } + + memcpy(outBuf, tmp_buf + (offset - block_start_offset), bufSize); + + free(tmp_buf); + + return true; +} + +bool encryptNcaHeader(nca_header_t *input, u8 *outBuf, u64 outBufSize) +{ + if (!input || !outBuf || !outBufSize || outBufSize < NCA_FULL_HEADER_LENGTH || (bswap_32(input->magic) != NCA3_MAGIC && bswap_32(input->magic) != NCA2_MAGIC)) + { + uiDrawString("Error: invalid NCA header encryption parameters.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (!loadNcaKeyset()) return false; + + u32 i; + size_t crypt_res; + Aes128XtsContext hdr_aes_ctx; + + u8 header_key_0[16]; + u8 header_key_1[16]; + + memcpy(header_key_0, nca_keyset.header_key, 16); + memcpy(header_key_1, nca_keyset.header_key + 16, 16); + + aes128XtsContextCreate(&hdr_aes_ctx, header_key_0, header_key_1, true); + + if (bswap_32(input->magic) == NCA3_MAGIC) + { + crypt_res = aes128XtsNintendoCrypt(&hdr_aes_ctx, outBuf, input, NCA_FULL_HEADER_LENGTH, 0, true); + if (crypt_res != NCA_FULL_HEADER_LENGTH) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid output length for encrypted NCA header! (%u != %lu)", NCA_FULL_HEADER_LENGTH, crypt_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + } else + if (bswap_32(input->magic) == NCA2_MAGIC) + { + crypt_res = aes128XtsNintendoCrypt(&hdr_aes_ctx, outBuf, input, NCA_HEADER_LENGTH, 0, true); + if (crypt_res != NCA_HEADER_LENGTH) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid output length for encrypted NCA header! (%u != %lu)", NCA_HEADER_LENGTH, crypt_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + for(i = 0; i < NCA_SECTION_HEADER_CNT; i++) + { + crypt_res = aes128XtsNintendoCrypt(&hdr_aes_ctx, outBuf + NCA_HEADER_LENGTH + (i * NCA_SECTION_HEADER_LENGTH), &(input->fs_headers[i]), NCA_SECTION_HEADER_LENGTH, 0, true); + if (crypt_res != NCA_SECTION_HEADER_LENGTH) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid output length for encrypted NCA header section #%u! (%u != %lu)", i, NCA_SECTION_HEADER_LENGTH, crypt_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + } + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid decrypted NCA magic word! (0x%08X)", bswap_32(input->magic)); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + return true; +} + +bool decryptNcaHeader(const u8 *ncaBuf, u64 ncaBufSize, nca_header_t *out, title_rights_ctx *rights_info, u8 *decrypted_nca_keys) +{ + if (!ncaBuf || !ncaBufSize || ncaBufSize < NCA_FULL_HEADER_LENGTH || !out || !decrypted_nca_keys) + { + uiDrawString("Error: invalid NCA header decryption parameters.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (!loadNcaKeyset()) return false; + + u32 i; + size_t crypt_res; + Aes128XtsContext hdr_aes_ctx; + + u8 header_key_0[16]; + u8 header_key_1[16]; + + bool has_rights_id = false; + + memcpy(header_key_0, nca_keyset.header_key, 16); + memcpy(header_key_1, nca_keyset.header_key + 16, 16); + + aes128XtsContextCreate(&hdr_aes_ctx, header_key_0, header_key_1, false); + + crypt_res = aes128XtsNintendoCrypt(&hdr_aes_ctx, out, ncaBuf, NCA_HEADER_LENGTH, 0, false); + if (crypt_res != NCA_HEADER_LENGTH) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid output length for decrypted NCA header! (%u != %lu)", NCA_HEADER_LENGTH, crypt_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (bswap_32(out->magic) == NCA3_MAGIC) + { + crypt_res = aes128XtsNintendoCrypt(&hdr_aes_ctx, out, ncaBuf, NCA_FULL_HEADER_LENGTH, 0, false); + if (crypt_res != NCA_FULL_HEADER_LENGTH) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid output length for decrypted NCA header! (%u != %lu)", NCA_FULL_HEADER_LENGTH, crypt_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + } else + if (bswap_32(out->magic) == NCA2_MAGIC) + { + for(i = 0; i < NCA_SECTION_HEADER_CNT; i++) + { + if (out->fs_headers[i]._0x148[0] != 0 || memcmp(out->fs_headers[i]._0x148, out->fs_headers[i]._0x148 + 1, 0xB7)) + { + crypt_res = aes128XtsNintendoCrypt(&hdr_aes_ctx, &(out->fs_headers[i]), ncaBuf + NCA_HEADER_LENGTH + (i * NCA_SECTION_HEADER_LENGTH), NCA_SECTION_HEADER_LENGTH, 0, false); + if (crypt_res != NCA_SECTION_HEADER_LENGTH) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid output length for decrypted NCA header section #%u! (%u != %lu)", i, NCA_SECTION_HEADER_LENGTH, crypt_res); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + } else { + memset(&(out->fs_headers[i]), 0, sizeof(nca_fs_header_t)); + } + } + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid NCA magic word! Wrong header key? (0x%08X)", bswap_32(out->magic)); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + for(i = 0; i < 0x10; i++) + { + if (out->rights_id[i] != 0) + { + has_rights_id = true; + break; + } + } + + if (has_rights_id) + { + if (rights_info != NULL && !rights_info->has_rights_id) + { + rights_info->has_rights_id = true; + memcpy(rights_info->rights_id, out->rights_id, 16); + convertDataToHexString(out->rights_id, 16, rights_info->rights_id_str, 33); + sprintf(rights_info->tik_filename, "%s.tik", rights_info->rights_id_str); + sprintf(rights_info->cert_filename, "%s.cert", rights_info->rights_id_str); + } + } else { + if (!decryptNcaKeyArea(out, decrypted_nca_keys)) return false; + } + + return true; +} + +bool processProgramNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, nca_header_t *dec_nca_header, cnmt_xml_content_info *xml_content_info, nca_program_mod_data *output) +{ + if (!ncmStorage || !ncaId || !dec_nca_header || !xml_content_info || !output) + { + uiDrawString("Error: invalid parameters to process Program NCA!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (dec_nca_header->fs_headers[0].partition_type != NCA_FS_HEADER_PARTITION_PFS0 || dec_nca_header->fs_headers[0].fs_type != NCA_FS_HEADER_FSTYPE_PFS0) + { + uiDrawString("Error: Program NCA section #0 doesn't hold a PFS0 partition!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (!dec_nca_header->fs_headers[0].pfs0_superblock.pfs0_size) + { + uiDrawString("Error: invalid size for PFS0 partition in Program NCA section #0!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (dec_nca_header->fs_headers[0].crypt_type != NCA_FS_HEADER_CRYPT_CTR) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid AES crypt type for Program NCA section #0! (0x%02X)", dec_nca_header->fs_headers[0].crypt_type); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + u32 i; + + u64 section_offset; + u64 hash_table_offset; + u64 nca_pfs0_offset; + + pfs0_header nca_pfs0_header; + pfs0_entry_table *nca_pfs0_entries = NULL; + u64 nca_pfs0_data_offset; + + npdm_t npdm_header; + bool found_meta = false; + u64 meta_offset; + u64 acid_pubkey_offset; + + u64 block_hash_table_offset; + u64 block_hash_table_end_offset; + u64 block_start_offset[2] = { 0, 0 }; + u64 block_size[2] = { 0, 0 }; + u8 block_hash[2][SHA256_HASH_LENGTH]; + u8 *block_data[2] = { NULL, NULL }; + + u64 sig_write_size[2] = { 0, 0 }; + + u8 *hash_table = NULL; + + Aes128CtrContext aes_ctx; + + section_offset = ((u64)dec_nca_header->section_entries[0].media_start_offset * (u64)MEDIA_UNIT_SIZE); + hash_table_offset = (section_offset + dec_nca_header->fs_headers[0].pfs0_superblock.hash_table_offset); + nca_pfs0_offset = (section_offset + dec_nca_header->fs_headers[0].pfs0_superblock.pfs0_offset); + + if (!section_offset || section_offset < NCA_FULL_HEADER_LENGTH || !hash_table_offset || hash_table_offset < section_offset || !nca_pfs0_offset || nca_pfs0_offset <= hash_table_offset) + { + uiDrawString("Error: invalid offsets for Program NCA section #0!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + // Generate initial CTR + unsigned char ctr[0x10]; + u64 ofs = (section_offset >> 4); + + for(i = 0; i < 0x8; i++) + { + ctr[i] = dec_nca_header->fs_headers[0].section_ctr[0x08 - i - 1]; + ctr[0x10 - i - 1] = (unsigned char)(ofs & 0xFF); + ofs >>= 8; + } + + u8 ctr_key[NCA_KEY_AREA_KEY_SIZE]; + memcpy(ctr_key, xml_content_info->decrypted_nca_keys + (NCA_KEY_AREA_KEY_SIZE * 2), NCA_KEY_AREA_KEY_SIZE); + aes128CtrContextCreate(&aes_ctx, ctr_key, ctr); + + if (!processNcaCtrSectionBlock(ncmStorage, ncaId, nca_pfs0_offset, &nca_pfs0_header, sizeof(pfs0_header), &aes_ctx, false)) + { + breaks++; + uiDrawString("Failed to read Program NCA section #0 PFS0 partition header!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (bswap_32(nca_pfs0_header.magic) != PFS0_MAGIC) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid magic word for Program NCA section #0 PFS0 partition! Wrong KAEK? (0x%08X)", bswap_32(nca_pfs0_header.magic)); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (!nca_pfs0_header.file_cnt || !nca_pfs0_header.str_table_size) + { + uiDrawString("Error: Program NCA section #0 PFS0 partition is empty! Wrong KAEK?", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + nca_pfs0_entries = calloc(nca_pfs0_header.file_cnt, sizeof(pfs0_entry_table)); + if (!nca_pfs0_entries) + { + uiDrawString("Error: unable to allocate memory for Program NCA section #0 PFS0 partition entries!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (!processNcaCtrSectionBlock(ncmStorage, ncaId, nca_pfs0_offset + sizeof(pfs0_header), nca_pfs0_entries, (u64)nca_pfs0_header.file_cnt * sizeof(pfs0_entry_table), &aes_ctx, false)) + { + breaks++; + uiDrawString("Failed to read Program NCA section #0 PFS0 partition entries!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(nca_pfs0_entries); + return false; + } + + nca_pfs0_data_offset = (nca_pfs0_offset + sizeof(pfs0_header) + ((u64)nca_pfs0_header.file_cnt * sizeof(pfs0_entry_table)) + (u64)nca_pfs0_header.str_table_size); + + // Looking for META magic + for(i = 0; i < nca_pfs0_header.file_cnt; i++) + { + u64 nca_pfs0_cur_file_offset = (nca_pfs0_data_offset + nca_pfs0_entries[i].file_offset); + + // Read and decrypt NPDM header + if (!processNcaCtrSectionBlock(ncmStorage, ncaId, nca_pfs0_cur_file_offset, &npdm_header, sizeof(npdm_t), &aes_ctx, false)) + { + breaks++; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to read Program NCA section #0 PFS0 entry #%u!", i); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(nca_pfs0_entries); + return false; + } + + if (bswap_32(npdm_header.magic) == META_MAGIC) + { + found_meta = true; + meta_offset = nca_pfs0_cur_file_offset; + acid_pubkey_offset = (meta_offset + (u64)npdm_header.acid_offset + (u64)NPDM_SIGNATURE_SIZE); + break; + } + } + + free(nca_pfs0_entries); + + if (!found_meta) + { + uiDrawString("Error: unable to find NPDM entry in Program NCA section #0 PFS0 partition!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + // Calculate block offsets + block_hash_table_offset = (hash_table_offset + (((acid_pubkey_offset - nca_pfs0_offset) / (u64)dec_nca_header->fs_headers[0].pfs0_superblock.block_size)) * (u64)SHA256_HASH_LENGTH); + block_hash_table_end_offset = (hash_table_offset + (((acid_pubkey_offset + (u64)NPDM_SIGNATURE_SIZE - nca_pfs0_offset) / (u64)dec_nca_header->fs_headers[0].pfs0_superblock.block_size) * (u64)SHA256_HASH_LENGTH)); + block_start_offset[0] = (nca_pfs0_offset + (((acid_pubkey_offset - nca_pfs0_offset) / (u64)dec_nca_header->fs_headers[0].pfs0_superblock.block_size) * (u64)dec_nca_header->fs_headers[0].pfs0_superblock.block_size)); + + // Make sure our block doesn't pass PFS0 end offset + if ((block_start_offset[0] - nca_pfs0_offset + dec_nca_header->fs_headers[0].pfs0_superblock.block_size) > dec_nca_header->fs_headers[0].pfs0_superblock.pfs0_size) + { + block_size[0] = (dec_nca_header->fs_headers[0].pfs0_superblock.pfs0_size - (block_start_offset[0] - nca_pfs0_offset)); + } else { + block_size[0] = (u64)dec_nca_header->fs_headers[0].pfs0_superblock.block_size; + } + + block_data[0] = malloc(block_size[0]); + if (!block_data[0]) + { + uiDrawString("Error: unable to allocate memory for Program NCA section #0 PFS0 NPDM block 0!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + // Read and decrypt block + if (!processNcaCtrSectionBlock(ncmStorage, ncaId, block_start_offset[0], block_data[0], block_size[0], &aes_ctx, false)) + { + breaks++; + uiDrawString("Failed to read Program NCA section #0 PFS0 NPDM block 0!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(block_data[0]); + return false; + } + + // Make sure that 1 block will cover all patched bytes, otherwise we'll have to recalculate another hash block + if (block_hash_table_offset != block_hash_table_end_offset) + { + sig_write_size[1] = (acid_pubkey_offset - block_start_offset[0] + (u64)NPDM_SIGNATURE_SIZE - block_size[0]); + sig_write_size[0] = ((u64)NPDM_SIGNATURE_SIZE - sig_write_size[1]); + } else { + sig_write_size[0] = (u64)NPDM_SIGNATURE_SIZE; + } + + // Patch acid pubkey changing it to a self-generated pubkey + memcpy(block_data[0] + (acid_pubkey_offset - block_start_offset[0]), rsa_get_public_key(), sig_write_size[0]); + + // Calculate new block hash + sha256CalculateHash(block_hash[0], block_data[0], block_size[0]); + + if (block_hash_table_offset != block_hash_table_end_offset) + { + block_start_offset[1] = (nca_pfs0_offset + (((acid_pubkey_offset + (u64)NPDM_SIGNATURE_SIZE - nca_pfs0_offset) / (u64)dec_nca_header->fs_headers[0].pfs0_superblock.block_size) * (u64)dec_nca_header->fs_headers[0].pfs0_superblock.block_size)); + + if ((block_start_offset[1] - nca_pfs0_offset + dec_nca_header->fs_headers[0].pfs0_superblock.block_size) > dec_nca_header->fs_headers[0].pfs0_superblock.pfs0_size) + { + block_size[1] = (dec_nca_header->fs_headers[0].pfs0_superblock.pfs0_size - (block_start_offset[1] - nca_pfs0_offset)); + } else { + block_size[1] = (u64)dec_nca_header->fs_headers[0].pfs0_superblock.block_size; + } + + block_data[1] = malloc(block_size[1]); + if (!block_data[1]) + { + uiDrawString("Error: unable to allocate memory for Program NCA section #0 PFS0 NPDM block 1!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(block_data[0]); + return false; + } + + if (!processNcaCtrSectionBlock(ncmStorage, ncaId, block_start_offset[1], block_data[1], block_size[1], &aes_ctx, false)) + { + breaks++; + uiDrawString("Failed to read Program NCA section #0 PFS0 NPDM block 1!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(block_data[0]); + free(block_data[1]); + return false; + } + + memcpy(block_data[1], rsa_get_public_key() + sig_write_size[0], sig_write_size[1]); + + sha256CalculateHash(block_hash[1], block_data[1], block_size[1]); + } + + hash_table = malloc(dec_nca_header->fs_headers[0].pfs0_superblock.hash_table_size); + if (!hash_table) + { + uiDrawString("Error: unable to allocate memory for Program NCA section #0 PFS0 hash table!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(block_data[0]); + if (block_data[1]) free(block_data[1]); + return false; + } + + if (!processNcaCtrSectionBlock(ncmStorage, ncaId, hash_table_offset, hash_table, dec_nca_header->fs_headers[0].pfs0_superblock.hash_table_size, &aes_ctx, false)) + { + breaks++; + uiDrawString("Failed to read Program NCA section #0 PFS0 hash table!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(block_data[0]); + if (block_data[1]) free(block_data[1]); + free(hash_table); + return false; + } + + // Update block hashes + memcpy(hash_table + (block_hash_table_offset - hash_table_offset), block_hash[0], SHA256_HASH_LENGTH); + if (block_hash_table_offset != block_hash_table_end_offset) memcpy(hash_table + (block_hash_table_end_offset - hash_table_offset), block_hash[1], SHA256_HASH_LENGTH); + + // Calculate PFS0 superblock master hash + sha256CalculateHash(dec_nca_header->fs_headers[0].pfs0_superblock.master_hash, hash_table, dec_nca_header->fs_headers[0].pfs0_superblock.hash_table_size); + + // Calculate section hash + sha256CalculateHash(dec_nca_header->section_hashes[0], &(dec_nca_header->fs_headers[0]), sizeof(nca_fs_header_t)); + + // Recreate NPDM signature + if (!rsa_sign(&(dec_nca_header->magic), NPDM_SIGNATURE_AREA_SIZE, dec_nca_header->npdm_key_sig, NPDM_SIGNATURE_SIZE)) + { + breaks++; + uiDrawString("Failed to recreate Program NCA NPDM signature!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(block_data[0]); + if (block_data[1]) free(block_data[1]); + free(hash_table); + return false; + } + + // Reencrypt relevant data blocks + if (!processNcaCtrSectionBlock(ncmStorage, ncaId, block_start_offset[0], block_data[0], block_size[0], &aes_ctx, true)) + { + breaks++; + uiDrawString("Failed to encrypt Program NCA section #0 PFS0 NPDM block 0!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(block_data[0]); + if (block_data[1]) free(block_data[1]); + free(hash_table); + return false; + } + + if (block_hash_table_offset != block_hash_table_end_offset) + { + if (!processNcaCtrSectionBlock(ncmStorage, ncaId, block_start_offset[1], block_data[1], block_size[1], &aes_ctx, true)) + { + breaks++; + uiDrawString("Failed to encrypt Program NCA section #0 PFS0 NPDM block 1!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(block_data[0]); + free(block_data[1]); + free(hash_table); + return false; + } + } + + if (!processNcaCtrSectionBlock(ncmStorage, ncaId, hash_table_offset, hash_table, dec_nca_header->fs_headers[0].pfs0_superblock.hash_table_size, &aes_ctx, true)) + { + breaks++; + uiDrawString("Failed to encrypt Program NCA section #0 PFS0 hash table!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(block_data[0]); + if (block_data[1]) free(block_data[1]); + free(hash_table); + return false; + } + + // Save data to the output struct so we can write it later + // The caller function must free these data pointers + output->hash_table = hash_table; + output->hash_table_offset = hash_table_offset; + output->hash_table_size = dec_nca_header->fs_headers[0].pfs0_superblock.hash_table_size; + + output->block_mod_cnt = (block_hash_table_offset != block_hash_table_end_offset ? 2 : 1); + + output->block_data[0] = block_data[0]; + output->block_offset[0] = block_start_offset[0]; + output->block_size[0] = block_size[0]; + + if (block_hash_table_offset != block_hash_table_end_offset) + { + output->block_data[1] = block_data[1]; + output->block_offset[1] = block_start_offset[1]; + output->block_size[1] = block_size[1]; + } + + return true; +} + +bool retrieveCnmtNcaData(nspDumpType selectedNspDumpType, u8 *ncaBuf, cnmt_xml_program_info *xml_program_info, cnmt_xml_content_info *xml_content_info, nca_cnmt_mod_data *output, title_rights_ctx *rights_info) +{ + if (!ncaBuf || !xml_program_info || !xml_content_info || !output || !rights_info) + { + uiDrawString("Error: invalid parameters to retrieve CNMT NCA!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + nca_header_t dec_header; + + u64 section_offset; + u64 section_size; + u8 *section_data = NULL; + + Aes128CtrContext aes_ctx; + + u64 nca_pfs0_offset; + u64 nca_pfs0_str_table_offset; + u64 nca_pfs0_data_offset; + pfs0_header nca_pfs0_header; + pfs0_entry_table *nca_pfs0_entries = NULL; + + bool found_cnmt = false; + + u64 title_cnmt_offset; + u64 title_cnmt_size; + + cnmt_header title_cnmt_header; + cnmt_extended_header title_cnmt_extended_header; + + u64 digest_offset; + + // Generate filename for our required CNMT file + char cnmtFileName[50] = {'\0'}; + snprintf(cnmtFileName, sizeof(cnmtFileName) / sizeof(cnmtFileName[0]), "%s_%016lx.cnmt", getTitleType(xml_program_info->type), xml_program_info->title_id); + + if (!decryptNcaHeader(ncaBuf, xml_content_info->size, &dec_header, rights_info, xml_content_info->decrypted_nca_keys)) return false; + + if (dec_header.fs_headers[0].partition_type != NCA_FS_HEADER_PARTITION_PFS0 || dec_header.fs_headers[0].fs_type != NCA_FS_HEADER_FSTYPE_PFS0) + { + uiDrawString("Error: CNMT NCA section #0 doesn't hold a PFS0 partition!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (!dec_header.fs_headers[0].pfs0_superblock.pfs0_size) + { + uiDrawString("Error: invalid size for PFS0 partition in CNMT NCA section #0!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (dec_header.fs_headers[0].crypt_type != NCA_FS_HEADER_CRYPT_CTR) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid AES crypt type for CNMT NCA section #0! (0x%02X)", dec_header.fs_headers[0].crypt_type); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + // Modify distribution type + if (selectedNspDumpType != DUMP_PATCH_NSP) dec_header.distribution = 0; + + section_offset = ((u64)dec_header.section_entries[0].media_start_offset * (u64)MEDIA_UNIT_SIZE); + section_size = (((u64)dec_header.section_entries[0].media_end_offset * (u64)MEDIA_UNIT_SIZE) - section_offset); + + if (!section_offset || section_offset < NCA_FULL_HEADER_LENGTH || !section_size) + { + uiDrawString("Error: invalid offset/size for CNMT NCA section #0!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + // Generate initial CTR + unsigned char ctr[0x10]; + unsigned int i; + u64 ofs = (section_offset >> 4); + + for(i = 0; i < 0x8; i++) + { + ctr[i] = dec_header.fs_headers[0].section_ctr[0x08 - i - 1]; + ctr[0x10 - i - 1] = (unsigned char)(ofs & 0xFF); + ofs >>= 8; + } + + u8 ctr_key[NCA_KEY_AREA_KEY_SIZE]; + memcpy(ctr_key, xml_content_info->decrypted_nca_keys + (NCA_KEY_AREA_KEY_SIZE * 2), NCA_KEY_AREA_KEY_SIZE); + aes128CtrContextCreate(&aes_ctx, ctr_key, ctr); + + section_data = malloc(section_size); + if (!section_data) + { + uiDrawString("Error: unable to allocate memory for the decrypted CNMT NCA section #0!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + aes128CtrCrypt(&aes_ctx, section_data, ncaBuf + section_offset, section_size); + + nca_pfs0_offset = dec_header.fs_headers[0].pfs0_superblock.pfs0_offset; + memcpy(&nca_pfs0_header, section_data + nca_pfs0_offset, sizeof(pfs0_header)); + + if (bswap_32(nca_pfs0_header.magic) != PFS0_MAGIC) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid magic word for CNMT NCA section #0 PFS0 partition! Wrong KAEK? (0x%08X)", bswap_32(nca_pfs0_header.magic)); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(section_data); + return false; + } + + if (!nca_pfs0_header.file_cnt || !nca_pfs0_header.str_table_size) + { + uiDrawString("Error: CNMT NCA section #0 PFS0 partition is empty! Wrong KAEK?", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(section_data); + return false; + } + + nca_pfs0_entries = calloc(nca_pfs0_header.file_cnt, sizeof(pfs0_entry_table)); + if (!nca_pfs0_entries) + { + uiDrawString("Error: unable to allocate memory for CNMT NCA section #0 PFS0 partition entries!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(section_data); + return false; + } + + memcpy(nca_pfs0_entries, section_data + nca_pfs0_offset + sizeof(pfs0_header), (u64)nca_pfs0_header.file_cnt * sizeof(pfs0_entry_table)); + + nca_pfs0_str_table_offset = (nca_pfs0_offset + sizeof(pfs0_header) + ((u64)nca_pfs0_header.file_cnt * sizeof(pfs0_entry_table))); + nca_pfs0_data_offset = (nca_pfs0_str_table_offset + (u64)nca_pfs0_header.str_table_size); + + // Looking for the CNMT + for(i = 0; i < nca_pfs0_header.file_cnt; i++) + { + u64 filename_offset = (nca_pfs0_str_table_offset + nca_pfs0_entries[i].filename_offset); + if (!strncasecmp((char*)section_data + filename_offset, cnmtFileName, strlen(cnmtFileName))) + { + found_cnmt = true; + title_cnmt_offset = (nca_pfs0_data_offset + nca_pfs0_entries[i].file_offset); + title_cnmt_size = nca_pfs0_entries[i].file_size; + break; + } + } + + free(nca_pfs0_entries); + + if (!found_cnmt) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unable to find file \"%s\" in PFS0 partition from CNMT NCA section #0!", cnmtFileName); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(section_data); + return false; + } + + memcpy(&title_cnmt_header, section_data + title_cnmt_offset, sizeof(cnmt_header)); + memcpy(&title_cnmt_extended_header, section_data + title_cnmt_offset + sizeof(cnmt_header), sizeof(cnmt_extended_header)); + + // Fill information for our CNMT XML + digest_offset = (title_cnmt_offset + title_cnmt_size - (u64)SHA256_HASH_LENGTH); + memcpy(xml_program_info->digest, section_data + digest_offset, SHA256_HASH_LENGTH); + convertDataToHexString(xml_program_info->digest, 32, xml_program_info->digest_str, 65); + xml_content_info->keyblob = (dec_header.crypto_type2 > dec_header.crypto_type ? dec_header.crypto_type2 : dec_header.crypto_type); + xml_program_info->min_keyblob = (rights_info->has_rights_id ? rights_info->rights_id[15] : xml_content_info->keyblob); + xml_program_info->min_sysver = title_cnmt_extended_header.min_sysver; + xml_program_info->patch_tid = title_cnmt_extended_header.patch_tid; + + // Empty CNMT content records + memset(section_data + title_cnmt_offset + sizeof(cnmt_header) + (u64)title_cnmt_header.table_offset, 0, title_cnmt_size - sizeof(cnmt_header) - (u64)title_cnmt_header.table_offset - (u64)SHA256_HASH_LENGTH); + + // Replace input buffer data in-place + memcpy(ncaBuf, &dec_header, NCA_FULL_HEADER_LENGTH); + memcpy(ncaBuf + section_offset, section_data, section_size); + + free(section_data); + + // Update offsets + nca_pfs0_offset += section_offset; + title_cnmt_offset += section_offset; + + // Save data to output struct + output->section_offset = section_offset; + output->section_size = section_size; + output->hash_table_offset = (section_offset + dec_header.fs_headers[0].pfs0_superblock.hash_table_offset); + output->pfs0_offset = nca_pfs0_offset; + output->pfs0_size = dec_header.fs_headers[0].pfs0_superblock.pfs0_size; + output->title_cnmt_offset = title_cnmt_offset; + output->title_cnmt_size = title_cnmt_size; + + return true; +} + +bool patchCnmtNca(u8 *ncaBuf, u64 ncaBufSize, cnmt_xml_program_info *xml_program_info, cnmt_xml_content_info *xml_content_info, nca_cnmt_mod_data *cnmt_mod) +{ + if (!ncaBuf || !ncaBufSize || !xml_program_info || xml_program_info->nca_cnt <= 1 || !xml_content_info || !cnmt_mod) + { + uiDrawString("Error: invalid parameters to patch CNMT NCA!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + u32 i; + u32 nca_cnt; + + cnmt_header title_cnmt_header; + cnmt_content_record *title_cnmt_content_records = NULL; + u64 title_cnmt_content_records_offset; + + u8 pfs0_block_hash[SHA256_HASH_LENGTH]; + + nca_header_t dec_header; + + Aes128CtrContext aes_ctx; + + // Update number of content records + nca_cnt = (xml_program_info->nca_cnt - 1); // Discard CNMT NCA + memcpy(&title_cnmt_header, ncaBuf + cnmt_mod->title_cnmt_offset, sizeof(cnmt_header)); + title_cnmt_header.content_records_cnt = (u16)nca_cnt; + memcpy(ncaBuf + cnmt_mod->title_cnmt_offset, &title_cnmt_header, sizeof(cnmt_header)); + + // Allocate memory for our content records + title_cnmt_content_records = calloc(nca_cnt, sizeof(cnmt_content_record)); + if (!title_cnmt_content_records) + { + uiDrawString("Error: unable to allocate memory for CNMT NCA content records!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + title_cnmt_content_records_offset = (cnmt_mod->title_cnmt_offset + sizeof(cnmt_header) + (u64)title_cnmt_header.table_offset); + memcpy(title_cnmt_content_records, ncaBuf + title_cnmt_content_records_offset, (u64)nca_cnt * sizeof(cnmt_content_record)); + + // Write content records + for(i = 0; i < nca_cnt; i++) + { + memcpy(title_cnmt_content_records[i].hash, xml_content_info[i].hash, 32); + memcpy(title_cnmt_content_records[i].nca_id, xml_content_info[i].nca_id, 16); + convertU64ToNcaSize(xml_content_info[i].size, title_cnmt_content_records[i].size); + title_cnmt_content_records[i].type = xml_content_info[i].type; + + u64 cur_content_record = (title_cnmt_content_records_offset + ((u64)i * sizeof(cnmt_content_record))); + memcpy(ncaBuf + cur_content_record, &(title_cnmt_content_records[i]), sizeof(cnmt_content_record)); + } + + free(title_cnmt_content_records); + + // Calculate block hash + sha256CalculateHash(pfs0_block_hash, ncaBuf + cnmt_mod->pfs0_offset, cnmt_mod->pfs0_size); + memcpy(ncaBuf + cnmt_mod->hash_table_offset, pfs0_block_hash, SHA256_HASH_LENGTH); + + // Copy header to struct + memcpy(&dec_header, ncaBuf, sizeof(nca_header_t)); + + // Calculate PFS0 superblock master hash + sha256CalculateHash(dec_header.fs_headers[0].pfs0_superblock.master_hash, ncaBuf + cnmt_mod->hash_table_offset, dec_header.fs_headers[0].pfs0_superblock.hash_table_size); + + // Calculate section hash + sha256CalculateHash(dec_header.section_hashes[0], &(dec_header.fs_headers[0]), sizeof(nca_fs_header_t)); + + // Generate initial CTR + unsigned char ctr[0x10]; + u64 ofs = (cnmt_mod->section_offset >> 4); + + for(i = 0; i < 0x8; i++) + { + ctr[i] = dec_header.fs_headers[0].section_ctr[0x08 - i - 1]; + ctr[0x10 - i - 1] = (unsigned char)(ofs & 0xFF); + ofs >>= 8; + } + + u8 ctr_key[NCA_KEY_AREA_KEY_SIZE]; + memcpy(ctr_key, xml_content_info[xml_program_info->nca_cnt - 1].decrypted_nca_keys + (NCA_KEY_AREA_KEY_SIZE * 2), NCA_KEY_AREA_KEY_SIZE); + aes128CtrContextCreate(&aes_ctx, ctr_key, ctr); + + // Reencrypt CNMT NCA + if (!encryptNcaHeader(&dec_header, ncaBuf, ncaBufSize)) + { + breaks++; + uiDrawString("Failed to encrypt modified CNMT NCA header!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + aes128CtrCrypt(&aes_ctx, ncaBuf + cnmt_mod->section_offset, ncaBuf + cnmt_mod->section_offset, cnmt_mod->section_size); + + // Calculate CNMT NCA SHA-256 checksum and fill information for our CNMT XML + sha256CalculateHash(xml_content_info[xml_program_info->nca_cnt - 1].hash, ncaBuf, ncaBufSize); + convertDataToHexString(xml_content_info[xml_program_info->nca_cnt - 1].hash, 32, xml_content_info[xml_program_info->nca_cnt - 1].hash_str, 65); + memcpy(xml_content_info[xml_program_info->nca_cnt - 1].nca_id, xml_content_info[xml_program_info->nca_cnt - 1].hash, 16); + convertDataToHexString(xml_content_info[xml_program_info->nca_cnt - 1].nca_id, 16, xml_content_info[xml_program_info->nca_cnt - 1].nca_id_str, 33); + + return true; +} + +bool readRomFsEntriesFromNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, nca_header_t *dec_nca_header, u8 *decrypted_nca_keys) +{ + if (!ncmStorage || !ncaId || !dec_nca_header || !decrypted_nca_keys) + { + uiDrawString("Error: invalid parameters to read RomFS section from NCA!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + u8 romfs_index; + bool found_romfs = false; + + u32 i; + + u64 section_offset; + u64 section_size; + + Aes128CtrContext aes_ctx; + + u64 romfs_offset; + u64 romfs_size; + romfs_header romFsHeader; + + u64 romfs_dirtable_offset; + u64 romfs_dirtable_size; + + u64 romfs_filetable_offset; + u64 romfs_filetable_size; + + u64 romfs_filedata_offset; + + romfs_dir *romfs_dir_entries = NULL; + romfs_file *romfs_file_entries = NULL; + + for(romfs_index = 0; romfs_index < 4; romfs_index++) + { + if (dec_nca_header->fs_headers[romfs_index].partition_type == NCA_FS_HEADER_PARTITION_ROMFS && dec_nca_header->fs_headers[romfs_index].fs_type == NCA_FS_HEADER_FSTYPE_ROMFS) + { + found_romfs = true; + break; + } + } + + if (!found_romfs) + { + uiDrawString("Error: NCA doesn't hold a RomFS section!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + section_offset = ((u64)dec_nca_header->section_entries[romfs_index].media_start_offset * (u64)MEDIA_UNIT_SIZE); + section_size = (((u64)dec_nca_header->section_entries[romfs_index].media_end_offset * (u64)MEDIA_UNIT_SIZE) - section_offset); + + if (!section_offset || section_offset < NCA_FULL_HEADER_LENGTH || !section_size) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid offset/size for NCA RomFS section! (#%u)", romfs_index); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + // Generate initial CTR + unsigned char ctr[0x10]; + u64 ofs = (section_offset >> 4); + + for(i = 0; i < 0x8; i++) + { + ctr[i] = dec_nca_header->fs_headers[romfs_index].section_ctr[0x08 - i - 1]; + ctr[0x10 - i - 1] = (unsigned char)(ofs & 0xFF); + ofs >>= 8; + } + + u8 ctr_key[NCA_KEY_AREA_KEY_SIZE]; + memcpy(ctr_key, decrypted_nca_keys + (NCA_KEY_AREA_KEY_SIZE * 2), NCA_KEY_AREA_KEY_SIZE); + aes128CtrContextCreate(&aes_ctx, ctr_key, ctr); + + if (bswap_32(dec_nca_header->fs_headers[romfs_index].romfs_superblock.ivfc_header.magic) != IVFC_MAGIC) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid magic word for NCA RomFS section! Wrong KAEK? (0x%08X)", bswap_32(dec_nca_header->fs_headers[romfs_index].romfs_superblock.ivfc_header.magic)); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (dec_nca_header->fs_headers[romfs_index].crypt_type != NCA_FS_HEADER_CRYPT_CTR) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid AES crypt type for NCA RomFS section! (0x%02X)", dec_nca_header->fs_headers[0].crypt_type); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + romfs_offset = (section_offset + dec_nca_header->fs_headers[romfs_index].romfs_superblock.ivfc_header.level_headers[IVFC_MAX_LEVEL - 1].logical_offset); + romfs_size = dec_nca_header->fs_headers[romfs_index].romfs_superblock.ivfc_header.level_headers[IVFC_MAX_LEVEL - 1].hash_data_size; + + if (romfs_offset < section_offset || romfs_offset >= (section_offset + section_size) || romfs_size < ROMFS_HEADER_SIZE || (romfs_offset + romfs_size) > (section_offset + section_size)) + { + uiDrawString("Error: invalid offset/size for NCA RomFS section!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + // First read the RomFS header + if (!processNcaCtrSectionBlock(ncmStorage, ncaId, romfs_offset, &romFsHeader, sizeof(romfs_header), &aes_ctx, false)) + { + breaks++; + uiDrawString("Failed to read NCA RomFS section header!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (romFsHeader.headerSize != ROMFS_HEADER_SIZE) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid header size for NCA RomFS section! (0x%016lX at 0x%016lX)", romFsHeader.headerSize, romfs_offset); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + romfs_dirtable_offset = (romfs_offset + romFsHeader.dirTableOff); + romfs_dirtable_size = romFsHeader.dirTableSize; + + romfs_filetable_offset = (romfs_offset + romFsHeader.fileTableOff); + romfs_filetable_size = romFsHeader.fileTableSize; + + if (romfs_dirtable_offset >= (section_offset + section_size) || !romfs_dirtable_size || (romfs_dirtable_offset + romfs_dirtable_size) >= (section_offset + section_size) || romfs_filetable_offset >= (section_offset + section_size) || !romfs_filetable_size || (romfs_filetable_offset + romfs_filetable_size) >= (section_offset + section_size)) + { + uiDrawString("Error: invalid directory/file table for NCA RomFS section!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + romfs_filedata_offset = (romfs_offset + romFsHeader.fileDataOff); + + if (romfs_filedata_offset >= (section_offset + section_size)) + { + uiDrawString("Error: invalid file data block offset for NCA RomFS section!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + romfs_dir_entries = calloc(1, romfs_dirtable_size); + if (!romfs_dir_entries) + { + uiDrawString("Error: unable to allocate memory for NCA RomFS section directory entries!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + if (!processNcaCtrSectionBlock(ncmStorage, ncaId, romfs_dirtable_offset, romfs_dir_entries, romfs_dirtable_size, &aes_ctx, false)) + { + breaks++; + uiDrawString("Failed to read NCA RomFS section directory entries!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(romfs_dir_entries); + return false; + } + + romfs_file_entries = calloc(1, romfs_filetable_size); + if (!romfs_file_entries) + { + uiDrawString("Error: unable to allocate memory for NCA RomFS section file entries!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(romfs_dir_entries); + return false; + } + + if (!processNcaCtrSectionBlock(ncmStorage, ncaId, romfs_filetable_offset, romfs_file_entries, romfs_filetable_size, &aes_ctx, false)) + { + breaks++; + uiDrawString("Failed to read NCA RomFS section file entries!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + free(romfs_file_entries); + free(romfs_dir_entries); + return false; + } + + // Save data to output struct + // The caller function must free these data pointers + memcpy(&(romFsContext.ncmStorage), ncmStorage, sizeof(NcmContentStorage)); + memcpy(&(romFsContext.ncaId), ncaId, sizeof(NcmNcaId)); + memcpy(&(romFsContext.aes_ctx), &aes_ctx, sizeof(Aes128CtrContext)); + romFsContext.romfs_offset = romfs_offset; + romFsContext.romfs_size = romfs_size; + romFsContext.romfs_dirtable_offset = romfs_dirtable_offset; + romFsContext.romfs_dirtable_size = romfs_dirtable_size; + romFsContext.romfs_dir_entries = romfs_dir_entries; + romFsContext.romfs_filetable_offset = romfs_filetable_offset; + romFsContext.romfs_filetable_size = romfs_filetable_size; + romFsContext.romfs_file_entries = romfs_file_entries; + romFsContext.romfs_filedata_offset = romfs_filedata_offset; + + return true; +} + +char *getNacpLangName(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "AmericanEnglish"; + break; + case 1: + out = "BritishEnglish"; + break; + case 2: + out = "Japanese"; + break; + case 3: + out = "French"; + break; + case 4: + out = "German"; + break; + case 5: + out = "LatinAmericanSpanish"; + break; + case 6: + out = "Spanish"; + break; + case 7: + out = "Italian"; + break; + case 8: + out = "Dutch"; + break; + case 9: + out = "CanadianFrench"; + break; + case 10: + out = "Portuguese"; + break; + case 11: + out = "Russian"; + break; + case 12: + out = "Korean"; + break; + case 13: + out = "TraditionalChinese"; + break; + case 14: + out = "SimplifiedChinese"; + break; + case 15: // Unknown + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpStartupUserAccount(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "None"; + break; + case 1: + out = "Required"; + break; + case 2: + out = "RequiredWithNetworkServiceAccountAvailable"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpUserAccountSwitchLock(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "Disable"; + break; + case 1: + out = "Enable"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpParentalControlFlag(u32 flag) +{ + char *out = NULL; + + switch(flag) + { + case 0: + out = "None"; + break; + case 1: + out = "FreeCommunication"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpScreenshot(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "Allow"; + break; + case 1: + out = "Deny"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpVideoCapture(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "Disable"; + break; + case 1: + out = "Manual"; + break; + case 2: + out = "Enable"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpRatingAgeOrganizationName(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "CERO"; + break; + case 1: + out = "GRACGCRB"; + break; + case 2: + out = "GSRMR"; + break; + case 3: + out = "ESRB"; + break; + case 4: + out = "ClassInd"; + break; + case 5: + out = "USK"; + break; + case 6: + out = "PEGI"; + break; + case 7: + out = "PEGIPortugal"; + break; + case 8: + out = "PEGIBBFC"; + break; + case 9: + out = "Russian"; + break; + case 10: + out = "ACB"; + break; + case 11: + out = "OFLC"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpDataLossConfirmation(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "None"; + break; + case 1: + out = "Required"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpPlayLogPolicy(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "All"; + break; + case 1: + out = "LogOnly"; + break; + case 2: + out = "None"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpLogoType(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "LicensedByNintendo"; + break; + case 2: + out = "Nintendo"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpLogoHandling(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "Auto"; + break; + case 1: + out = "Manual"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpStartupUserAccountOptionFlag(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "None"; + break; + case 1: + out = "IsOptional"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpAddOnContentRegistrationType(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "AllOnLaunch"; + break; + case 1: + out = "OnDemand"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpHdcp(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "None"; + break; + case 1: + out = "Required"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpCrashReport(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "Deny"; + break; + case 1: + out = "Allow"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpRuntimeAddOnContentInstall(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "Deny"; + break; + case 1: + out = "AllowAppend"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpPlayLogQueryCapability(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "None"; + break; + case 1: + out = "WhiteList"; + break; + case 2: + out = "All"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpRepairFlag(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "None"; + break; + case 1: + out = "SuppressGameCardAccess"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpAttributeFlag(u32 flag) +{ + char *out = NULL; + + switch(flag) + { + case 0: + out = "None"; + break; + case 1: + out = "Demo"; + break; + case 2: + out = "RetailInteractiveDisplay"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +char *getNacpRequiredNetworkServiceLicenseOnLaunchFlag(u8 val) +{ + char *out = NULL; + + switch(val) + { + case 0: + out = "None"; + break; + case 1: + out = "Common"; + break; + default: + out = "Unknown"; + break; + } + + return out; +} + +bool generateNacpXmlFromNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, nca_header_t *dec_nca_header, u8 *decrypted_nca_keys, char **outBuf) +{ + if (!ncmStorage || !ncaId || !dec_nca_header || !decrypted_nca_keys || !outBuf) + { + uiDrawString("Error: invalid parameters to generate NACP XML!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + return false; + } + + u64 entryOffset = 0; + romfs_file *entry = NULL; + bool found_nacp = false, success = false; + + nacp_t controlNacp; + char *nacpXml = NULL; + + u8 i; + char tmp[NAME_BUF_LEN] = {'\0'}; + + initRomFsContext(); + + if (!readRomFsEntriesFromNca(ncmStorage, ncaId, dec_nca_header, decrypted_nca_keys)) return false; + + // Look for the control.nacp file + while(entryOffset < romFsContext.romfs_filetable_size) + { + entry = (romfs_file*)((u8*)romFsContext.romfs_file_entries + entryOffset); + + if (entry->parent == 0 && entry->nameLen == 12 && !strncasecmp((char*)entry->name, "control.nacp", 12)) + { + found_nacp = true; + break; + } + + entryOffset += round_up(ROMFS_NONAME_FILEENTRY_SIZE + entry->nameLen, 4); + } + + if (!found_nacp) + { + uiDrawString("Error: unable to find control.nacp file in Control NCA RomFS section!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + + if (!processNcaCtrSectionBlock(ncmStorage, ncaId, romFsContext.romfs_filedata_offset + entry->dataOff, &controlNacp, sizeof(nacp_t), &(romFsContext.aes_ctx), false)) + { + breaks++; + uiDrawString("Failed to read Control.nacp from RomFS section in Control NCA!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + + // Make sure that the output buffer for our NACP XML is big enough + nacpXml = calloc(NAME_BUF_LEN * 4, sizeof(char)); + if (!nacpXml) + { + uiDrawString("Error: unable to allocate memory for the NACP XML!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + + sprintf(nacpXml, "\xEF\xBB\xBF\r\n" \ + "\r\n"); + + for(i = 0; i < 16; i++) + { + if (strlen(controlNacp.lang[i].name) || strlen(controlNacp.lang[i].author)) + { + sprintf(tmp, " \r\n" \ + " <Language>%s</Language>\r\n" \ + " <Name>%s</Name>\r\n" \ + " <Publisher>%s</Publisher>\r\n" \ + " \r\n", \ + getNacpLangName(i), \ + controlNacp.lang[i].name, \ + controlNacp.lang[i].author); + + strcat(nacpXml, tmp); + } + } + + if (strlen(controlNacp.Isbn)) + { + sprintf(tmp, " %s\r\n", controlNacp.Isbn); + strcat(nacpXml, tmp); + } else { + strcat(nacpXml, " \r\n"); + } + + sprintf(tmp, " %s\r\n", getNacpStartupUserAccount(controlNacp.StartupUserAccount)); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", getNacpUserAccountSwitchLock(controlNacp.UserAccountSwitchLock)); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", getNacpParentalControlFlag(controlNacp.ParentalControlFlag)); + strcat(nacpXml, tmp); + + for(i = 0; i < 16; i++) + { + u8 bit = (u8)((controlNacp.SupportedLanguageFlag >> i) & 0x1); + if (bit) + { + sprintf(tmp, " %s\r\n", getNacpLangName(i)); + strcat(nacpXml, tmp); + } + } + + sprintf(tmp, " %s\r\n", getNacpScreenshot(controlNacp.Screenshot)); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", getNacpVideoCapture(controlNacp.VideoCapture)); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.PresenceGroupId); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", controlNacp.DisplayVersion); + strcat(nacpXml, tmp); + + for(i = 0; i < 32; i++) + { + if (controlNacp.RatingAge[i] != 0xFF) + { + sprintf(tmp, " \r\n" \ + " %s\r\n" \ + " %u\r\n" \ + " \r\n", \ + getNacpRatingAgeOrganizationName(i), \ + controlNacp.RatingAge[i]); + + strcat(nacpXml, tmp); + } + } + + sprintf(tmp, " %s\r\n", getNacpDataLossConfirmation(controlNacp.DataLossConfirmation)); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", getNacpPlayLogPolicy(controlNacp.PlayLogPolicy)); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.SaveDataOwnerId); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.UserAccountSaveDataSize); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.UserAccountSaveDataJournalSize); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.DeviceSaveDataSize); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.DeviceSaveDataJournalSize); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.BcatDeliveryCacheStorageSize); + strcat(nacpXml, tmp); + + if (strlen(controlNacp.ApplicationErrorCodeCategory)) + { + sprintf(tmp, " %s\r\n", controlNacp.ApplicationErrorCodeCategory); + strcat(nacpXml, tmp); + } else { + strcat(nacpXml, " \r\n"); + } + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.AddOnContentBaseId); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", getNacpLogoType(controlNacp.LogoType)); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.LocalCommunicationId[0]); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", getNacpLogoHandling(controlNacp.LogoHandling)); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.SeedForPseudoDeviceId); + strcat(nacpXml, tmp); + + if (strlen(controlNacp.BcatPassphrase)) + { + sprintf(tmp, " %s\r\n", controlNacp.BcatPassphrase); + strcat(nacpXml, tmp); + } else { + strcat(nacpXml, " \r\n"); + } + + sprintf(tmp, " %s\r\n", getNacpStartupUserAccountOptionFlag(controlNacp.StartupUserAccountOptionFlag)); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", getNacpAddOnContentRegistrationType(controlNacp.AddOnContentRegistrationType)); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.UserAccountSaveDataSizeMax); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.UserAccountSaveDataJournalSizeMax); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.DeviceSaveDataSizeMax); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.DeviceSaveDataJournalSizeMax); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.TemporaryStorageSize); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.CacheStorageSize); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.CacheStorageJournalSize); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.CacheStorageDataAndJournalSizeMax); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%04x\r\n", controlNacp.CacheStorageIndexMax); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", getNacpHdcp(controlNacp.Hdcp)); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", getNacpCrashReport(controlNacp.CrashReport)); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", getNacpRuntimeAddOnContentInstall(controlNacp.RuntimeAddOnContentInstall)); + strcat(nacpXml, tmp); + + sprintf(tmp, " 0x%016lx\r\n", controlNacp.PlayLogQueryableApplicationId[0]); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", getNacpPlayLogQueryCapability(controlNacp.PlayLogQueryCapability)); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", getNacpRepairFlag(controlNacp.RepairFlag)); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", getNacpAttributeFlag(controlNacp.AttributeFlag)); + strcat(nacpXml, tmp); + + sprintf(tmp, " %u\r\n", controlNacp.ProgramIndex); + strcat(nacpXml, tmp); + + sprintf(tmp, " %s\r\n", getNacpRequiredNetworkServiceLicenseOnLaunchFlag(controlNacp.RequiredNetworkServiceLicenseOnLaunchFlag)); + strcat(nacpXml, tmp); + + strcat(nacpXml, "\r\n"); + + *outBuf = nacpXml; + + success = true; + +out: + // Manually free these pointers + // Calling freeRomFsContext() would also close the ncmStorage handle + free(romFsContext.romfs_dir_entries); + romFsContext.romfs_dir_entries = NULL; + + free(romFsContext.romfs_file_entries); + romFsContext.romfs_file_entries = NULL; + + return success; +} diff --git a/source/nca.h b/source/nca.h new file mode 100644 index 0000000..49869af --- /dev/null +++ b/source/nca.h @@ -0,0 +1,362 @@ +#pragma once + +#ifndef __NCA_H__ +#define __NCA_H__ + +#include + +#define NCA3_MAGIC (u32)0x4E434133 // "NCA3" +#define NCA2_MAGIC (u32)0x4E434132 // "NCA2" + +#define NCA_HEADER_LENGTH 0x400 +#define NCA_SECTION_HEADER_LENGTH 0x200 +#define NCA_SECTION_HEADER_CNT 4 +#define NCA_FULL_HEADER_LENGTH (NCA_HEADER_LENGTH + (NCA_SECTION_HEADER_LENGTH * NCA_SECTION_HEADER_CNT)) + +#define NCA_CONTENT_TYPE_DELTA 0x06 + +#define NCA_AES_XTS_SECTOR_SIZE 0x200 + +#define NCA_KEY_AREA_KEY_CNT 4 +#define NCA_KEY_AREA_KEY_SIZE 0x10 +#define NCA_KEY_AREA_SIZE (NCA_KEY_AREA_KEY_CNT * NCA_KEY_AREA_KEY_SIZE) + +#define NCA_FS_HEADER_PARTITION_PFS0 0x01 +#define NCA_FS_HEADER_FSTYPE_PFS0 0x02 + +#define NCA_FS_HEADER_PARTITION_ROMFS 0x00 +#define NCA_FS_HEADER_FSTYPE_ROMFS 0x03 + +#define NCA_FS_HEADER_CRYPT_NONE 0x01 +#define NCA_FS_HEADER_CRYPT_XTS 0x02 +#define NCA_FS_HEADER_CRYPT_CTR 0x03 +#define NCA_FS_HEADER_CRYPT_BKTR 0x04 + +#define PFS0_MAGIC (u32)0x50465330 // "PFS0" + +#define IVFC_MAGIC (u32)0x49564643 // "IVFC" +#define IVFC_MAX_LEVEL 6 + +#define ROMFS_HEADER_SIZE 0x50 +#define ROMFS_ENTRY_EMPTY (u32)0xFFFFFFFF + +#define ROMFS_NONAME_DIRENTRY_SIZE 0x18 +#define ROMFS_NONAME_FILEENTRY_SIZE 0x20 + +#define ROMFS_ENTRY_DIR 1 +#define ROMFS_ENTRY_FILE 2 + +#define META_MAGIC (u32)0x4D455441 // "META" + +#define NPDM_SIGNATURE_SIZE 0x100 +#define NPDM_SIGNATURE_AREA_SIZE 0x200 + +#define NSP_NCA_FILENAME_LENGTH 0x25 // NCA ID + ".nca" + NULL terminator +#define NSP_CNMT_FILENAME_LENGTH 0x2A // NCA ID + ".cnmt.nca" / ".cnmt.xml" + NULL terminator +#define NSP_NACP_FILENAME_LENGTH 0x2A // NCA ID + ".nacp.xml" + NULL terminator +#define NSP_TIK_FILENAME_LENGTH 0x25 // Rights ID + ".tik" + NULL terminator +#define NSP_CERT_FILENAME_LENGTH 0x26 // Rights ID + ".cert" + NULL terminator + +typedef enum { + DUMP_APP_NSP = 0, + DUMP_PATCH_NSP, + DUMP_ADDON_NSP +} nspDumpType; + +typedef struct { + u32 magic; + u32 file_cnt; + u32 str_table_size; + u32 reserved; +} PACKED pfs0_header; + +typedef struct { + u64 file_offset; + u64 file_size; + u32 filename_offset; + u32 reserved; +} PACKED pfs0_entry_table; + +typedef struct { + u32 media_start_offset; + u32 media_end_offset; + u8 _0x8[0x8]; /* Padding. */ +} PACKED nca_section_entry_t; + +typedef struct { + u8 master_hash[0x20]; /* SHA-256 hash of the hash table. */ + u32 block_size; /* In bytes. */ + u32 always_2; + u64 hash_table_offset; /* Normally zero. */ + u64 hash_table_size; + u64 pfs0_offset; + u64 pfs0_size; + u8 _0x48[0xF0]; +} PACKED pfs0_superblock_t; + +typedef struct { + u64 logical_offset; + u64 hash_data_size; + u32 block_size; + u32 reserved; +} PACKED ivfc_level_hdr_t; + +typedef struct { + u32 magic; + u32 id; + u32 master_hash_size; + u32 num_levels; + ivfc_level_hdr_t level_headers[IVFC_MAX_LEVEL]; + u8 _0xA0[0x20]; + u8 master_hash[0x20]; +} PACKED ivfc_hdr_t; + +typedef struct { + ivfc_hdr_t ivfc_header; + u8 _0xE0[0x58]; +} PACKED romfs_superblock_t; + +/* NCA FS header. */ +typedef struct { + u8 _0x0; + u8 _0x1; + u8 partition_type; + u8 fs_type; + u8 crypt_type; + u8 _0x5[0x3]; + union { /* FS-specific superblock. Size = 0x138. */ + pfs0_superblock_t pfs0_superblock; + romfs_superblock_t romfs_superblock; + }; + union { + u8 section_ctr[0x8]; + struct { + u32 section_ctr_low; + u32 section_ctr_high; + }; + }; + u8 _0x148[0xB8]; /* Padding. */ +} PACKED nca_fs_header_t; + +/* Nintendo content archive header. */ +typedef struct { + u8 fixed_key_sig[0x100]; /* RSA-PSS signature over header with fixed key. */ + u8 npdm_key_sig[0x100]; /* RSA-PSS signature over header with key in NPDM. */ + u32 magic; + u8 distribution; /* System vs gamecard. */ + u8 content_type; + u8 crypto_type; /* Which keyblob (field 1) */ + u8 kaek_ind; /* Which kaek index? */ + u64 nca_size; /* Entire archive size. */ + u64 title_id; + u8 _0x218[0x4]; /* Padding. */ + union { + u32 sdk_version; /* What SDK was this built with? */ + struct { + u8 sdk_revision; + u8 sdk_micro; + u8 sdk_minor; + u8 sdk_major; + }; + }; + u8 crypto_type2; /* Which keyblob (field 2) */ + u8 _0x221[0xF]; /* Padding. */ + u8 rights_id[0x10]; /* Rights ID (for titlekey crypto). */ + nca_section_entry_t section_entries[4]; /* Section entry metadata. */ + u8 section_hashes[4][0x20]; /* SHA-256 hashes for each section header. */ + u8 nca_keys[4][0x10]; /* Key area (encrypted) */ + u8 _0x340[0xC0]; /* Padding. */ + nca_fs_header_t fs_headers[4]; /* FS section headers. */ +} PACKED nca_header_t; + +typedef struct { + u32 magic; + u32 _0x4; + u32 _0x8; + u8 mmu_flags; + u8 _0xD; + u8 main_thread_prio; + u8 default_cpuid; + u64 _0x10; + u32 process_category; + u32 main_stack_size; + char title_name[0x50]; + u32 aci0_offset; + u32 aci0_size; + u32 acid_offset; + u32 acid_size; +} PACKED npdm_t; + +typedef struct { + u64 title_id; + u32 version; + u8 type; + u8 unk1; + u16 table_offset; + u16 content_records_cnt; + u16 meta_records_cnt; + u8 unk2[12]; +} PACKED cnmt_header; + +typedef struct { + u64 patch_tid; /* Patch TID / Original TID / Application TID */ + u32 min_sysver; /* Minimum system/application version */ +} PACKED cnmt_extended_header; + +typedef struct { + u8 hash[0x20]; + u8 nca_id[0x10]; + u8 size[6]; + u8 type; + u8 unk; +} PACKED cnmt_content_record; + +typedef struct { + u8 type; + u64 title_id; + u32 version; + u32 required_dl_sysver; + u32 nca_cnt; + u8 digest[32]; + char digest_str[65]; + u8 min_keyblob; + u32 min_sysver; + u64 patch_tid; +} PACKED cnmt_xml_program_info; + +typedef struct { + u8 type; + u8 nca_id[16]; + char nca_id_str[33]; + u64 size; + u8 hash[32]; + char hash_str[65]; + u8 keyblob; + u8 decrypted_nca_keys[NCA_KEY_AREA_SIZE]; + u8 encrypted_header_mod[NCA_FULL_HEADER_LENGTH]; +} PACKED cnmt_xml_content_info; + +typedef struct { + u8 *hash_table; + u64 hash_table_offset; // Relative to NCA start + u64 hash_table_size; + u8 block_mod_cnt; + u8 *block_data[2]; + u64 block_offset[2]; // Relative to NCA start + u64 block_size[2]; +} PACKED nca_program_mod_data; + +typedef struct { + u64 section_offset; // Relative to NCA start + u64 section_size; + u64 hash_table_offset; // Relative to NCA start + u64 pfs0_offset; // Relative to NCA start + u64 pfs0_size; + u64 title_cnmt_offset; // Relative to NCA start + u64 title_cnmt_size; +} PACKED nca_cnmt_mod_data; + +typedef struct { + NacpLanguageEntry lang[16]; + char Isbn[0x25]; + u8 StartupUserAccount; + u8 UserAccountSwitchLock; + u8 AddOnContentRegistrationType; + u32 AttributeFlag; + u32 SupportedLanguageFlag; + u32 ParentalControlFlag; + u8 Screenshot; + u8 VideoCapture; + u8 DataLossConfirmation; + u8 PlayLogPolicy; + u64 PresenceGroupId; + u8 RatingAge[0x20]; + char DisplayVersion[0x10]; + u64 AddOnContentBaseId; + u64 SaveDataOwnerId; + u64 UserAccountSaveDataSize; + u64 UserAccountSaveDataJournalSize; + u64 DeviceSaveDataSize; + u64 DeviceSaveDataJournalSize; + u64 BcatDeliveryCacheStorageSize; + char ApplicationErrorCodeCategory[0x8]; + u64 LocalCommunicationId[0x8]; + u8 LogoType; + u8 LogoHandling; + u8 RuntimeAddOnContentInstall; + u8 _0x30F3[0x3]; + u8 CrashReport; + u8 Hdcp; + u64 SeedForPseudoDeviceId; + char BcatPassphrase[0x41]; + u8 StartupUserAccountOptionFlag; + u8 ReservedForUserAccountSaveDataOperation[0x6]; + u64 UserAccountSaveDataSizeMax; + u64 UserAccountSaveDataJournalSizeMax; + u64 DeviceSaveDataSizeMax; + u64 DeviceSaveDataJournalSizeMax; + u64 TemporaryStorageSize; + u64 CacheStorageSize; + u64 CacheStorageJournalSize; + u64 CacheStorageDataAndJournalSizeMax; + u16 CacheStorageIndexMax; + u8 reserved_0x318a[0x6]; + u64 PlayLogQueryableApplicationId[0x10]; + u8 PlayLogQueryCapability; + u8 RepairFlag; + u8 ProgramIndex; + u8 RequiredNetworkServiceLicenseOnLaunchFlag; + u8 Reserved[0xDEC]; +} PACKED nacp_t; + +typedef struct { + bool has_rights_id; + u8 rights_id[0x10]; + char rights_id_str[33]; + char tik_filename[37]; + char cert_filename[38]; +} PACKED title_rights_ctx; + +typedef struct { + NcmContentStorage ncmStorage; + NcmNcaId ncaId; + Aes128CtrContext aes_ctx; + u64 romfs_offset; // Relative to NCA start + u64 romfs_size; + u64 romfs_dirtable_offset; // Relative to NCA start + u64 romfs_dirtable_size; + romfs_dir *romfs_dir_entries; + u64 romfs_filetable_offset; // Relative to NCA start + u64 romfs_filetable_size; + romfs_file *romfs_file_entries; + u64 romfs_filedata_offset; // Relative to NCA start +} PACKED romfs_ctx_t; + +typedef struct { + u8 type; // 1 = Dir, 2 = File + u64 offset; // Relative to directory/file table, depending on type +} PACKED romfs_browser_entry; + +void generateCnmtXml(cnmt_xml_program_info *xml_program_info, cnmt_xml_content_info *xml_content_info, char *out); + +void convertNcaSizeToU64(const u8 size[0x6], u64 *out); + +void convertU64ToNcaSize(const u64 size, u8 out[0x6]); + +bool processNcaCtrSectionBlock(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, u64 offset, void *outBuf, size_t bufSize, Aes128CtrContext *ctx, bool encrypt); + +bool encryptNcaHeader(nca_header_t *input, u8 *outBuf, u64 outBufSize); + +bool decryptNcaHeader(const u8 *ncaBuf, u64 ncaBufSize, nca_header_t *out, title_rights_ctx *rights_info, u8 *decrypted_nca_keys); + +bool processProgramNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, nca_header_t *dec_nca_header, cnmt_xml_content_info *xml_content_info, nca_program_mod_data *output); + +bool retrieveCnmtNcaData(nspDumpType selectedNspDumpType, u8 *ncaBuf, cnmt_xml_program_info *xml_program_info, cnmt_xml_content_info *xml_content_info, nca_cnmt_mod_data *output, title_rights_ctx *rights_info); + +bool patchCnmtNca(u8 *ncaBuf, u64 ncaBufSize, cnmt_xml_program_info *xml_program_info, cnmt_xml_content_info *xml_content_info, nca_cnmt_mod_data *cnmt_mod); + +bool readRomFsEntriesFromNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, nca_header_t *dec_nca_header, u8 *decrypted_nca_keys); + +bool generateNacpXmlFromNca(NcmContentStorage *ncmStorage, const NcmNcaId *ncaId, nca_header_t *dec_nca_header, u8 *decrypted_nca_keys, char **outBuf); + +#endif diff --git a/source/rsa.c b/source/rsa.c new file mode 100644 index 0000000..01074fb --- /dev/null +++ b/source/rsa.c @@ -0,0 +1,83 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "rsa.h" +#include "rsa_keys.h" +#include "ui.h" +#include "util.h" + +/* Extern variables */ + +extern int breaks; +extern int font_height; + +extern char strbuf[NAME_BUF_LEN * 4]; + +bool rsa_sign(void* input, size_t input_size, unsigned char* output, size_t output_size) +{ + unsigned char hash[32]; + unsigned char buf[MBEDTLS_MPI_MAX_SIZE]; + const char *pers = "rsa_sign_pss"; + size_t olen = 0; + + int ret; + bool success = false; + + mbedtls_pk_context pk; + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + + mbedtls_entropy_init(&entropy); + mbedtls_pk_init(&pk); + mbedtls_ctr_drbg_init(&ctr_drbg); + + // Calculate SHA-256 checksum for the input data + sha256CalculateHash(hash, input, input_size); + + // Seed the random number generator + ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *)pers, strlen(pers)); + if (ret == 0) + { + // Parse private key + ret = mbedtls_pk_parse_key(&pk, (unsigned char*)rsa_private_key, strlen(rsa_private_key) + 1, NULL, 0); + if (ret == 0) + { + // Set RSA padding + mbedtls_rsa_set_padding(mbedtls_pk_rsa(pk), MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256); + + // Calculate hash signature + ret = mbedtls_pk_sign(&pk, MBEDTLS_MD_SHA256, hash, 0, buf, &olen, mbedtls_ctr_drbg_random, &ctr_drbg); + if (ret == 0) + { + // Copy signature to output + memcpy(output, buf, output_size); + success = true; + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "rsa_sign: mbedtls_pk_sign failed! (%d)", ret); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "rsa_sign: mbedtls_pk_parse_key failed! (%d)", ret); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "rsa_sign: mbedtls_ctr_drbg_seed failed! (%d)", ret); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } + + mbedtls_ctr_drbg_free(&ctr_drbg); + mbedtls_pk_free(&pk); + mbedtls_entropy_free(&entropy); + + return success; +} + +const unsigned char *rsa_get_public_key() +{ + return rsa_public_key; +} diff --git a/source/rsa.h b/source/rsa.h new file mode 100644 index 0000000..a30c92f --- /dev/null +++ b/source/rsa.h @@ -0,0 +1,11 @@ +#pragma once + +#ifndef __RSA_H__ +#define __RSA_H__ + +#include + +bool rsa_sign(void* input, size_t input_size, unsigned char* output, size_t output_size); +const unsigned char *rsa_get_public_key(); + +#endif diff --git a/source/rsa_keys.h b/source/rsa_keys.h new file mode 100644 index 0000000..0c1a3a1 --- /dev/null +++ b/source/rsa_keys.h @@ -0,0 +1,55 @@ +#pragma once + +#ifndef __RSA_KEYS_H__ +#define __RSA_KEYS_H__ + +// Self-generated private key +const char rsa_private_key[] = "-----BEGIN RSA PRIVATE KEY-----\r\n" +"MIIEowIBAAKCAQEAvVRzt+8mE7oE4RkmSh3ws4CGlBj7uhHkfwCpPFsn4TNVdLRo\r\n" +"YYY17jQYWTtcOYPMcHxwUpgJyspGN8QGXEkJqY8jILv2eO0jBGtg7Br2afUBp6/x\r\n" +"BOMT2RlYVX6H4a1UA19Hzmcn+T1hdDwS6oBYpi8rJSm0+q+yB34dueNkVsk4eKbj\r\n" +"CNNKFi+XgyNBi41d57SPCrkcm/9tkagRorE8vLcFPcXcYOjdXH3L4XTXq7sxxytA\r\n" +"I66erfSc4XunkoLifcbfMOB3gjGCoQs6GfaiAU3TwxewQ7hdoqvj5Gm9VyHqzeDF\r\n" +"5mUTlmed2I6m4ELxbV1b0lUguR5ZEzwXwiVWxwIDAQABAoIBADvLYkijFOmCBGx7\r\n" +"HualkhF+9AHt6gKYCAw8Tzaqq2uqZMDZAWZblsjGVzJHVxcrEvQruOW88srDG24d\r\n" +"UMzwnEaa2ENMWclTS43nw9KNqWlJYd5t6LbcaLZWFNnbflq9/RybiPgdCDjlM9Qb\r\n" +"7PV214iUuRGhnHDX8GgBYq4ErPnjQ7+Gv1ducpMYjZencLWCl4fFX86U0/MU0+Qf\r\n" +"jKGegQTnk52aaeScbDOjjx5h+m0hkDNSfsmXTlvJt2c8wy/Yx+leVgCPjMC1nbft\r\n" +"Ob1TlpjuEAKBOGt4+DkWwVmIlxilmx9wCTZnwvPKd7A0e0FGsdHnQienPrMqlgbl\r\n" +"JPYwJuECgYEA6yLZHTfX3ebpzcdQQqmuHZtbOcs+EGRy24gAzd+9vCGKf0VtKSl9\r\n" +"3oA3XBOe2C2TgSgbWFZ7v/2efWRjgwJta0BQlpkzkh6NUQa2LI2M3zgZwHCZ7Ihr\r\n" +"skG73qZsMHOOv7VQz/wDp6AZNasfz21Mcyh4uFzpkb3NKLXqsJ9LeG8CgYEAziEb\r\n" +"yBCuhCKq7YZt/cHlbCbi7HbCYbub0isOCUtV0qPsX+kVZdPS+oGLPq1905JKdAe9\r\n" +"O+4SltCw6qn9RgYnCCVQ47SGHg7KO8Z5vdcNUiDvsQ+jNFlmM5QBuf1UV/Y+DV/Q\r\n" +"fZdA06OeYxkfPuBMtjdS9qMKwm3OsCkiQasWQykCgYAqALieAoq6JfSgALmyntLu\r\n" +"kQDzyv2UOg1Wb+4M2KnxAGDYKVO9pZ7Jb0f0V8DpRwLxcHOqDRDgE/MK3TL1hSp8\r\n" +"nSmILWfL8081KSjDvqlqeoAHI1YrrZbnadyggkQTR6E5V69O5+rTN8MpFh+Bkzmz\r\n" +"3IfsDxTeJvSOECkTUfFOWwKBgQDG/id3yMLxRRaGH5TnuNvmwNOpPC0DdL5E8tOm\r\n" +"HVhI9X8oSDgkCY5Pz+fBJnOmYEAIK8B/rqG7ftSMdnbPtvjPYFbqvEgNlHGfq0e0\r\n" +"AXwWoT1ETbhcvUFw4Z2ZE/rswAe/mZQI6o/mwLoTKRmE9byY3Gf3OgcVFDTI060C\r\n" +"gEwJoQKBgHpOmtGum3JuLpPc+PTXZOe29tdWndkFWktjPoow60d+NO2jpTFuEpmW\r\n" +"XRW35vXI8PqMCmHOQ8YU59aMN9juAnsJmPUxbAW5fZfvVwWUo0cTOenfT6syrEYO\r\n" +"n5NEG+mY4WZaOFRNiZu8+4aJI1yycXMyA22iKcU8+nN/sMAJs3Nx\r\n" +"-----END RSA PRIVATE KEY-----\r\n"; + +// Self-generated public key +const unsigned char rsa_public_key[] = { + 0xbd, 0x54, 0x73, 0xb7, 0xef, 0x26, 0x13, 0xba, 0x04, 0xe1, 0x19, 0x26, 0x4a, 0x1d, 0xf0, 0xb3, + 0x80, 0x86, 0x94, 0x18, 0xfb, 0xba, 0x11, 0xe4, 0x7f, 0x00, 0xa9, 0x3c, 0x5b, 0x27, 0xe1, 0x33, + 0x55, 0x74, 0xb4, 0x68, 0x61, 0x86, 0x35, 0xee, 0x34, 0x18, 0x59, 0x3b, 0x5c, 0x39, 0x83, 0xcc, + 0x70, 0x7c, 0x70, 0x52, 0x98, 0x09, 0xca, 0xca, 0x46, 0x37, 0xc4, 0x06, 0x5c, 0x49, 0x09, 0xa9, + 0x8f, 0x23, 0x20, 0xbb, 0xf6, 0x78, 0xed, 0x23, 0x04, 0x6b, 0x60, 0xec, 0x1a, 0xf6, 0x69, 0xf5, + 0x01, 0xa7, 0xaf, 0xf1, 0x04, 0xe3, 0x13, 0xd9, 0x19, 0x58, 0x55, 0x7e, 0x87, 0xe1, 0xad, 0x54, + 0x03, 0x5f, 0x47, 0xce, 0x67, 0x27, 0xf9, 0x3d, 0x61, 0x74, 0x3c, 0x12, 0xea, 0x80, 0x58, 0xa6, + 0x2f, 0x2b, 0x25, 0x29, 0xb4, 0xfa, 0xaf, 0xb2, 0x07, 0x7e, 0x1d, 0xb9, 0xe3, 0x64, 0x56, 0xc9, + 0x38, 0x78, 0xa6, 0xe3, 0x08, 0xd3, 0x4a, 0x16, 0x2f, 0x97, 0x83, 0x23, 0x41, 0x8b, 0x8d, 0x5d, + 0xe7, 0xb4, 0x8f, 0x0a, 0xb9, 0x1c, 0x9b, 0xff, 0x6d, 0x91, 0xa8, 0x11, 0xa2, 0xb1, 0x3c, 0xbc, + 0xb7, 0x05, 0x3d, 0xc5, 0xdc, 0x60, 0xe8, 0xdd, 0x5c, 0x7d, 0xcb, 0xe1, 0x74, 0xd7, 0xab, 0xbb, + 0x31, 0xc7, 0x2b, 0x40, 0x23, 0xae, 0x9e, 0xad, 0xf4, 0x9c, 0xe1, 0x7b, 0xa7, 0x92, 0x82, 0xe2, + 0x7d, 0xc6, 0xdf, 0x30, 0xe0, 0x77, 0x82, 0x31, 0x82, 0xa1, 0x0b, 0x3a, 0x19, 0xf6, 0xa2, 0x01, + 0x4d, 0xd3, 0xc3, 0x17, 0xb0, 0x43, 0xb8, 0x5d, 0xa2, 0xab, 0xe3, 0xe4, 0x69, 0xbd, 0x57, 0x21, + 0xea, 0xcd, 0xe0, 0xc5, 0xe6, 0x65, 0x13, 0x96, 0x67, 0x9d, 0xd8, 0x8e, 0xa6, 0xe0, 0x42, 0xf1, + 0x6d, 0x5d, 0x5b, 0xd2, 0x55, 0x20, 0xb9, 0x1e, 0x59, 0x13, 0x3c, 0x17, 0xc2, 0x25, 0x56, 0xc7 +}; + +#endif diff --git a/source/ui.c b/source/ui.c index d9ba198..ca0c7bf 100644 --- a/source/ui.c +++ b/source/ui.c @@ -4,14 +4,17 @@ #include #include #include +#include #include #include #include FT_FREETYPE_H +#include + #include "dumper.h" -#include "fsext.h" +#include "fs_ext.h" #include "ui.h" #include "util.h" @@ -25,20 +28,30 @@ extern bool gameCardInserted; extern char gameCardSizeStr[32], trimmedCardSizeStr[32]; -extern char *hfs0_header; +extern u8 *hfs0_header; extern u64 hfs0_offset, hfs0_size; extern u32 hfs0_partition_cnt; -extern char *partitionHfs0Header; +extern u8 *partitionHfs0Header; extern u64 partitionHfs0HeaderOffset, partitionHfs0HeaderSize; extern u32 partitionHfs0FileCount, partitionHfs0StrTableSize; extern u32 gameCardAppCount; extern u64 *gameCardTitleID; +extern u32 *gameCardVersion; + +extern u32 gameCardPatchCount; +extern u64 *gameCardPatchTitleID; +extern u32 *gameCardPatchVersion; + +extern u32 gameCardAddOnCount; +extern u64 *gameCardAddOnTitleID; +extern u32 *gameCardAddOnVersion; extern char **gameCardName; extern char **gameCardAuthor; extern char **gameCardVersionStr; +extern u8 **gameCardIcon; extern char gameCardUpdateVersionStr[128]; @@ -46,13 +59,18 @@ extern char *filenameBuffer; extern char *filenames[FILENAME_MAX_CNT]; extern int filenamesCount; +extern char curRomFsPath[NAME_BUF_LEN]; +extern romfs_browser_entry *romFsBrowserEntries; + extern char strbuf[NAME_BUF_LEN * 4]; /* Statically allocated variables */ -static PlFontData font; +static PlFontData standardFont; +static PlFontData nintendoExtFont; static FT_Library library; -static FT_Face face; +static FT_Face standardFontFace; +static FT_Face nintendoExtFontFace; static Framebuffer fb; static u32 *framebuf = NULL; @@ -63,13 +81,18 @@ int scroll = 0; int breaks = 0; int font_height = 0; +static u32 selectedAppInfoIndex = 0; +static u32 selectedAppIndex; +static u32 selectedPatchIndex; +static u32 selectedAddOnIndex; static u32 selectedPartitionIndex; static u32 selectedFileIndex; -static u32 selectedAppIndex; + +static nspDumpType selectedNspDumpType; static bool highlight = false; -static bool isFat32 = false, dumpCert = false, trimDump = false, calcCrc = true; +static bool isFat32 = false, dumpCert = false, trimDump = false, calcCrc = true, setXciArchiveBit = false; static char statusMessage[2048] = {'\0'}; static int statusMessageFadeout = 0; @@ -77,20 +100,40 @@ static int statusMessageFadeout = 0; u64 freeSpace = 0; static char freeSpaceStr[64] = {'\0'}; -static const int maxListElements = 15; - static UIState uiState; -static const char *appHeadline = "Nintendo Switch Game Card Dump Tool v" APP_VERSION ".\nOriginal codebase by MCMrARM.\nUpdated and maintained by DarkMatterCore.\n\n"; -static const char *appControls = "[D-Pad / Analog Sticks] Move | [A] Select | [B] Back | [+] Exit"; +static const char *dirNormalIconPath = "romfs:/dir_normal.jpg"; +static u8 *dirNormalIconBuf = NULL; -static const char *mainMenuItems[] = { "Cartridge Image (XCI) dump", "Nintendo Submission Package (NSP) dump", "Raw partition dump", "Partition data dump", "View game card files", "Dump game card certificate", "Update NSWDB.COM XML database", "Update application" }; -static const char *xciDumpMenuItems[] = { "Start XCI dump process", "Split output dump (FAT32 support): ", "Dump certificate: ", "Trim output dump: ", "CRC32 checksum calculation + dump verification: " }; -static const char *nspDumpMenuItems[] = { "Start NSP dump process", "Split output dump (FAT32 support): ", "CRC32 checksum calculation: ", "Bundled application to dump: " }; -static const char *partitionDumpType1MenuItems[] = { "Dump partition 0 (Update)", "Dump partition 1 (Normal)", "Dump partition 2 (Secure)" }; -static const char *partitionDumpType2MenuItems[] = { "Dump partition 0 (Update)", "Dump partition 1 (Logo)", "Dump partition 2 (Normal)", "Dump partition 3 (Secure)" }; -static const char *viewGameCardFsType1MenuItems[] = { "View files from partition 0 (Update)", "View files from partition 1 (Normal)", "View files from partition 2 (Secure)" }; -static const char *viewGameCardFsType2MenuItems[] = { "View files from partition 0 (Update)", "View files from partition 1 (Logo)", "View files from partition 2 (Normal)", "View files from partition 3 (Secure)" }; +static const char *dirHighlightIconPath = "romfs:/dir_highlight.jpg"; +static u8 *dirHighlightIconBuf = NULL; + +static const char *fileNormalIconPath = "romfs:/file_normal.jpg"; +static u8 *fileNormalIconBuf = NULL; + +static const char *fileHighlightIconPath = "romfs:/file_highlight.jpg"; +static u8 *fileHighlightIconBuf = NULL; + +static const char *appHeadline = "Nintendo Switch Game Card Dump Tool v" APP_VERSION ".\nOriginal codebase by MCMrARM.\nUpdated and maintained by DarkMatterCore.\n\n"; +static const char *appControlsNoGameCard = "[ " NINTENDO_FONT_PLUS " ] Exit"; +static const char *appControlsSingleApp = "[ " NINTENDO_FONT_DPAD " / " NINTENDO_FONT_LSTICK " / " NINTENDO_FONT_RSTICK " ] Move | [ " NINTENDO_FONT_A " ] Select | [ " NINTENDO_FONT_B " ] Back | [ " NINTENDO_FONT_PLUS " ] Exit"; +static const char *appControlsMultiApp = "[ " NINTENDO_FONT_DPAD " / " NINTENDO_FONT_LSTICK " / " NINTENDO_FONT_RSTICK " ] Move | [ " NINTENDO_FONT_A " ] Select | [ " NINTENDO_FONT_B " ] Back | [ " NINTENDO_FONT_L " / " NINTENDO_FONT_R " / " NINTENDO_FONT_ZL " / " NINTENDO_FONT_ZR " ] Change displayed base application info | [ " NINTENDO_FONT_PLUS " ] Exit"; + +static const char *mainMenuItems[] = { "Cartridge Image (XCI) dump", "Nintendo Submission Package (NSP) dump", "HFS0 options", "RomFS options", "Dump game card certificate", "Update options" }; +static const char *xciDumpMenuItems[] = { "Start XCI dump process", "Split output dump (FAT32 support): ", "Create directory with archive bit set: ", "Dump certificate: ", "Trim output dump: ", "CRC32 checksum calculation + dump verification: " }; +static const char *nspDumpMenuItems[] = { "Dump base application NSP", "Dump bundled update NSP", "Dump bundled DLC NSP" }; +static const char *nspAppDumpMenuItems[] = { "Start NSP dump process", "Split output dump (FAT32 support): ", "CRC32 checksum calculation: ", "Bundled application to dump: " }; +static const char *nspPatchDumpMenuItems[] = { "Start NSP dump process", "Split output dump (FAT32 support): ", "CRC32 checksum calculation: ", "Bundled update to dump: " }; +static const char *nspAddOnDumpMenuItems[] = { "Start NSP dump process", "Split output dump (FAT32 support): ", "CRC32 checksum calculation: ", "Bundled DLC to dump: " }; +static const char *hfs0MenuItems[] = { "Raw HFS0 partition dump", "HFS0 partition data dump", "Browse HFS0 partitions" }; +static const char *hfs0PartitionDumpType1MenuItems[] = { "Dump HFS0 partition 0 (Update)", "Dump HFS0 partition 1 (Normal)", "Dump HFS0 partition 2 (Secure)" }; +static const char *hfs0PartitionDumpType2MenuItems[] = { "Dump HFS0 partition 0 (Update)", "Dump HFS0 partition 1 (Logo)", "Dump HFS0 partition 2 (Normal)", "Dump HFS0 partition 3 (Secure)" }; +static const char *hfs0BrowserType1MenuItems[] = { "Browse HFS0 partition 0 (Update)", "Browse HFS0 partition 1 (Normal)", "Browse HFS0 partition 2 (Secure)" }; +static const char *hfs0BrowserType2MenuItems[] = { "Browse HFS0 partition 0 (Update)", "Browse HFS0 partition 1 (Logo)", "Browse HFS0 partition 2 (Normal)", "Browse HFS0 partition 3 (Secure)" }; +static const char *romFsMenuItems[] = { "RomFS section data dump", "Browse RomFS section" }; +static const char *romFsSectionDumpMenuItems[] = { "Start RomFS data dump process", "Bundled application to dump: " }; +static const char *romFsSectionBrowserMenuItems[] = { "Browse RomFS section", "Bundled application to browse: " }; +static const char *updateMenuItems[] = { "Update NSWDB.COM XML database", "Update application" }; void uiFill(int x, int y, int width, int height, u8 r, u8 g, u8 b) { @@ -108,9 +151,9 @@ void uiFill(int x, int y, int width, int height, u8 r, u8 g, u8 b) height += y; y = 0; } - + if ((x + width) >= FB_WIDTH) width = (FB_WIDTH - x); - + if ((y + height) >= FB_HEIGHT) height = (FB_HEIGHT - y); if (framebuf == NULL) @@ -136,6 +179,189 @@ void uiFill(int x, int y, int width, int height, u8 r, u8 g, u8 b) } } +void uiDrawIcon(const u8 *icon, int width, int height, int x, int y) +{ + /* Perform validity checks */ + if (!icon || !width || !height || (x + width) < 0 || (y + height) < 0 || x >= FB_WIDTH || y >= FB_HEIGHT) return; + + if (x < 0) + { + width += x; + x = 0; + } + + if (y < 0) + { + height += y; + y = 0; + } + + if ((x + width) >= FB_WIDTH) width = (FB_WIDTH - x); + + if ((y + height) >= FB_HEIGHT) height = (FB_HEIGHT - y); + + if (framebuf == NULL) + { + /* Begin new frame */ + u32 stride; + framebuf = (u32*)framebufferBegin(&fb, &stride); + framebuf_width = (stride / sizeof(u32)); + } + + u32 lx, ly; + u32 framex, framey; + u32 pos = 0; + + for (ly = 0; ly < height; ly++) + { + for (lx = 0; lx < width; lx++) + { + framex = (x + lx); + framey = (y + ly); + + pos = (((ly * width) + lx) * 3); + + framebuf[(framey * framebuf_width) + framex] = RGBA8_MAXALPHA(icon[pos], icon[pos + 1], icon[pos + 2]); + } + } +} + +bool uiLoadJpgFromMem(u8 *rawJpg, size_t rawJpgSize, int expectedWidth, int expectedHeight, int desiredWidth, int desiredHeight, u8 **outBuf) +{ + if (!rawJpg || !rawJpgSize || !expectedWidth || !expectedHeight || !desiredWidth || !desiredHeight || !outBuf) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "uiLoadJpgFromMem: invalid parameters to process JPG image buffer."); + return false; + } + + int ret, w, h, samp; + tjhandle _jpegDecompressor = NULL; + bool success = false; + + bool foundScalingFactor = false; + int i, numScalingFactors = 0, pitch; + tjscalingfactor *scalingFactors = NULL; + + u8 *jpgScaledBuf = NULL; + + _jpegDecompressor = tjInitDecompress(); + if (_jpegDecompressor) + { + ret = tjDecompressHeader2(_jpegDecompressor, rawJpg, rawJpgSize, &w, &h, &samp); + if (ret != -1) + { + if (w == expectedWidth && h == expectedHeight) + { + scalingFactors = tjGetScalingFactors(&numScalingFactors); + if (scalingFactors) + { + for(i = 0; i < numScalingFactors; i++) + { + if (TJSCALED(expectedWidth, scalingFactors[i]) == desiredWidth && TJSCALED(expectedHeight, scalingFactors[i]) == desiredHeight) + { + foundScalingFactor = true; + break; + } + } + + if (foundScalingFactor) + { + pitch = TJPAD(desiredWidth * tjPixelSize[TJPF_RGB]); + + jpgScaledBuf = malloc(pitch * desiredHeight); + if (jpgScaledBuf) + { + ret = tjDecompress2(_jpegDecompressor, rawJpg, rawJpgSize, jpgScaledBuf, desiredWidth, 0, desiredHeight, TJPF_RGB, TJFLAG_ACCURATEDCT); + if (ret != -1) + { + *outBuf = jpgScaledBuf; + success = true; + } else { + free(jpgScaledBuf); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "uiLoadJpgFromMem: tjDecompress2 failed (%d).", ret); + } + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "uiLoadJpgFromMem: unable to allocated memory for the scaled RGB image output."); + } + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "uiLoadJpgFromMem: unable to find a valid scaling factor."); + } + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "uiLoadJpgFromMem: error retrieving scaling factors."); + } + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "uiLoadJpgFromMem: invalid image width/height."); + } + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "uiLoadJpgFromMem: tjDecompressHeader2 failed (%d).", ret); + } + + tjDestroy(_jpegDecompressor); + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "uiLoadJpgFromMem: tjInitDecompress failed."); + } + + return success; +} + +bool uiLoadJpgFromFile(const char *filename, int expectedWidth, int expectedHeight, int desiredWidth, int desiredHeight, u8 **outBuf) +{ + if (!filename || !desiredWidth || !desiredHeight || !outBuf) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "uiLoadJpgFromFile: invalid parameters to process JPG image file.\n"); + return false; + } + + u8 *buf = NULL; + FILE *fp = NULL; + size_t filesize = 0, read = 0; + + fp = fopen(filename, "rb"); + if (!fp) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "uiLoadJpgFromFile: failed to open file \"%s\".\n", filename); + return false; + } + + fseek(fp, 0, SEEK_END); + filesize = ftell(fp); + rewind(fp); + + if (!filesize) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "uiLoadJpgFromFile: file \"%s\" is empty.\n", filename); + fclose(fp); + return false; + } + + buf = malloc(filesize); + if (!buf) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "uiLoadJpgFromFile: error allocating memory for image \"%s\".\n", filename); + fclose(fp); + return false; + } + + read = fread(buf, 1, filesize, fp); + + fclose(fp); + + if (read != filesize) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "uiLoadJpgFromFile: error reading image \"%s\".\n", filename); + free(buf); + return false; + } + + bool ret = uiLoadJpgFromMem(buf, filesize, expectedWidth, expectedHeight, desiredWidth, desiredHeight, outBuf); + + free(buf); + + if (!ret) strcat(strbuf, "\n"); + + return ret; +} + void uiDrawChar(FT_Bitmap *bitmap, int x, int y, u8 r, u8 g, u8 b) { if (framebuf == NULL) return; @@ -207,7 +433,7 @@ void uiScroll() u32 lx, ly; - for (ly = 0; ly < (FB_HEIGHT - font_height); ly++) + for (ly = 0; ly < (FB_HEIGHT - font_height - 8); ly++) { for (lx = 0; lx < FB_WIDTH; lx++) { @@ -215,20 +441,23 @@ void uiScroll() } } - uiFill(0, FB_HEIGHT - font_height, FB_WIDTH, font_height, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + uiFill(0, FB_HEIGHT - (8 + (font_height + (font_height / 4))), FB_WIDTH, (8 + (font_height + (font_height / 4))), BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + + breaks = (FB_HEIGHT - (8 + (font_height + (font_height / 4))) + (font_height / 8)); } void uiDrawString(const char *string, int x, int y, u8 r, u8 g, u8 b) { - u32 tmpx = x; - u32 tmpy = (font_height + y); + u32 tmpx = (x <= 8 ? 8 : (x + 8)); + u32 tmpy = (font_height + (y <= 8 ? 8 : (y + 8))); FT_Error ret = 0; FT_UInt glyph_index; - FT_GlyphSlot slot = face->glyph; + FT_GlyphSlot standardFontSlot = standardFontFace->glyph; + FT_GlyphSlot nintendoExtFontSlot = nintendoExtFontFace->glyph; u32 i; u32 str_size = strlen(string); - uint32_t tmpchar; + u32 tmpchar; ssize_t unitcount = 0; if (framebuf == NULL) @@ -241,51 +470,81 @@ void uiDrawString(const char *string, int x, int y, u8 r, u8 g, u8 b) if (tmpy >= FB_HEIGHT) { - tmpy = (FB_HEIGHT - font_height); + tmpy = (FB_HEIGHT - (8 + (font_height + (font_height / 4))) + (font_height / 8)); uiScroll(); } for(i = 0; i < str_size;) { - unitcount = decode_utf8(&tmpchar, (const uint8_t*)&string[i]); - if (unitcount <= 0) break; - i += unitcount; + bool useNintendoExt = (string[i] == 0xE0); - if (tmpchar == '\n') + if (useNintendoExt) { - tmpx = 0; - tmpy += font_height; - breaks++; - continue; - } else - if (tmpchar == '\t') - { - tmpx += (font_height * TAB_WIDTH); - continue; - } else - if (tmpchar == '\r') - { - continue; + tmpchar = (((string[i] << 8) & 0xFF00) | string[i + 1]); + i += 2; + } else { + unitcount = decode_utf8(&tmpchar, (const u8*)&string[i]); + if (unitcount <= 0) break; + i += unitcount; + + if (tmpchar == '\n') + { + tmpx = 8; + tmpy += ((font_height + (font_height / 4)) + (font_height / 8)); + breaks++; + continue; + } else + if (tmpchar == '\t') + { + tmpx += (font_height * TAB_WIDTH); + continue; + } else + if (tmpchar == '\r') + { + continue; + } } - glyph_index = FT_Get_Char_Index(face, tmpchar); - - ret = FT_Load_Glyph(face, glyph_index, FT_LOAD_DEFAULT); - if (ret == 0) ret = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL); - - if (ret) break; - - if ((tmpx + (slot->advance.x >> 6)) >= FB_WIDTH) + if (useNintendoExt) { - tmpx = 0; - tmpy += font_height; - breaks++; + glyph_index = FT_Get_Char_Index(nintendoExtFontFace, tmpchar); + + ret = FT_Load_Glyph(nintendoExtFontFace, glyph_index, FT_LOAD_DEFAULT); + if (ret == 0) ret = FT_Render_Glyph(nintendoExtFontFace->glyph, FT_RENDER_MODE_NORMAL); + + if (ret) break; + + if ((tmpx + (nintendoExtFontSlot->advance.x >> 6)) >= FB_WIDTH) + { + tmpx = 8; + tmpy += ((font_height + (font_height / 4)) + (font_height / 8)); + breaks++; + } + + uiDrawChar(&nintendoExtFontSlot->bitmap, tmpx + nintendoExtFontSlot->bitmap_left, tmpy - nintendoExtFontSlot->bitmap_top, r, g, b); + + tmpx += (nintendoExtFontSlot->advance.x >> 6); + tmpy += (nintendoExtFontSlot->advance.y >> 6); + } else { + glyph_index = FT_Get_Char_Index(standardFontFace, tmpchar); + + ret = FT_Load_Glyph(standardFontFace, glyph_index, FT_LOAD_DEFAULT); + if (ret == 0) ret = FT_Render_Glyph(standardFontFace->glyph, FT_RENDER_MODE_NORMAL); + + if (ret) break; + + if ((tmpx + (standardFontSlot->advance.x >> 6)) >= FB_WIDTH) + { + tmpx = 8; + tmpy += ((font_height + (font_height / 4)) + (font_height / 8)); + breaks++; + } + + uiDrawChar(&standardFontSlot->bitmap, tmpx + standardFontSlot->bitmap_left, tmpy - standardFontSlot->bitmap_top, r, g, b); + + tmpx += (standardFontSlot->advance.x >> 6); + tmpy += (standardFontSlot->advance.y >> 6); } - - uiDrawChar(&slot->bitmap, tmpx + slot->bitmap_left, tmpy - slot->bitmap_top, r, g, b); - - tmpx += (slot->advance.x >> 6); - tmpy += (slot->advance.y >> 6); } } @@ -316,8 +575,8 @@ void uiUpdateStatusMsg() if ((statusMessageFadeout - 4) > BG_COLOR_RGB) { int fadeout = (statusMessageFadeout > 255 ? 255 : statusMessageFadeout); - uiFill(0, FB_HEIGHT - (font_height * 2), FB_WIDTH, font_height, BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); - uiDrawString(statusMessage, 0, FB_HEIGHT - (font_height * 2), fadeout, fadeout, fadeout); + uiFill(0, FB_HEIGHT - (8 + (font_height + (font_height / 4))), FB_WIDTH, (8 + (font_height + (font_height / 4))), BG_COLOR_RGB, BG_COLOR_RGB, BG_COLOR_RGB); + uiDrawString(statusMessage, 8, (FB_HEIGHT - (16 + (font_height + (font_height / 4))) + (font_height / 8)), fadeout, fadeout, fadeout); statusMessageFadeout -= 4; } else { statusMessageFadeout = 0; @@ -326,7 +585,7 @@ void uiUpdateStatusMsg() void uiPleaseWait(u8 wait) { - uiDrawString("Please wait...", 0, breaks * font_height, 115, 115, 255); + uiDrawString("Please wait...", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); uiRefreshDisplay(); if (wait) delay(wait); } @@ -350,10 +609,10 @@ void uiPrintHeadline() { breaks = 0; uiClearScreen(); - uiDrawString(appHeadline, 0, 0, 255, 255, 255); + uiDrawString(appHeadline, 8, 8, 255, 255, 255); } -int error_screen(const char *fmt, ...) +void error_screen(const char *fmt, ...) { consoleInit(NULL); @@ -362,18 +621,21 @@ int error_screen(const char *fmt, ...) vprintf(fmt, va); va_end(va); - printf("Press [+] to exit.\n"); + printf("Press any button to exit.\n"); - while (appletMainLoop()) + while(appletMainLoop()) { hidScanInput(); - if (hidKeysDown(CONTROLLER_P1_AUTO) & KEY_PLUS) break; + + 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; + consoleUpdate(NULL); } consoleExit(NULL); - - return 0; } int uiInit() @@ -381,69 +643,166 @@ int uiInit() Result rc = 0; FT_Error ret = 0; + int status = 0; + bool pl_init = false, romfs_init = false, ft_lib_init = false, ft_std_face_init = false, ft_nintendo_face_init = false; + + /* Set initial UI state */ + uiState = stateMainMenu; + cursor = 0; + scroll = 0; + /* Initialize pl service */ rc = plInitialize(); - if (R_FAILED(rc)) return error_screen("plInitialize() failed: 0x%x\n", rc); - - /* Retrieve shared font */ - rc = plGetSharedFontByType(&font, PlSharedFontType_Standard); if (R_FAILED(rc)) { - plExit(); - return error_screen("plGetSharedFontByType() failed: 0x%x\n", rc); + error_screen("plInitialize() failed (0x%08X).\n", rc); + goto out; + } + + pl_init = true; + + /* Retrieve standard shared font */ + rc = plGetSharedFontByType(&standardFont, PlSharedFontType_Standard); + if (R_FAILED(rc)) + { + error_screen("plGetSharedFontByType() failed to retrieve standard shared font (0x%08X).\n", rc); + goto out; + } + + /* Retrieve Nintendo shared font */ + rc = plGetSharedFontByType(&nintendoExtFont, PlSharedFontType_NintendoExt); + if (R_FAILED(rc)) + { + error_screen("plGetSharedFontByType() failed to retrieve Nintendo shared font (0x%08X).\n", rc); + goto out; } /* Initialize FreeType */ ret = FT_Init_FreeType(&library); if (ret) { - plExit(); - return error_screen("FT_Init_FreeType() failed: %d\n", ret); + error_screen("FT_Init_FreeType() failed (%d).\n", ret); + goto out; } - /* Create memory face */ - ret = FT_New_Memory_Face(library, font.address, font.size, 0, &face); + ft_lib_init = true; + + /* Create memory face for the standard shared font */ + ret = FT_New_Memory_Face(library, standardFont.address, standardFont.size, 0, &standardFontFace); if (ret) { - FT_Done_FreeType(library); - plExit(); - return error_screen("FT_New_Memory_Face() failed: %d\n", ret); + error_screen("FT_New_Memory_Face() failed to create memory face for the standard shared font (%d).\n", ret); + goto out; } - /* Set font character size */ - ret = FT_Set_Char_Size(face, 0, CHAR_PT_SIZE * 64, SCREEN_DPI_CNT, SCREEN_DPI_CNT); + ft_std_face_init = true; + + /* Create memory face for the Nintendo shared font */ + ret = FT_New_Memory_Face(library, nintendoExtFont.address, nintendoExtFont.size, 0, &nintendoExtFontFace); if (ret) { - FT_Done_Face(face); - FT_Done_FreeType(library); - plExit(); - return error_screen("FT_Set_Char_Size() failed: %d\n", ret); + error_screen("FT_New_Memory_Face() failed to create memory face for the Nintendo shared font (%d).\n", ret); + goto out; + } + + ft_nintendo_face_init = true; + + /* Set standard shared font character size */ + ret = FT_Set_Char_Size(standardFontFace, 0, CHAR_PT_SIZE * 64, SCREEN_DPI_CNT, SCREEN_DPI_CNT); + if (ret) + { + error_screen("FT_Set_Char_Size() failed to set character size for the standard shared font (%d).\n", ret); + goto out; + } + + /* Set Nintendo shared font character size */ + ret = FT_Set_Char_Size(nintendoExtFontFace, 0, CHAR_PT_SIZE * 64, SCREEN_DPI_CNT, SCREEN_DPI_CNT); + if (ret) + { + error_screen("FT_Set_Char_Size() failed to set character size for the Nintendo shared font (%d).\n", ret); + goto out; } /* Store font height and max width */ - font_height = (face->size->metrics.height / 64); + font_height = (standardFontFace->size->metrics.height / 64); + + /* Prepare additional data needed by the UI functions */ + filenameBuffer = calloc(FILENAME_BUFFER_SIZE, sizeof(char)); + if (!filenameBuffer) + { + error_screen("Failed to allocate memory for the filename buffer.\n"); + goto out; + } + + /* Mount Application's RomFS */ + rc = romfsInit(); + if (R_FAILED(rc)) + { + error_screen("romfsInit() failed (0x%08X).\n", rc); + goto out; + } + + romfs_init = true; + + if (!uiLoadJpgFromFile(dirNormalIconPath, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, &dirNormalIconBuf)) + { + strcat(strbuf, "Failed to load directory icon (normal).\n"); + error_screen(strbuf); + goto out; + } + + if (!uiLoadJpgFromFile(dirHighlightIconPath, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, &dirHighlightIconBuf)) + { + strcat(strbuf, "Failed to load directory icon (highlighted).\n"); + error_screen(strbuf); + goto out; + } + + if (!uiLoadJpgFromFile(fileNormalIconPath, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, &fileNormalIconBuf)) + { + strcat(strbuf, "Failed to load file icon (normal).\n"); + error_screen(strbuf); + goto out; + } + + if (!uiLoadJpgFromFile(fileHighlightIconPath, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, &fileHighlightIconBuf)) + { + strcat(strbuf, "Failed to load file icon (highlighted).\n"); + error_screen(strbuf); + goto out; + } + + /* Unmount Application's RomFS */ + romfsExit(); + romfs_init = false; + + /* Reinitialize FS stuff */ + /* Fixes a problem where the file descriptor for the application NRO isn't properly closed */ + /* We'll need to have write access to the NRO if the user runs the update procedure */ + if (!envIsNso()) + { + fsdevUnmountAll(); + fsExit(); + + rc = fsInitialize(); + if (R_FAILED(rc)) + { + error_screen("fsInitialize() failed (0x%08X).\n", rc); + goto out; + } + + rc = fsdevMountSdmc(); + if (R_FAILED(rc)) + { + error_screen("fsdevMountSdmc() failed (0x%08X).\n", rc); + goto out; + } + } /* Create framebuffer */ framebufferCreate(&fb, nwindowGetDefault(), FB_WIDTH, FB_HEIGHT, PIXEL_FORMAT_RGBA_8888, 2); framebufferMakeLinear(&fb); - /* Prepare additional data needed by the UI functions */ - uiState = stateMainMenu; - cursor = 0; - scroll = 0; - - filenameBuffer = (char*)calloc(1, FILENAME_BUFFER_SIZE); - if (!filenameBuffer) - { - framebufferClose(&fb); - FT_Done_Face(face); - FT_Done_FreeType(library); - plExit(); - return error_screen("Failed to allocate memory for the filename buffer.\n"); - } - - uiUpdateFreeSpace(); - /* Disable screen dimming and auto sleep */ appletSetMediaPlaybackState(true); @@ -456,7 +815,32 @@ int uiInit() /* Clear screen */ uiClearScreen(); - return 1; + /* Update free space */ + uiUpdateFreeSpace(); + + /* Set output status */ + status = 1; + +out: + if (!status) + { + if (fileHighlightIconBuf) free(fileHighlightIconBuf); + if (fileNormalIconBuf) free(fileNormalIconBuf); + if (dirHighlightIconBuf) free(dirHighlightIconBuf); + if (dirNormalIconBuf) free(dirNormalIconBuf); + + if (romfs_init) romfsExit(); + + if (filenameBuffer) free(filenameBuffer); + + if (ft_nintendo_face_init) FT_Done_Face(nintendoExtFontFace); + if (ft_std_face_init) FT_Done_Face(standardFontFace); + if (ft_lib_init) FT_Done_FreeType(library); + + if (pl_init) plExit(); + } + + return status; } void uiDeinit() @@ -467,14 +851,21 @@ void uiDeinit() /* Enable screen dimming and auto sleep */ appletSetMediaPlaybackState(false); - /* Free filename buffer */ - if (filenameBuffer) free(filenameBuffer); - /* Free framebuffer object */ framebufferClose(&fb); + /* Free directory/file icons */ + free(fileHighlightIconBuf); + free(fileNormalIconBuf); + free(dirHighlightIconBuf); + free(dirNormalIconBuf); + + /* Free filename buffer */ + free(filenameBuffer); + /* Free FreeType resources */ - FT_Done_Face(face); + FT_Done_Face(nintendoExtFontFace); + FT_Done_Face(standardFontFace); FT_Done_FreeType(library); /* Deinitialize pl service */ @@ -505,272 +896,503 @@ UIResult uiProcess() u32 keysDown; u32 keysHeld; + int scrollAmount = 0; + + u32 patch, addon, xpos, ypos, startYPos; + + char versionStr[128] = {'\0'}; + uiPrintHeadline(); loadGameCardInfo(); - if (uiState == stateMainMenu || uiState == stateXciDumpMenu || uiState == stateNspDumpMenu || uiState == stateRawPartitionDumpMenu || uiState == statePartitionDataDumpMenu || uiState == stateViewGameCardFsMenu || uiState == stateViewGameCardFsBrowser) + if (uiState == stateMainMenu || uiState == stateXciDumpMenu || uiState == stateNspDumpMenu || uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu || uiState == stateHfs0Menu || uiState == stateRawHfs0PartitionDumpMenu || uiState == stateHfs0PartitionDataDumpMenu || uiState == stateHfs0BrowserMenu || uiState == stateHfs0Browser || uiState == stateRomFsMenu || uiState == stateRomFsSectionDataDumpMenu || uiState == stateRomFsSectionBrowserMenu || uiState == stateRomFsSectionBrowser || uiState == stateUpdateMenu) { - uiDrawString(appControls, 0, breaks * font_height, 255, 255, 255); + uiDrawString((!gameCardInserted ? appControlsNoGameCard : (gameCardAppCount > 1 ? appControlsMultiApp : appControlsSingleApp)), 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks += 2; - uiDrawString(freeSpaceStr, 0, breaks * font_height, 255, 255, 255); + uiDrawString(freeSpaceStr, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks += 2; - - if (uiState != stateViewGameCardFsBrowser) + } + + if (!gameCardInserted || hfs0_header == NULL || (hfs0_partition_cnt != GAMECARD_TYPE1_PARTITION_CNT && hfs0_partition_cnt != GAMECARD_TYPE2_PARTITION_CNT) || !gameCardAppCount || gameCardTitleID == NULL) + { + if (gameCardInserted) { - if (gameCardInserted && hfs0_header != NULL && (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT || hfs0_partition_cnt == GAMECARD_TYPE2_PARTITION_CNT) && gameCardAppCount > 0 && gameCardTitleID != NULL) + if (hfs0_header != NULL) { - uiDrawString("Game card is inserted!", 0, breaks * font_height, 0, 255, 0); - breaks += 2; - - /*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Root HFS0 header offset: 0x%016lX", hfs0_offset); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); - breaks++; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Root HFS0 header size: 0x%016lX", hfs0_size); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); - breaks++;*/ - - u32 app; - for(app = 0; app < gameCardAppCount; app++) + if (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT || hfs0_partition_cnt == GAMECARD_TYPE2_PARTITION_CNT) { - if (gameCardAppCount > 1) + if (gameCardAppCount > 0) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Bundled application #%u:", app + 1); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); - breaks++; - } - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Name: %s", gameCardName[app]); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); - breaks++; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Developer: %s", gameCardAuthor[app]); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); - breaks++; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Title ID: %016lX", gameCardTitleID[app]); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); - breaks++; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Version: %s", gameCardVersionStr[app]); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); - breaks += (gameCardAppCount > 1 ? 2 : 1); - } - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Size: %s", gameCardSizeStr); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); - breaks++; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Used space: %s", trimmedCardSizeStr); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); - breaks++; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Partition count: %u (%s)", hfs0_partition_cnt, GAMECARD_TYPE(hfs0_partition_cnt)); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); - - if (strlen(gameCardUpdateVersionStr)) - { - breaks++; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Bundled FW update: %s", gameCardUpdateVersionStr); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); - } - } else { - if (gameCardInserted) - { - if (hfs0_header != NULL) - { - if (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT || hfs0_partition_cnt == GAMECARD_TYPE2_PARTITION_CNT) + uiDrawString("Error: unable to retrieve the game card Title ID!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + + if (strlen(gameCardUpdateVersionStr)) { - if (gameCardAppCount > 0) - { - uiDrawString("Error: unable to retrieve the game card Title ID!", 0, breaks * font_height, 255, 0, 0); - - if (strlen(gameCardUpdateVersionStr)) - { - breaks++; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Bundled FW Update: %s", gameCardUpdateVersionStr); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); - breaks++; - - uiDrawString("In order to be able to dump data from this cartridge, make sure your console is at least on this FW version.", 0, breaks * font_height, 255, 255, 255); - } - } else { - uiDrawString("Error: gamecard application count is zero!", 0, breaks * font_height, 255, 0, 0); - } - } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unknown root HFS0 header partition count! (%u)", hfs0_partition_cnt); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + breaks++; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Bundled FW Update: %s", gameCardUpdateVersionStr); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + breaks++; + + uiDrawString("In order to be able to dump data from this cartridge, make sure your console is at least on this FW version.", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); } } else { - uiDrawString("Error: unable to get root HFS0 header data!", 0, breaks * font_height, 255, 0, 0); + uiDrawString("Error: gamecard application count is zero!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } else { - uiDrawString("Game card is not inserted!", 0, breaks * font_height, 255, 0, 0); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unknown root HFS0 header partition count! (%u)", hfs0_partition_cnt); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } + } else { + uiDrawString("Error: unable to get root HFS0 header data!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } + } else { + uiDrawString("Game card is not inserted!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + } + + uiUpdateStatusMsg(); + uiRefreshDisplay(); + + res = resultShowMainMenu; + + hidScanInput(); + keysDown = hidKeysDown(CONTROLLER_P1_AUTO); + + // Exit + if (keysDown & KEY_PLUS) res = resultExit; + + return res; + } + + if (uiState == stateMainMenu || uiState == stateXciDumpMenu || uiState == stateNspDumpMenu || uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu || uiState == stateHfs0Menu || uiState == stateRawHfs0PartitionDumpMenu || uiState == stateHfs0PartitionDataDumpMenu || uiState == stateHfs0BrowserMenu || uiState == stateHfs0Browser || uiState == stateRomFsMenu || uiState == stateRomFsSectionDataDumpMenu || uiState == stateRomFsSectionBrowserMenu || uiState == stateRomFsSectionBrowser || uiState == stateUpdateMenu) + { + if (uiState != stateHfs0Browser && uiState != stateRomFsSectionBrowser) + { + uiDrawString("Game card is inserted!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + breaks += 2; + + /*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Root HFS0 header offset: 0x%016lX", hfs0_offset); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + breaks++; + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Root HFS0 header size: 0x%016lX", hfs0_size); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + breaks++;*/ + + /* Print application info */ + xpos = 8; + ypos = ((breaks * (font_height + (font_height / 4))) + (font_height / 8)); + startYPos = ypos; + + /* Draw icon */ + if (gameCardIcon != NULL && gameCardIcon[selectedAppInfoIndex] != NULL) + { + uiDrawIcon(gameCardIcon[selectedAppInfoIndex], NACP_ICON_DOWNSCALED, NACP_ICON_DOWNSCALED, xpos, ypos + 8); + xpos += (NACP_ICON_DOWNSCALED + 8); + ypos += 8; + } + + if (gameCardName != NULL && gameCardName[selectedAppInfoIndex] != NULL && strlen(gameCardName[selectedAppInfoIndex])) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Name: %s", gameCardName[selectedAppInfoIndex]); + uiDrawString(strbuf, xpos, ypos, 0, 255, 0); + ypos += (font_height + (font_height / 4)); + } + + if (gameCardAuthor != NULL && gameCardAuthor[selectedAppInfoIndex] != NULL && strlen(gameCardAuthor[selectedAppInfoIndex])) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Developer: %s", gameCardAuthor[selectedAppInfoIndex]); + uiDrawString(strbuf, xpos, ypos, 0, 255, 0); + ypos += (font_height + (font_height / 4)); + } + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Title ID: %016lX", gameCardTitleID[selectedAppInfoIndex]); + uiDrawString(strbuf, xpos, ypos, 0, 255, 0); + + if (gameCardPatchCount > 0) + { + u32 patchCnt = 0; + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Bundled update(s): v"); + + for(patch = 0; patch < gameCardPatchCount; patch++) + { + if ((gameCardTitleID[selectedAppInfoIndex] | APPLICATION_PATCH_BITMASK) == gameCardPatchTitleID[patch]) + { + if (patchCnt > 0) strcat(strbuf, ", v"); + + convertTitleVersionToDecimal(gameCardPatchVersion[patch], versionStr, sizeof(versionStr)); + strcat(strbuf, versionStr); + + patchCnt++; + } } - res = resultShowMainMenu; + if (patchCnt > 0) uiDrawString(strbuf, (FB_WIDTH / 2) - (FB_WIDTH / 8), ypos, 0, 255, 0); + } + + ypos += (font_height + (font_height / 4)); + + if (gameCardVersionStr != NULL && gameCardVersionStr[selectedAppInfoIndex] != NULL && strlen(gameCardVersionStr[selectedAppInfoIndex])) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Version: %s", gameCardVersionStr[selectedAppInfoIndex]); + uiDrawString(strbuf, xpos, ypos, 0, 255, 0); + if (!gameCardAddOnCount) ypos += (font_height + (font_height / 4)); + } + + if (gameCardAddOnCount > 0) + { + u32 addOnCnt = 0; + + for(addon = 0; addon < gameCardAddOnCount; addon++) + { + if ((gameCardTitleID[selectedAppInfoIndex] & APPLICATION_ADDON_BITMASK) == (gameCardAddOnTitleID[addon] & APPLICATION_ADDON_BITMASK)) addOnCnt++; + } + + if (addOnCnt > 0) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Bundled DLC(s): %u", addOnCnt); + uiDrawString(strbuf, (FB_WIDTH / 2) - (FB_WIDTH / 8), ypos, 0, 255, 0); + ypos += (font_height + (font_height / 4)); + } + } + + ypos += 8; + if (xpos > 8 && (ypos - NACP_ICON_DOWNSCALED) < startYPos) ypos += (NACP_ICON_DOWNSCALED - (ypos - startYPos)); + ypos += (font_height + (font_height / 4)); + + breaks += (int)round((double)(ypos - startYPos) / (double)(font_height + (font_height / 4))); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Size: %s | Used space: %s", gameCardSizeStr, trimmedCardSizeStr); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + + if (gameCardAppCount > 1) + { + breaks++; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Base application count: %u | Base application currently displayed: %u", gameCardAppCount, selectedAppInfoIndex + 1); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + } + + breaks++; + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Partition count: %u (%s)", hfs0_partition_cnt, GAMECARD_TYPE(hfs0_partition_cnt)); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + + if (strlen(gameCardUpdateVersionStr)) + { + breaks++; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Bundled FW update: %s", gameCardUpdateVersionStr); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + } + + if (gameCardPatchCount > 0) + { + breaks++; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Total bundled update(s): %u", gameCardPatchCount); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); + } + + if (gameCardAddOnCount > 0) + { + breaks++; + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Total bundled DLC(s): %u", gameCardAddOnCount); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 0, 255, 0); } breaks += 2; } - if (gameCardInserted && hfs0_header != NULL && (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT || hfs0_partition_cnt == GAMECARD_TYPE2_PARTITION_CNT) && gameCardAppCount > 0 && gameCardTitleID != NULL) + switch(uiState) { - switch(uiState) - { - case stateMainMenu: - menu = mainMenuItems; - menuItemsCount = sizeof(mainMenuItems) / sizeof(mainMenuItems[0]); - break; - case stateXciDumpMenu: - menu = xciDumpMenuItems; - menuItemsCount = sizeof(xciDumpMenuItems) / sizeof(xciDumpMenuItems[0]); - - uiDrawString(mainMenuItems[0], 0, breaks * font_height, 115, 115, 255); - - break; - case stateNspDumpMenu: - menu = nspDumpMenuItems; - menuItemsCount = sizeof(nspDumpMenuItems) / sizeof(nspDumpMenuItems[0]); - - uiDrawString(mainMenuItems[1], 0, breaks * font_height, 115, 115, 255); - - break; - case stateRawPartitionDumpMenu: - case statePartitionDataDumpMenu: - menu = (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? partitionDumpType1MenuItems : partitionDumpType2MenuItems); - menuItemsCount = (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? (sizeof(partitionDumpType1MenuItems) / sizeof(partitionDumpType1MenuItems[0])) : (sizeof(partitionDumpType2MenuItems) / sizeof(partitionDumpType2MenuItems[0]))); - - uiDrawString((uiState == stateRawPartitionDumpMenu ? mainMenuItems[2] : mainMenuItems[3]), 0, breaks * font_height, 115, 115, 255); - - break; - case stateViewGameCardFsMenu: - menu = (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? viewGameCardFsType1MenuItems : viewGameCardFsType2MenuItems); - menuItemsCount = (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? (sizeof(viewGameCardFsType1MenuItems) / sizeof(viewGameCardFsType1MenuItems[0])) : (sizeof(viewGameCardFsType2MenuItems) / sizeof(viewGameCardFsType2MenuItems[0]))); - - uiDrawString(mainMenuItems[4], 0, breaks * font_height, 115, 115, 255); - - break; - case stateViewGameCardFsBrowser: - menu = (const char**)filenames; - menuItemsCount = filenamesCount; - - uiDrawString((hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? viewGameCardFsType1MenuItems[selectedPartitionIndex] : viewGameCardFsType2MenuItems[selectedPartitionIndex]), 0, breaks * font_height, 115, 115, 255); - breaks += 2; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "File count: %d | Current file: %d", menuItemsCount, cursor + 1); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks++; - - break; - default: - break; - } - - if (menu && menuItemsCount) - { - if (uiState != stateMainMenu) breaks += 2; + case stateMainMenu: + menu = mainMenuItems; + menuItemsCount = sizeof(mainMenuItems) / sizeof(mainMenuItems[0]); + break; + case stateXciDumpMenu: + menu = xciDumpMenuItems; + menuItemsCount = sizeof(xciDumpMenuItems) / sizeof(xciDumpMenuItems[0]); - j = 0; + uiDrawString(mainMenuItems[0], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); - for(i = scroll; i < menuItemsCount; i++, j++) + break; + case stateNspDumpMenu: + menu = nspDumpMenuItems; + menuItemsCount = sizeof(nspDumpMenuItems) / sizeof(nspDumpMenuItems[0]); + + uiDrawString(mainMenuItems[1], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + + break; + case stateNspAppDumpMenu: + menu = nspAppDumpMenuItems; + menuItemsCount = sizeof(nspAppDumpMenuItems) / sizeof(nspAppDumpMenuItems[0]); + + uiDrawString(nspDumpMenuItems[0], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + + break; + case stateNspPatchDumpMenu: + menu = nspPatchDumpMenuItems; + menuItemsCount = sizeof(nspPatchDumpMenuItems) / sizeof(nspPatchDumpMenuItems[0]); + + uiDrawString(nspDumpMenuItems[1], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + + break; + case stateNspAddOnDumpMenu: + menu = nspAddOnDumpMenuItems; + menuItemsCount = sizeof(nspAddOnDumpMenuItems) / sizeof(nspAddOnDumpMenuItems[0]); + + uiDrawString(nspDumpMenuItems[2], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + + break; + case stateHfs0Menu: + menu = hfs0MenuItems; + menuItemsCount = sizeof(hfs0MenuItems) / sizeof(hfs0MenuItems[0]); + + uiDrawString(mainMenuItems[2], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + + break; + case stateRawHfs0PartitionDumpMenu: + case stateHfs0PartitionDataDumpMenu: + menu = (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? hfs0PartitionDumpType1MenuItems : hfs0PartitionDumpType2MenuItems); + menuItemsCount = (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? (sizeof(hfs0PartitionDumpType1MenuItems) / sizeof(hfs0PartitionDumpType1MenuItems[0])) : (sizeof(hfs0PartitionDumpType2MenuItems) / sizeof(hfs0PartitionDumpType2MenuItems[0]))); + + uiDrawString((uiState == stateRawHfs0PartitionDumpMenu ? hfs0MenuItems[0] : hfs0MenuItems[1]), 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + + break; + case stateHfs0BrowserMenu: + menu = (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? hfs0BrowserType1MenuItems : hfs0BrowserType2MenuItems); + menuItemsCount = (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? (sizeof(hfs0BrowserType1MenuItems) / sizeof(hfs0BrowserType1MenuItems[0])) : (sizeof(hfs0BrowserType2MenuItems) / sizeof(hfs0BrowserType2MenuItems[0]))); + + uiDrawString(hfs0MenuItems[2], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + + break; + case stateHfs0Browser: + menu = (const char**)filenames; + menuItemsCount = filenamesCount; + + uiDrawString((hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? hfs0BrowserType1MenuItems[selectedPartitionIndex] : hfs0BrowserType2MenuItems[selectedPartitionIndex]), 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks += 2; + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "File count: %d | Current file: %d", menuItemsCount, cursor + 1); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + break; + case stateRomFsMenu: + menu = romFsMenuItems; + menuItemsCount = sizeof(romFsMenuItems) / sizeof(romFsMenuItems[0]); + + uiDrawString(mainMenuItems[3], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + + break; + case stateRomFsSectionDataDumpMenu: + menu = romFsSectionDumpMenuItems; + menuItemsCount = sizeof(romFsSectionDumpMenuItems) / sizeof(romFsSectionDumpMenuItems[0]); + + uiDrawString(romFsMenuItems[0], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + + break; + case stateRomFsSectionBrowserMenu: + menu = romFsSectionBrowserMenuItems; + menuItemsCount = sizeof(romFsSectionBrowserMenuItems) / sizeof(romFsSectionBrowserMenuItems[0]); + + uiDrawString(romFsMenuItems[1], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + + break; + case stateRomFsSectionBrowser: + menu = (const char**)filenames; + menuItemsCount = filenamesCount; + + uiDrawString(romFsMenuItems[1], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks++; + + convertTitleVersionToDecimal(gameCardVersion[selectedAppIndex], versionStr, sizeof(versionStr)); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s v%s", romFsSectionBrowserMenuItems[1], gameCardName[selectedAppIndex], versionStr); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks += 2; + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Path: romfs:%s", curRomFsPath); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks += 2; + + if (cursor > 0) { - if (j >= maxListElements) break; - - // Avoid printing the "Bundled application to dump" option in the NSP dump menu if we're not dealing with a multigame cart - if (uiState != stateNspDumpMenu || (uiState == stateNspDumpMenu && (j < 3 || (j == 3 && gameCardAppCount > 1)))) + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Entry count: %d | Current entry: %d", menuItemsCount - 1, cursor); + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Entry count: %d", menuItemsCount - 1); + } + + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); + + break; + case stateUpdateMenu: + menu = updateMenuItems; + menuItemsCount = sizeof(updateMenuItems) / sizeof(updateMenuItems[0]); + + uiDrawString(mainMenuItems[5], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + + break; + default: + break; + } + + if (menu && menuItemsCount) + { + if (uiState != stateMainMenu) breaks += 2; + + j = 0; + highlight = false; + + for(i = scroll; i < menuItemsCount; i++, j++) + { + if (((uiState != stateHfs0Browser && uiState != stateRomFsSectionBrowser) && j >= COMMON_MAX_ELEMENTS) || (uiState == stateHfs0Browser && j >= HFS0_MAX_ELEMENTS) || (uiState == stateRomFsSectionBrowser && j >= ROMFS_MAX_ELEMENTS)) break; + + // Avoid printing the "Create directory with archive bit set" option if "Split output dump" is disabled + if (uiState == stateXciDumpMenu && i == 2 && !isFat32) + { + j--; + continue; + } + + // Avoid printing the "Dump bundled update NSP" option in the NSP dump menu if our current gamecard doesn't include any bundled updates + // Also avoid printing the "Dump bundled DLC NSP" option in the NSP dump menu if our current gamecard doesn't include any bundled DLCs + if (uiState == stateNspDumpMenu) + { + if (i == 1) { - if ((j + scroll) == cursor) + if (!gameCardPatchCount) continue; + } else + if (i == 2) + { + if (!gameCardAddOnCount) { - highlight = true; - uiFill(0, (breaks * font_height) + (j * (font_height + 12)), FB_WIDTH / 2, font_height + 12, HIGHLIGHT_BG_COLOR_R, HIGHLIGHT_BG_COLOR_G, HIGHLIGHT_BG_COLOR_B); - uiDrawString(menu[i], 0, (breaks * font_height) + (j * (font_height + 12)) + 6, HIGHLIGHT_FONT_COLOR_R, HIGHLIGHT_FONT_COLOR_G, HIGHLIGHT_FONT_COLOR_B); - highlight = false; + continue; } else { - uiDrawString(menu[i], 0, (breaks * font_height) + (j * (font_height + 12)) + 6, 255, 255, 255); - } - - // Print XCI dump menu settings values - if (uiState == stateXciDumpMenu && j > 0) - { - if ((j + scroll) == cursor) highlight = true; - - switch(j) - { - case 1: // Split output dump (FAT32 support) - if (isFat32) - { - uiDrawString("Yes", OPTIONS_X_POS, (breaks * font_height) + (j * (font_height + 12)) + 6, 0, 255, 0); - } else { - uiDrawString("No", OPTIONS_X_POS, (breaks * font_height) + (j * (font_height + 12)) + 6, 255, 0, 0); - } - break; - case 2: // Dump certificate - if (dumpCert) - { - uiDrawString("Yes", OPTIONS_X_POS, (breaks * font_height) + (j * (font_height + 12)) + 6, 0, 255, 0); - } else { - uiDrawString("No", OPTIONS_X_POS, (breaks * font_height) + (j * (font_height + 12)) + 6, 255, 0, 0); - } - break; - case 3: // Trim output dump - if (trimDump) - { - uiDrawString("Yes", OPTIONS_X_POS, (breaks * font_height) + (j * (font_height + 12)) + 6, 0, 255, 0); - } else { - uiDrawString("No", OPTIONS_X_POS, (breaks * font_height) + (j * (font_height + 12)) + 6, 255, 0, 0); - } - break; - case 4: // CRC32 checksum calculation + dump verification - if (calcCrc) - { - uiDrawString("Yes", OPTIONS_X_POS, (breaks * font_height) + (j * (font_height + 12)) + 6, 0, 255, 0); - } else { - uiDrawString("No", OPTIONS_X_POS, (breaks * font_height) + (j * (font_height + 12)) + 6, 255, 0, 0); - } - break; - default: - break; - } - - if ((j + scroll) == cursor) highlight = false; - } - - // Print NSP dump menu settings values - if (uiState == stateNspDumpMenu && j > 0) - { - if ((j + scroll) == cursor) highlight = true; - - switch(j) - { - case 1: // Split output dump (FAT32 support) - if (isFat32) - { - uiDrawString("Yes", OPTIONS_X_POS, (breaks * font_height) + (j * (font_height + 12)) + 6, 0, 255, 0); - } else { - uiDrawString("No", OPTIONS_X_POS, (breaks * font_height) + (j * (font_height + 12)) + 6, 255, 0, 0); - } - break; - case 2: // CRC32 checksum calculation - if (calcCrc) - { - uiDrawString("Yes", OPTIONS_X_POS, (breaks * font_height) + (j * (font_height + 12)) + 6, 0, 255, 0); - } else { - uiDrawString("No", OPTIONS_X_POS, (breaks * font_height) + (j * (font_height + 12)) + 6, 255, 0, 0); - } - break; - case 3: // Bundled application to dump - uiDrawString(gameCardName[selectedAppIndex], OPTIONS_X_POS, (breaks * font_height) + (j * (font_height + 12)) + 6, 255, 255, 255); - break; - default: - break; - } - - if ((j + scroll) == cursor) highlight = false; + if (!gameCardPatchCount) j--; } } } + + // Avoid printing the parent directory entry ("..") in the RomFS browser if we're currently at the root directory + if (uiState == stateRomFsSectionBrowser && i == 0 && strlen(curRomFsPath) <= 1) + { + j--; + continue; + } + + xpos = 8; + int font_r = 255, font_g = 255, font_b = 255; + + if (i == cursor) + { + highlight = true; + + uiFill(0, 8 + (breaks * (font_height + (font_height / 4))) + (j * (font_height + 12)), FB_WIDTH, font_height + 12, HIGHLIGHT_BG_COLOR_R, HIGHLIGHT_BG_COLOR_G, HIGHLIGHT_BG_COLOR_B); + + font_r = HIGHLIGHT_FONT_COLOR_R; + font_g = HIGHLIGHT_FONT_COLOR_G; + font_b = HIGHLIGHT_FONT_COLOR_B; + } + + if (uiState == stateHfs0Browser || uiState == stateRomFsSectionBrowser) + { + u8 *icon = (highlight ? (uiState == stateRomFsSectionBrowser ? (romFsBrowserEntries[i].type == ROMFS_ENTRY_DIR ? dirHighlightIconBuf : fileHighlightIconBuf) : fileHighlightIconBuf) : (uiState == stateRomFsSectionBrowser ? (romFsBrowserEntries[i].type == ROMFS_ENTRY_DIR ? dirNormalIconBuf : fileNormalIconBuf) : fileNormalIconBuf)); + + uiDrawIcon(icon, BROWSER_ICON_DIMENSION, BROWSER_ICON_DIMENSION, xpos, (breaks * (font_height + (font_height / 4))) + (j * (font_height + 12)) + 14); + + xpos += (BROWSER_ICON_DIMENSION + 8); + } + + uiDrawString(menu[i], xpos, (breaks * (font_height + (font_height / 4))) + (j * (font_height + 12)) + 6, font_r, font_g, font_b); + + // Print XCI dump menu settings values + if (uiState == stateXciDumpMenu && i > 0) + { + switch(i) + { + case 1: // Split output dump (FAT32 support) + uiDrawString((isFat32 ? "Yes" : "No"), OPTIONS_X_POS, (breaks * (font_height + (font_height / 4))) + (j * (font_height + 12)) + 6, (isFat32 ? 0 : 255), (isFat32 ? 255 : 0), 0); + break; + case 2: // Create directory with archive bit set + uiDrawString((setXciArchiveBit ? "Yes" : "No"), OPTIONS_X_POS, (breaks * (font_height + (font_height / 4))) + (j * (font_height + 12)) + 6, (setXciArchiveBit ? 0 : 255), (setXciArchiveBit ? 255 : 0), 0); + break; + case 3: // Dump certificate + uiDrawString((dumpCert ? "Yes" : "No"), OPTIONS_X_POS, (breaks * (font_height + (font_height / 4))) + (j * (font_height + 12)) + 6, (dumpCert ? 0 : 255), (dumpCert ? 255 : 0), 0); + break; + case 4: // Trim output dump + uiDrawString((trimDump ? "Yes" : "No"), OPTIONS_X_POS, (breaks * (font_height + (font_height / 4))) + (j * (font_height + 12)) + 6, (trimDump ? 0 : 255), (trimDump ? 255 : 0), 0); + break; + case 5: // CRC32 checksum calculation + dump verification + uiDrawString((calcCrc ? "Yes" : "No"), OPTIONS_X_POS, (breaks * (font_height + (font_height / 4))) + (j * (font_height + 12)) + 6, (calcCrc ? 0 : 255), (calcCrc ? 255 : 0), 0); + break; + default: + break; + } + } + + // Print NSP dump menus settings values + if ((uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu) && i > 0) + { + switch(i) + { + case 1: // Split output dump (FAT32 support) + uiDrawString((isFat32 ? "Yes" : "No"), OPTIONS_X_POS, (breaks * (font_height + (font_height / 4))) + (j * (font_height + 12)) + 6, (isFat32 ? 0 : 255), (isFat32 ? 255 : 0), 0); + break; + case 2: // CRC32 checksum calculation + uiDrawString((calcCrc ? "Yes" : "No"), OPTIONS_X_POS, (breaks * (font_height + (font_height / 4))) + (j * (font_height + 12)) + 6, (calcCrc ? 0 : 255), (calcCrc ? 255 : 0), 0); + break; + case 3: // Bundled application/update/DLC to dump + if (uiState == stateNspAppDumpMenu) + { + // Print application name + convertTitleVersionToDecimal(gameCardVersion[selectedAppIndex], versionStr, sizeof(versionStr)); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s v%s", gameCardName[selectedAppIndex], versionStr); + } else + if (uiState == stateNspPatchDumpMenu) + { + // Find a matching application to print its name + // Otherwise, just print the Title ID + retrieveDescriptionForPatchOrAddOn(gameCardPatchTitleID[selectedPatchIndex], gameCardPatchVersion[selectedPatchIndex], false, NULL); + } else + if (uiState == stateNspAddOnDumpMenu) + { + // Find a matching application to print its name and Title ID + // Otherwise, just print the Title ID + retrieveDescriptionForPatchOrAddOn(gameCardAddOnTitleID[selectedAddOnIndex], gameCardAddOnVersion[selectedAddOnIndex], true, NULL); + } + + uiDrawString(strbuf, OPTIONS_X_POS, (breaks * (font_height + (font_height / 4))) + (j * (font_height + 12)) + 6, 255, 255, 255); + + break; + default: + break; + } + + if (i == 2) + { + if (calcCrc) + { + uiDrawString("This takes extra time after the NSP dump has been completed!", FB_WIDTH / 2, (breaks * (font_height + (font_height / 4))) + (j * (font_height + 12)) + 6, 255, 255, 255); + } else { + uiFill(FB_WIDTH / 2, 8 + (breaks * (font_height + (font_height / 4))) + (j * (font_height + 12)), FB_WIDTH / 2, font_height + 12, (highlight ? HIGHLIGHT_BG_COLOR_R : BG_COLOR_RGB), (highlight ? HIGHLIGHT_BG_COLOR_G : BG_COLOR_RGB), (highlight ? HIGHLIGHT_BG_COLOR_B : BG_COLOR_RGB)); + } + } + } + + // Print RomFS menus settings values + if ((uiState == stateRomFsSectionDataDumpMenu || uiState == stateRomFsSectionBrowserMenu) && i > 0) + { + switch(i) + { + case 1: // Bundled application to dump/browse + // Print application name + convertTitleVersionToDecimal(gameCardVersion[selectedAppIndex], versionStr, sizeof(versionStr)); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s v%s", gameCardName[selectedAppIndex], versionStr); + uiDrawString(strbuf, OPTIONS_X_POS, (breaks * (font_height + (font_height / 4))) + (j * (font_height + 12)) + 6, 255, 255, 255); + break; + default: + break; + } + } + + if (i == cursor) highlight = false; } } @@ -787,7 +1409,25 @@ UIResult uiProcess() // Process key inputs only if the UI state hasn't been changed if (res == resultNone) { - int scrollAmount = 0; + // Process base application info change + if (gameCardAppCount > 1 && uiState != stateHfs0Browser && uiState != stateRomFsSectionBrowser) + { + if ((keysDown & KEY_L) || (keysDown & KEY_ZL)) + { + if (selectedAppInfoIndex > 0) + { + selectedAppInfoIndex--; + } else { + selectedAppInfoIndex = 0; + } + } + + if ((keysDown & KEY_R) || (keysDown & KEY_ZR)) + { + selectedAppInfoIndex++; + if (selectedAppInfoIndex >= gameCardAppCount) selectedAppInfoIndex = (gameCardAppCount - 1); + } + } if (uiState == stateXciDumpMenu) { @@ -803,15 +1443,18 @@ UIResult uiProcess() switch(cursor) { case 1: // Split output dump (FAT32 support) - isFat32 = false; + isFat32 = setXciArchiveBit = false; break; - case 2: // Dump certificate + case 2: // Create directory with archive bit set + setXciArchiveBit = false; + break; + case 3: // Dump certificate dumpCert = false; break; - case 3: // Trim output dump + case 4: // Trim output dump trimDump = false; break; - case 4: // CRC32 checksum calculation + dump verification + case 5: // CRC32 checksum calculation + dump verification calcCrc = false; break; default: @@ -827,13 +1470,16 @@ UIResult uiProcess() case 1: // Split output dump (FAT32 support) isFat32 = true; break; - case 2: // Dump certificate + case 2: // Create directory with archive bit set + setXciArchiveBit = true; + break; + case 3: // Dump certificate dumpCert = true; break; - case 3: // Trim output dump + case 4: // Trim output dump trimDump = true; break; - case 4: // CRC32 checksum calculation + dump verification + case 5: // CRC32 checksum calculation + dump verification calcCrc = true; break; default: @@ -842,18 +1488,30 @@ UIResult uiProcess() } // Go up - if ((keysDown & KEY_DUP) || (keysHeld & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) scrollAmount = -1; + if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) scrollAmount = -1; // Go down - if ((keysDown & KEY_DDOWN) || (keysHeld & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) scrollAmount = 1; + if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) scrollAmount = 1; } else - if (uiState == stateNspDumpMenu) + if (uiState == stateNspAppDumpMenu || uiState == stateNspPatchDumpMenu || uiState == stateNspAddOnDumpMenu) { // Select - if ((keysDown & KEY_A) && cursor == 0) res = resultDumpNsp; + if ((keysDown & KEY_A) && cursor == 0) + { + selectedNspDumpType = (uiState == stateNspAppDumpMenu ? DUMP_APP_NSP : (uiState == stateNspPatchDumpMenu ? DUMP_PATCH_NSP : DUMP_ADDON_NSP)); + res = resultDumpNsp; + } // Back - if (keysDown & KEY_B) res = resultShowMainMenu; + if (keysDown & KEY_B) + { + if (uiState == stateNspAppDumpMenu && !gameCardPatchCount && !gameCardAddOnCount) + { + res = resultShowMainMenu; + } else { + res = resultShowNspDumpMenu; + } + } // Change option to false if (keysDown & KEY_LEFT) @@ -866,7 +1524,102 @@ UIResult uiProcess() case 2: // CRC32 checksum calculation calcCrc = false; break; - case 3: // Bundled application to dump + case 3: // Bundled application/update/DLC to dump + if (uiState == stateNspAppDumpMenu) + { + if (selectedAppIndex > 0) + { + selectedAppIndex--; + } else { + selectedAppIndex = 0; + } + } else + if (uiState == stateNspPatchDumpMenu) + { + if (selectedPatchIndex > 0) + { + selectedPatchIndex--; + } else { + selectedPatchIndex = 0; + } + } else + if (uiState == stateNspAddOnDumpMenu) + { + if (selectedAddOnIndex > 0) + { + selectedAddOnIndex--; + } else { + selectedAddOnIndex = 0; + } + } + break; + default: + break; + } + } + + // Change option to true + if (keysDown & KEY_RIGHT) + { + switch(cursor) + { + case 1: // Split output dump (FAT32 support) + isFat32 = true; + break; + case 2: // CRC32 checksum calculation + calcCrc = true; + break; + case 3: // Bundled application/update/DLC to dump + if (uiState == stateNspAppDumpMenu) + { + if (gameCardAppCount > 1) + { + selectedAppIndex++; + if (selectedAppIndex >= gameCardAppCount) selectedAppIndex = (gameCardAppCount - 1); + } + } else + if (uiState == stateNspPatchDumpMenu) + { + if (gameCardPatchCount > 1) + { + selectedPatchIndex++; + if (selectedPatchIndex >= gameCardPatchCount) selectedPatchIndex = (gameCardPatchCount - 1); + } + } else + if (uiState == stateNspAddOnDumpMenu) + { + if (gameCardAddOnCount > 1) + { + selectedAddOnIndex++; + if (selectedAddOnIndex >= gameCardAddOnCount) selectedAddOnIndex = (gameCardAddOnCount - 1); + } + } + break; + default: + break; + } + } + + // Go up + if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) scrollAmount = -1; + + // Go down + if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) scrollAmount = 1; + } else + if (uiState == stateRomFsSectionDataDumpMenu || uiState == stateRomFsSectionBrowserMenu) + { + // Select + if ((keysDown & KEY_A) && cursor == 0) res = (uiState == stateRomFsSectionDataDumpMenu ? resultDumpRomFsSectionData : resultRomFsSectionBrowserGetEntries); + + // Back + if (keysDown & KEY_B) res = resultShowMainMenu; + + // Change option to false + if (keysDown & KEY_LEFT) + { + switch(cursor) + { + case 1: // Bundled application to dump/browse if (selectedAppIndex > 0) { selectedAppIndex--; @@ -884,13 +1637,7 @@ UIResult uiProcess() { switch(cursor) { - case 1: // Split output dump (FAT32 support) - isFat32 = true; - break; - case 2: // CRC32 checksum calculation - calcCrc = true; - break; - case 3: // Bundled application to dump + case 1: // Bundled application to dump/browse if (gameCardAppCount > 1) { selectedAppIndex++; @@ -903,10 +1650,10 @@ UIResult uiProcess() } // Go up - if ((keysDown & KEY_DUP) || (keysHeld & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) scrollAmount = -1; + if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) scrollAmount = -1; // Go down - if ((keysDown & KEY_DDOWN) || (keysHeld & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) scrollAmount = 1; + if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) scrollAmount = 1; } else { // Select if (keysDown & KEY_A) @@ -917,67 +1664,166 @@ UIResult uiProcess() { case 0: res = resultShowXciDumpMenu; + + // Reset options to their default values + isFat32 = false; + dumpCert = false; + trimDump = false; + calcCrc = true; break; case 1: - selectedAppIndex = 0; - res = resultShowNspDumpMenu; + if (!gameCardPatchCount && !gameCardAddOnCount) + { + res = resultShowNspAppDumpMenu; + + // Reset options to their default values + isFat32 = false; + calcCrc = false; + selectedAppIndex = 0; + } else { + res = resultShowNspDumpMenu; + } break; case 2: - res = resultShowRawPartitionDumpMenu; + res = resultShowHfs0Menu; break; case 3: - res = resultShowPartitionDataDumpMenu; + res = resultShowRomFsMenu; break; case 4: - res = resultShowViewGameCardFsMenu; - break; - case 5: res = resultDumpGameCardCertificate; break; - case 6: - res = resultUpdateNSWDBXml; - break; - case 7: - res = resultUpdateApplication; + case 5: + res = resultShowUpdateMenu; break; default: break; } } else - if (uiState == stateRawPartitionDumpMenu) + if (uiState == stateNspDumpMenu) + { + switch(cursor) + { + case 0: + res = resultShowNspAppDumpMenu; + break; + case 1: + res = (gameCardPatchCount > 0 ? resultShowNspPatchDumpMenu : resultShowNspAddOnDumpMenu); + break; + case 2: + res = resultShowNspAddOnDumpMenu; + break; + default: + break; + } + + // Reset options to their default values + isFat32 = false; + calcCrc = false; + selectedAppIndex = 0; + selectedPatchIndex = 0; + selectedAddOnIndex = 0; + } else + if (uiState == stateHfs0Menu) + { + switch(cursor) + { + case 0: + res = resultShowRawHfs0PartitionDumpMenu; + break; + case 1: + res = resultShowHfs0PartitionDataDumpMenu; + break; + case 2: + res = resultShowHfs0BrowserMenu; + break; + default: + break; + } + } else + if (uiState == stateRawHfs0PartitionDumpMenu) { // Save selected partition index selectedPartitionIndex = (u32)cursor; - res = resultDumpRawPartition; + res = resultDumpRawHfs0Partition; } else - if (uiState == statePartitionDataDumpMenu) + if (uiState == stateHfs0PartitionDataDumpMenu) { // Save selected partition index selectedPartitionIndex = (u32)cursor; - res = resultDumpPartitionData; + res = resultDumpHfs0PartitionData; } else - if (uiState == stateViewGameCardFsMenu) + if (uiState == stateHfs0BrowserMenu) { // Save selected partition index selectedPartitionIndex = (u32)cursor; - res = resultShowViewGameCardFsGetList; + res = resultHfs0BrowserGetList; } else - if (uiState == stateViewGameCardFsBrowser) + if (uiState == stateHfs0Browser) { // Save selected file index selectedFileIndex = (u32)cursor; - res = resultViewGameCardFsBrowserCopyFile; + res = resultHfs0BrowserCopyFile; + } else + if (uiState == stateRomFsMenu) + { + switch(cursor) + { + case 0: + res = (gameCardAppCount > 1 ? resultShowRomFsSectionDataDumpMenu : resultDumpRomFsSectionData); + + // Reset options to their default values + selectedAppIndex = 0; + + break; + case 1: + res = (gameCardAppCount > 1 ? resultShowRomFsSectionBrowserMenu : resultRomFsSectionBrowserGetEntries); + + // Reset options to their default values + selectedAppIndex = 0; + + break; + default: + break; + } + } else + if (uiState == stateRomFsSectionBrowser) + { + if (menu && menuItemsCount) + { + // Save selected file index + selectedFileIndex = (u32)cursor; + res = (romFsBrowserEntries[cursor].type == ROMFS_ENTRY_DIR ? resultRomFsSectionBrowserChangeDir : resultRomFsSectionBrowserCopyFile); + } + } else + if (uiState == stateUpdateMenu) + { + switch(cursor) + { + case 0: + res = resultUpdateNSWDBXml; + break; + case 1: + res = resultUpdateApplication; + break; + default: + break; + } } } // Back if (keysDown & KEY_B) { - if (uiState == stateRawPartitionDumpMenu || uiState == statePartitionDataDumpMenu || uiState == stateViewGameCardFsMenu) + if (uiState == stateNspDumpMenu || uiState == stateHfs0Menu || uiState == stateRomFsMenu || uiState == stateUpdateMenu) { res = resultShowMainMenu; } else - if (uiState == stateViewGameCardFsBrowser) + if (uiState == stateRawHfs0PartitionDumpMenu || uiState == stateHfs0PartitionDataDumpMenu || uiState == stateHfs0BrowserMenu) + { + res = resultShowHfs0Menu; + } else + if (uiState == stateHfs0Browser) { free(partitionHfs0Header); partitionHfs0Header = NULL; @@ -986,17 +1832,43 @@ UIResult uiProcess() partitionHfs0FileCount = 0; partitionHfs0StrTableSize = 0; - res = resultShowViewGameCardFsMenu; + res = resultShowHfs0BrowserMenu; + } else + if (uiState == stateRomFsSectionDataDumpMenu || uiState == stateRomFsSectionBrowserMenu) + { + res = resultShowRomFsMenu; + } else + if (uiState == stateRomFsSectionBrowser) + { + if (strlen(curRomFsPath) > 1) + { + // Point to the parent directory entry ("..") + selectedFileIndex = 0; + res = resultRomFsSectionBrowserChangeDir; + } else { + if (romFsBrowserEntries != NULL) + { + free(romFsBrowserEntries); + romFsBrowserEntries = NULL; + } + + freeRomFsContext(); + + res = (gameCardAppCount > 1 ? resultShowRomFsSectionBrowserMenu : resultShowRomFsMenu); + } } } - // Go up - if ((keysDown & KEY_DUP) || (keysHeld & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) scrollAmount = -1; - if ((keysDown & KEY_DLEFT) || (keysHeld & KEY_LSTICK_LEFT) || (keysHeld & KEY_RSTICK_LEFT)) scrollAmount = -5; - - // Go down - if ((keysDown & KEY_DDOWN) || (keysHeld & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) scrollAmount = 1; - if ((keysDown & KEY_DRIGHT) || (keysHeld & KEY_LSTICK_RIGHT) || (keysHeld & KEY_RSTICK_RIGHT)) scrollAmount = 5; + if (menu && menuItemsCount) + { + // Go up + if ((keysDown & KEY_DUP) || (keysDown & KEY_LSTICK_UP) || (keysHeld & KEY_RSTICK_UP)) scrollAmount = -1; + if ((keysDown & KEY_DLEFT) || (keysDown & KEY_LSTICK_LEFT) || (keysHeld & KEY_RSTICK_LEFT)) scrollAmount = -5; + + // Go down + if ((keysDown & KEY_DDOWN) || (keysDown & KEY_LSTICK_DOWN) || (keysHeld & KEY_RSTICK_DOWN)) scrollAmount = 1; + if ((keysDown & KEY_DRIGHT) || (keysDown & KEY_LSTICK_RIGHT) || (keysHeld & KEY_RSTICK_RIGHT)) scrollAmount = 5; + } } // Calculate scroll only if the UI state hasn't been changed @@ -1009,7 +1881,7 @@ UIResult uiProcess() if (cursor < menuItemsCount - 1) { cursor++; - if ((cursor - scroll) >= maxListElements) scroll++; + if (((uiState != stateHfs0Browser && uiState != stateRomFsSectionBrowser) && (cursor - scroll) >= COMMON_MAX_ELEMENTS) || (uiState == stateHfs0Browser && (cursor - scroll) >= HFS0_MAX_ELEMENTS) || (uiState == stateRomFsSectionBrowser && (cursor - scroll) >= ROMFS_MAX_ELEMENTS)) scroll++; } } } else @@ -1025,35 +1897,70 @@ UIResult uiProcess() } } - // Avoid placing the cursor on the "Bundled application to dump" option in the NSP dump menu if we're not dealing with multigame carts - if (uiState == stateNspDumpMenu && cursor == 3 && gameCardAppCount == 1) cursor = 2; + // Avoid placing the cursor on the "Create directory with archive bit set" option in the XCI dump menu if "Split output dump" is disabled + if (uiState == stateXciDumpMenu && cursor == 2 && !isFat32) + { + if (scrollAmount > 0) + { + cursor++; + } else + if (scrollAmount < 0) + { + cursor--; + } + } + + // Avoid placing the cursor on the "Dump bundled update NSP" option in the NSP dump menu if our current gamecard doesn't include any bundled updates + // Also avoid placing the cursor on the "Dump bundled DLC NSP" option in the NSP dump menu if our current gamecard doesn't include any bundled DLCs + if (uiState == stateNspDumpMenu) + { + if ((gameCardPatchCount && !gameCardAddOnCount) || (!gameCardPatchCount && gameCardAddOnCount)) + { + if (cursor >= 2) cursor = 1; + } else + if (!gameCardPatchCount && !gameCardAddOnCount) + { + // Just in case + cursor = 0; + } + } + + // Avoid placing the cursor on the parent directory entry ("..") in the RomFS browser if we're currently at the root directory + if (uiState == stateRomFsSectionBrowser && cursor == 0 && strlen(curRomFsPath) <= 1) cursor = 1; } } } else if (uiState == stateDumpXci) { - uiDrawString(mainMenuItems[0], 0, breaks * font_height, 115, 115, 255); + uiDrawString(mainMenuItems[0], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks++; snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", xciDumpMenuItems[1], (isFat32 ? "Yes" : "No")); - uiDrawString(strbuf, 0, breaks * font_height, 115, 115, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks++; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", xciDumpMenuItems[2], (dumpCert ? "Yes" : "No")); - uiDrawString(strbuf, 0, breaks * font_height, 115, 115, 255); + if (isFat32) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", xciDumpMenuItems[2], (setXciArchiveBit ? "Yes" : "No")); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks++; + } + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", xciDumpMenuItems[3], (dumpCert ? "Yes" : "No")); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks++; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", xciDumpMenuItems[3], (trimDump ? "Yes" : "No")); - uiDrawString(strbuf, 0, breaks * font_height, 115, 115, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", xciDumpMenuItems[4], (trimDump ? "Yes" : "No")); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks++; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", xciDumpMenuItems[4], (calcCrc ? "Yes" : "No")); - uiDrawString(strbuf, 0, breaks * font_height, 115, 115, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", xciDumpMenuItems[5], (calcCrc ? "Yes" : "No")); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks += 2; uiRefreshDisplay(); - dumpCartridgeImage(&fsOperatorInstance, isFat32, dumpCert, trimDump, calcCrc); + dumpCartridgeImage(&fsOperatorInstance, isFat32, setXciArchiveBit, dumpCert, trimDump, calcCrc); waitForButtonPress(); @@ -1062,67 +1969,79 @@ UIResult uiProcess() } else if (uiState == stateDumpNsp) { - uiDrawString(mainMenuItems[1], 0, breaks * font_height, 115, 115, 255); + uiDrawString(nspDumpMenuItems[selectedNspDumpType], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks++; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", nspDumpMenuItems[1], (isFat32 ? "Yes" : "No")); - uiDrawString(strbuf, 0, breaks * font_height, 115, 115, 255); + menu = (selectedNspDumpType == DUMP_APP_NSP ? nspAppDumpMenuItems : (selectedNspDumpType == DUMP_PATCH_NSP ? nspPatchDumpMenuItems : nspAddOnDumpMenuItems)); + + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", menu[1], (isFat32 ? "Yes" : "No")); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks++; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", nspDumpMenuItems[2], (calcCrc ? "Yes" : "No")); - uiDrawString(strbuf, 0, breaks * font_height, 115, 115, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", menu[2], (calcCrc ? "Yes" : "No")); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks++; - if (gameCardAppCount > 1) + if (selectedNspDumpType == DUMP_APP_NSP) { - breaks++; - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s", nspDumpMenuItems[3], gameCardName[selectedAppIndex]); - uiDrawString(strbuf, 0, breaks * font_height, 115, 115, 255); + convertTitleVersionToDecimal(gameCardVersion[selectedAppIndex], versionStr, sizeof(versionStr)); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s v%s", menu[3], gameCardName[selectedAppIndex], versionStr); + } else + if (selectedNspDumpType == DUMP_PATCH_NSP) + { + retrieveDescriptionForPatchOrAddOn(gameCardPatchTitleID[selectedPatchIndex], gameCardPatchVersion[selectedPatchIndex], false, menu[3]); + } else + if (selectedNspDumpType == DUMP_ADDON_NSP) + { + retrieveDescriptionForPatchOrAddOn(gameCardAddOnTitleID[selectedAddOnIndex], gameCardAddOnVersion[selectedAddOnIndex], true, menu[3]); } + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks += 2; uiRefreshDisplay(); - dumpApplicationNSP(&fsOperatorInstance, isFat32, calcCrc, selectedAppIndex); + dumpNintendoSubmissionPackage(&fsOperatorInstance, selectedNspDumpType, (selectedNspDumpType == DUMP_APP_NSP ? selectedAppIndex : (selectedNspDumpType == DUMP_PATCH_NSP ? selectedPatchIndex : selectedAddOnIndex)), isFat32, calcCrc); waitForButtonPress(); uiUpdateFreeSpace(); - res = resultShowNspDumpMenu; + + res = (selectedNspDumpType == DUMP_APP_NSP ? resultShowNspAppDumpMenu : (selectedNspDumpType == DUMP_PATCH_NSP ? resultShowNspPatchDumpMenu : resultShowNspAddOnDumpMenu)); } else - if (uiState == stateDumpRawPartition) + if (uiState == stateDumpRawHfs0Partition) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Raw %s", (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? partitionDumpType1MenuItems[selectedPartitionIndex] : partitionDumpType2MenuItems[selectedPartitionIndex])); - uiDrawString(strbuf, 0, breaks * font_height, 115, 115, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Raw %s", (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? hfs0PartitionDumpType1MenuItems[selectedPartitionIndex] : hfs0PartitionDumpType2MenuItems[selectedPartitionIndex])); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks += 2; uiRefreshDisplay(); - dumpRawPartition(&fsOperatorInstance, selectedPartitionIndex, true); + dumpRawHfs0Partition(&fsOperatorInstance, selectedPartitionIndex, true); waitForButtonPress(); uiUpdateFreeSpace(); - res = resultShowRawPartitionDumpMenu; + res = resultShowRawHfs0PartitionDumpMenu; } else - if (uiState == stateDumpPartitionData) + if (uiState == stateDumpHfs0PartitionData) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Data %s", (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? partitionDumpType1MenuItems[selectedPartitionIndex] : partitionDumpType2MenuItems[selectedPartitionIndex])); - uiDrawString(strbuf, 0, breaks * font_height, 115, 115, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Data %s", (hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? hfs0PartitionDumpType1MenuItems[selectedPartitionIndex] : hfs0PartitionDumpType2MenuItems[selectedPartitionIndex])); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks += 2; uiRefreshDisplay(); - dumpPartitionData(&fsOperatorInstance, selectedPartitionIndex); + dumpHfs0PartitionData(&fsOperatorInstance, selectedPartitionIndex); waitForButtonPress(); uiUpdateFreeSpace(); - res = resultShowPartitionDataDumpMenu; + res = resultShowHfs0PartitionDataDumpMenu; } else - if (uiState == stateViewGameCardFsGetList) + if (uiState == stateHfs0BrowserGetList) { - uiDrawString((hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? viewGameCardFsType1MenuItems[selectedPartitionIndex] : viewGameCardFsType2MenuItems[selectedPartitionIndex]), 0, breaks * font_height, 115, 115, 255); + uiDrawString((hfs0_partition_cnt == GAMECARD_TYPE1_PARTITION_CNT ? hfs0BrowserType1MenuItems[selectedPartitionIndex] : hfs0BrowserType2MenuItems[selectedPartitionIndex]), 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks += 2; uiPleaseWait(0); @@ -1132,33 +2051,157 @@ UIResult uiProcess() { cursor = 0; scroll = 0; - res = resultShowViewGameCardFsBrowser; + res = resultShowHfs0Browser; } else { - breaks += 2; waitForButtonPress(); - res = resultShowViewGameCardFsMenu; + res = resultShowHfs0BrowserMenu; } } else - if (uiState == stateViewGameCardFsBrowserCopyFile) + if (uiState == stateHfs0BrowserCopyFile) { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Manual File Dump: %s (Partition %u [%s])", filenames[selectedFileIndex], selectedPartitionIndex, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, selectedPartitionIndex)); - uiDrawString(strbuf, 0, breaks * font_height, 115, 115, 255); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Manual File Dump: %s (HFS0 partition %u [%s])", filenames[selectedFileIndex], selectedPartitionIndex, GAMECARD_PARTITION_NAME(hfs0_partition_cnt, selectedPartitionIndex)); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks += 2; uiRefreshDisplay(); - dumpFileFromPartition(&fsOperatorInstance, selectedPartitionIndex, selectedFileIndex, filenames[selectedFileIndex]); - - breaks += 2; + dumpFileFromHfs0Partition(&fsOperatorInstance, selectedPartitionIndex, selectedFileIndex, filenames[selectedFileIndex]); waitForButtonPress(); uiUpdateFreeSpace(); - res = resultShowViewGameCardFsBrowser; + res = resultShowHfs0Browser; + } else + if (uiState == stateDumpRomFsSectionData) + { + uiDrawString(romFsMenuItems[0], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks++; + + convertTitleVersionToDecimal(gameCardVersion[selectedAppIndex], versionStr, sizeof(versionStr)); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s v%s", romFsSectionDumpMenuItems[1], gameCardName[selectedAppIndex], versionStr); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks += 2; + + uiRefreshDisplay(); + + dumpRomFsSectionData(&fsOperatorInstance, selectedAppIndex); + + waitForButtonPress(); + + uiUpdateFreeSpace(); + res = (gameCardAppCount > 1 ? resultShowRomFsSectionDataDumpMenu : resultShowRomFsMenu); + } else + if (uiState == stateRomFsSectionBrowserGetEntries) + { + uiDrawString(romFsMenuItems[1], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks++; + + convertTitleVersionToDecimal(gameCardVersion[selectedAppIndex], versionStr, sizeof(versionStr)); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s v%s", romFsSectionBrowserMenuItems[1], gameCardName[selectedAppIndex], versionStr); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks += 2; + + uiPleaseWait(0); + breaks += 2; + + bool romfs_fail = false; + + initRomFsContext(); + + if (readProgramNcaRomFs(selectedAppIndex)) + { + if (getRomFsFileList(0)) + { + cursor = 0; + scroll = 0; + res = resultShowRomFsSectionBrowser; + } else { + freeRomFsContext(); + romfs_fail = true; + } + } else { + romfs_fail = true; + } + + if (romfs_fail) + { + breaks += 2; + waitForButtonPress(); + res = (gameCardAppCount > 1 ? resultShowRomFsSectionBrowserMenu : resultShowRomFsMenu); + } + } else + if (uiState == stateRomFsSectionBrowserChangeDir) + { + uiDrawString(romFsMenuItems[1], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks++; + + convertTitleVersionToDecimal(gameCardVersion[selectedAppIndex], versionStr, sizeof(versionStr)); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s v%s", romFsSectionBrowserMenuItems[1], gameCardName[selectedAppIndex], versionStr); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks += 2; + + bool romfs_fail = false; + + if (romFsBrowserEntries[selectedFileIndex].type == ROMFS_ENTRY_DIR) + { + if (getRomFsFileList(romFsBrowserEntries[selectedFileIndex].offset)) + { + cursor = 0; + scroll = 0; + res = resultShowRomFsSectionBrowser; + } else { + romfs_fail = true; + } + } else { + // Unexpected condition + uiDrawString("Error: the selected entry is not a directory!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + romfs_fail = true; + } + + if (romfs_fail) + { + if (romFsBrowserEntries != NULL) + { + free(romFsBrowserEntries); + romFsBrowserEntries = NULL; + } + + freeRomFsContext(); + + breaks += 2; + waitForButtonPress(); + res = (gameCardAppCount > 1 ? resultShowRomFsSectionBrowserMenu : resultShowRomFsMenu); + } + } else + if (uiState == stateRomFsSectionBrowserCopyFile) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Manual File Dump: %s (RomFS)", filenames[selectedFileIndex]); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks++; + + convertTitleVersionToDecimal(gameCardVersion[selectedAppIndex], versionStr, sizeof(versionStr)); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Base application: %s v%s", gameCardName[selectedAppIndex], versionStr); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + breaks += 2; + + uiRefreshDisplay(); + + if (romFsBrowserEntries[selectedFileIndex].type == ROMFS_ENTRY_FILE) + { + dumpFileFromRomFsSection(selectedAppIndex, romFsBrowserEntries[selectedFileIndex].offset, true); + } else { + // Unexpected condition + uiDrawString("Error: the selected entry is not a file!", 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); + } + + waitForButtonPress(); + + uiUpdateFreeSpace(); + res = resultShowRomFsSectionBrowser; } else if (uiState == stateDumpGameCardCertificate) { - uiDrawString(mainMenuItems[5], 0, breaks * font_height, 115, 115, 255); + uiDrawString(mainMenuItems[4], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks += 2; dumpGameCertificate(&fsOperatorInstance); @@ -1170,7 +2213,7 @@ UIResult uiProcess() } else if (uiState == stateUpdateNSWDBXml) { - uiDrawString(mainMenuItems[6], 0, breaks * font_height, 115, 115, 255); + uiDrawString(updateMenuItems[0], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks += 2; updateNSWDBXml(); @@ -1178,11 +2221,11 @@ UIResult uiProcess() waitForButtonPress(); uiUpdateFreeSpace(); - res = resultShowMainMenu; + res = resultShowUpdateMenu; } else if (uiState == stateUpdateApplication) { - uiDrawString(mainMenuItems[7], 0, breaks * font_height, 115, 115, 255); + uiDrawString(updateMenuItems[1], 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 115, 115, 255); breaks += 2; updateApplication(); @@ -1190,7 +2233,7 @@ UIResult uiProcess() waitForButtonPress(); uiUpdateFreeSpace(); - res = resultShowMainMenu; + res = resultShowUpdateMenu; } return res; diff --git a/source/ui.h b/source/ui.h index b705eca..4194bc5 100644 --- a/source/ui.h +++ b/source/ui.h @@ -19,26 +19,57 @@ #define HIGHLIGHT_FONT_COLOR_G 255 #define HIGHLIGHT_FONT_COLOR_B 197 -#define OPTIONS_X_POS (35 * CHAR_PT_SIZE) +#define COMMON_MAX_ELEMENTS 8 +#define HFS0_MAX_ELEMENTS 14 +#define ROMFS_MAX_ELEMENTS 12 + +#define OPTIONS_X_POS (35 * CHAR_PT_SIZE) #define TAB_WIDTH 4 +#define BROWSER_ICON_DIMENSION 16 + +#define NINTENDO_FONT_A "\xE0\xA0" +#define NINTENDO_FONT_B "\xE0\xA1" +#define NINTENDO_FONT_L "\xE0\xA4" +#define NINTENDO_FONT_R "\xE0\xA5" +#define NINTENDO_FONT_ZL "\xE0\xA6" +#define NINTENDO_FONT_ZR "\xE0\xA7" +#define NINTENDO_FONT_DPAD "\xE0\xAA" +#define NINTENDO_FONT_PLUS "\xE0\xB5" +#define NINTENDO_FONT_HOME "\xE0\xB9" +#define NINTENDO_FONT_LSTICK "\xE0\xC1" +#define NINTENDO_FONT_RSTICK "\xE0\xC2" + typedef enum { resultNone, resultShowMainMenu, resultShowXciDumpMenu, resultDumpXci, resultShowNspDumpMenu, + resultShowNspAppDumpMenu, + resultShowNspPatchDumpMenu, + resultShowNspAddOnDumpMenu, resultDumpNsp, - resultShowRawPartitionDumpMenu, - resultDumpRawPartition, - resultShowPartitionDataDumpMenu, - resultDumpPartitionData, - resultShowViewGameCardFsMenu, - resultShowViewGameCardFsGetList, - resultShowViewGameCardFsBrowser, - resultViewGameCardFsBrowserCopyFile, + resultShowHfs0Menu, + resultShowRawHfs0PartitionDumpMenu, + resultDumpRawHfs0Partition, + resultShowHfs0PartitionDataDumpMenu, + resultDumpHfs0PartitionData, + resultShowHfs0BrowserMenu, + resultHfs0BrowserGetList, + resultShowHfs0Browser, + resultHfs0BrowserCopyFile, + resultShowRomFsMenu, + resultShowRomFsSectionDataDumpMenu, + resultDumpRomFsSectionData, + resultShowRomFsSectionBrowserMenu, + resultRomFsSectionBrowserGetEntries, + resultShowRomFsSectionBrowser, + resultRomFsSectionBrowserChangeDir, + resultRomFsSectionBrowserCopyFile, resultDumpGameCardCertificate, + resultShowUpdateMenu, resultUpdateNSWDBXml, resultUpdateApplication, resultExit @@ -49,22 +80,41 @@ typedef enum { stateXciDumpMenu, stateDumpXci, stateNspDumpMenu, + stateNspAppDumpMenu, + stateNspPatchDumpMenu, + stateNspAddOnDumpMenu, stateDumpNsp, - stateRawPartitionDumpMenu, - stateDumpRawPartition, - statePartitionDataDumpMenu, - stateDumpPartitionData, - stateViewGameCardFsMenu, - stateViewGameCardFsGetList, - stateViewGameCardFsBrowser, - stateViewGameCardFsBrowserCopyFile, + stateHfs0Menu, + stateRawHfs0PartitionDumpMenu, + stateDumpRawHfs0Partition, + stateHfs0PartitionDataDumpMenu, + stateDumpHfs0PartitionData, + stateHfs0BrowserMenu, + stateHfs0BrowserGetList, + stateHfs0Browser, + stateHfs0BrowserCopyFile, + stateRomFsMenu, + stateRomFsSectionDataDumpMenu, + stateDumpRomFsSectionData, + stateRomFsSectionBrowserMenu, + stateRomFsSectionBrowserGetEntries, + stateRomFsSectionBrowser, + stateRomFsSectionBrowserChangeDir, + stateRomFsSectionBrowserCopyFile, stateDumpGameCardCertificate, + stateUpdateMenu, stateUpdateNSWDBXml, stateUpdateApplication } UIState; void uiFill(int x, int y, int width, int height, u8 r, u8 g, u8 b); +void uiDrawIcon(const u8 *icon, int width, int height, int x, int y); + +bool uiLoadJpgFromMem(u8 *rawJpg, size_t rawJpgSize, int expectedWidth, int expectedHeight, int desiredWidth, int desiredHeight, u8 **outBuf); + +bool uiLoadJpgFromFile(const char *filename, int expectedWidth, int expectedHeight, int desiredWidth, int desiredHeight, u8 **outBuf); + void uiDrawString(const char *string, int x, int y, u8 r, u8 g, u8 b); void uiRefreshDisplay(); @@ -81,10 +131,10 @@ void uiClearScreen(); void uiPrintHeadline(); -int uiInit(); - void uiDeinit(); +int uiInit(); + void uiSetState(UIState state); UIState uiGetState(); diff --git a/source/util.c b/source/util.c index 51e7173..f3dfb2d 100644 --- a/source/util.c +++ b/source/util.c @@ -15,14 +15,11 @@ #include #include #include -#include #include "dumper.h" -#include "fsext.h" +#include "fs_ext.h" #include "ui.h" #include "util.h" -#include "aes.h" -#include "extkeys.h" /* Extern variables */ @@ -45,17 +42,11 @@ const char *nswReleasesChildrenImgCrc = "imgcrc"; const char *nswReleasesChildrenReleaseName = "releasename"; const char *githubReleasesApiUrl = "https://api.github.com/repos/DarkMatterCore/gcdumptool/releases/latest"; -const char *gcDumpToolTmpPath = "sdmc:/switch/gcdumptool.nro.tmp"; const char *gcDumpToolPath = "sdmc:/switch/gcdumptool.nro"; - const char *userAgent = "gcdumptool/" APP_VERSION " (Nintendo Switch)"; -const char *keysFilePath = "sdmc:/switch/prod.keys"; - /* Statically allocated variables */ -nca_keyset_t nca_keyset; - static char *result_buf = NULL; static size_t result_sz = 0; static size_t result_written = 0; @@ -77,11 +68,11 @@ bool gameCardInserted; u64 gameCardSize = 0, trimmedCardSize = 0; char gameCardSizeStr[32] = {'\0'}, trimmedCardSizeStr[32] = {'\0'}; -char *hfs0_header = NULL; +u8 *hfs0_header = NULL; u64 hfs0_offset = 0, hfs0_size = 0; u32 hfs0_partition_cnt = 0; -char *partitionHfs0Header = NULL; +u8 *partitionHfs0Header = NULL; u64 partitionHfs0HeaderOffset = 0, partitionHfs0HeaderSize = 0; u32 partitionHfs0FileCount = 0, partitionHfs0StrTableSize = 0; @@ -89,17 +80,33 @@ u32 gameCardAppCount = 0; u64 *gameCardTitleID = NULL; u32 *gameCardVersion = NULL; +u32 gameCardPatchCount = 0; +u64 *gameCardPatchTitleID = NULL; +u32 *gameCardPatchVersion = NULL; + +u32 gameCardAddOnCount = 0; +u64 *gameCardAddOnTitleID = NULL; +u32 *gameCardAddOnVersion = NULL; + char **gameCardName = NULL; char **fixedGameCardName = NULL; char **gameCardAuthor = NULL; char **gameCardVersionStr = NULL; +u8 **gameCardIcon = NULL; u64 gameCardUpdateTitleID = 0; u32 gameCardUpdateVersion = 0; char gameCardUpdateVersionStr[128] = {'\0'}; +romfs_ctx_t romFsContext; + +char curRomFsPath[NAME_BUF_LEN] = {'\0'}; +romfs_browser_entry *romFsBrowserEntries = NULL; + char strbuf[NAME_BUF_LEN * 4] = {'\0'}; +char appLaunchPath[NAME_BUF_LEN] = {'\0'}; + bool isGameCardInserted() { bool inserted; @@ -141,20 +148,152 @@ void delay(u8 seconds) 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); +} + +bool listGameCardTitles(NcmContentMetaDatabase *ncmDb, u8 filter) +{ + if (!ncmDb || (filter != META_DB_REGULAR_APPLICATION && filter != META_DB_PATCH && filter != META_DB_ADDON)) + { + uiStatusMsg("listGameCardTitles: invalid parameters (0x%02X filter).", filter); + return false; + } + + bool success = false; + bool proceed = true; + + Result result; + + NcmApplicationContentMetaKey *titleList = NULL; + NcmApplicationContentMetaKey *titleListTmp = NULL; + size_t titleListSize = sizeof(NcmApplicationContentMetaKey); + + u32 written = 0; + u32 total = 0; + + u64 *titleIDs = NULL; + u32 *versions = 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("listGameCardTitles: title count mismatch in ncmContentMetaDatabaseListApplication (%u != %u) (0x%02X filter).", written, total, filter); + proceed = false; + } + } else { + uiStatusMsg("listGameCardTitles: ncmContentMetaDatabaseListApplication failed! (0x%08X) (0x%02X filter).", result, filter); + proceed = false; + } + } else { + uiStatusMsg("listGameCardTitles: 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) + { + gameCardAppCount = total; + gameCardTitleID = titleIDs; + gameCardVersion = versions; + } else + if (filter == META_DB_PATCH) + { + gameCardPatchCount = total; + gameCardPatchTitleID = titleIDs; + gameCardPatchVersion = versions; + } else + if (filter == META_DB_ADDON) + { + gameCardAddOnCount = total; + gameCardAddOnTitleID = titleIDs; + gameCardAddOnVersion = versions; + } + + success = true; + } else { + if (titleIDs != NULL) free(titleIDs); + + if (versions != NULL) free(versions); + + uiStatusMsg("listGameCardTitles: failed to allocate memory for TID/version buffer! (0x%02X filter).", filter); + } + } + } else { + // Only display this error if we're dealing with regular applications. Patch and AddOn titles are optional + if (filter == META_DB_REGULAR_APPLICATION) uiStatusMsg("listGameCardTitles: ncmContentMetaDatabaseListApplication wrote no entries to output buffer! (0x%02X filter).", filter); + } + } else { + uiStatusMsg("listGameCardTitles: ncmContentMetaDatabaseListApplication failed! (0x%08X) (0x%02X filter).", result, filter); + } + + free(titleList); + } else { + uiStatusMsg("listGameCardTitles: unable to allocate memory for the ApplicationContentMetaKey struct (0x%02X filter).", filter); + } + + return success; +} + bool getGameCardTitleIDAndVersion() { - bool proceed = true; bool success = false; Result result; NcmContentMetaDatabase ncmDb; - NcmApplicationContentMetaKey *appList = NULL; - NcmApplicationContentMetaKey *appListTmp = NULL; - size_t appListSize = sizeof(NcmApplicationContentMetaKey); - - u32 written = 0; - u32 total = 0; + gameCardAppCount = 0; if (gameCardTitleID != NULL) { @@ -168,93 +307,52 @@ bool getGameCardTitleIDAndVersion() gameCardVersion = NULL; } - appList = (NcmApplicationContentMetaKey*)calloc(1, appListSize); - if (appList) + gameCardPatchCount = 0; + + if (gameCardPatchTitleID != NULL) { - if (R_SUCCEEDED(result = ncmOpenContentMetaDatabase(FsStorageId_GameCard, &ncmDb))) + free(gameCardPatchTitleID); + gameCardPatchTitleID = NULL; + } + + if (gameCardPatchVersion != NULL) + { + free(gameCardPatchVersion); + gameCardPatchVersion = NULL; + } + + gameCardAddOnCount = 0; + + if (gameCardAddOnTitleID != NULL) + { + free(gameCardAddOnTitleID); + gameCardAddOnTitleID = NULL; + } + + if (gameCardAddOnVersion != NULL) + { + free(gameCardAddOnVersion); + gameCardAddOnVersion = NULL; + } + + if (R_SUCCEEDED(result = ncmOpenContentMetaDatabase(FsStorageId_GameCard, &ncmDb))) + { + success = listGameCardTitles(&ncmDb, META_DB_REGULAR_APPLICATION); + if (success) { - if (R_SUCCEEDED(result = ncmContentMetaDatabaseListApplication(&ncmDb, META_DB_REGULAR_APPLICATION, appList, appListSize, &written, &total))) - { - if (written && total) - { - if (total > written) - { - appListSize *= total; - appListTmp = (NcmApplicationContentMetaKey*)realloc(appList, appListSize); - if (appListTmp) - { - appList = appListTmp; - memset(appList, 0, appListSize); - - if (R_SUCCEEDED(result = ncmContentMetaDatabaseListApplication(&ncmDb, META_DB_REGULAR_APPLICATION, appList, appListSize, &written, &total))) - { - if (written != total) - { - uiStatusMsg("getGameCardTitleIDAndVersion: application count mismatch in ncmContentMetaDatabaseListApplication (%u != %u)", written, total); - proceed = false; - } - } else { - uiStatusMsg("getGameCardTitleIDAndVersion: ncmContentMetaDatabaseListApplication failed! (0x%08X)", result); - proceed = false; - } - } else { - uiStatusMsg("getGameCardTitleIDAndVersion: error reallocating output buffer for ncmContentMetaDatabaseListApplication (%u %s).", total, (total == 1 ? "entry" : "entries")); - proceed = false; - } - } - - if (proceed) - { - gameCardTitleID = (u64*)calloc(total, sizeof(u64)); - gameCardVersion = (u32*)calloc(total, sizeof(u32)); - - if (gameCardTitleID != NULL && gameCardVersion != NULL) - { - u32 i; - for(i = 0; i < total; i++) - { - gameCardTitleID[i] = appList[i].metaRecord.titleId; - gameCardVersion[i] = appList[i].metaRecord.version; - } - - gameCardAppCount = total; - - success = true; - } else { - if (gameCardTitleID != NULL) - { - free(gameCardTitleID); - gameCardTitleID = NULL; - } - - if (gameCardVersion != NULL) - { - free(gameCardVersion); - gameCardVersion = NULL; - } - - uiStatusMsg("getGameCardTitleIDAndVersion: failed to allocate memory for TID/version buffer!"); - } - } - } else { - uiStatusMsg("getGameCardTitleIDAndVersion: ncmContentMetaDatabaseListApplication wrote no entries to output buffer!"); - } - } else { - uiStatusMsg("getGameCardTitleIDAndVersion: ncmContentMetaDatabaseListApplication failed! (0x%08X)", result); - } - } else { - uiStatusMsg("getGameCardTitleIDAndVersion: ncmOpenContentMetaDatabase failed! (0x%08X)", result); + listGameCardTitles(&ncmDb, META_DB_PATCH); + listGameCardTitles(&ncmDb, META_DB_ADDON); } - free(appList); + serviceClose(&(ncmDb.s)); } else { - uiStatusMsg("getGameCardTitleIDAndVersion: unable to allocate memory for the ApplicationContentMetaKey struct."); + uiStatusMsg("getGameCardTitleIDAndVersion: ncmOpenContentMetaDatabase failed! (0x%08X)", result); } return success; } -void convertTitleVersionToDecimal(u32 version, char *versionBuf, int versionBufSize) +void convertTitleVersionToDecimal(u32 version, char *versionBuf, size_t versionBufSize) { u8 major = (u8)((version >> 26) & 0x3F); u8 middle = (u8)((version >> 20) & 0x3F); @@ -264,17 +362,21 @@ void convertTitleVersionToDecimal(u32 version, char *versionBuf, int versionBufS snprintf(versionBuf, versionBufSize, "%u (%u.%u.%u.%u)", version, major, middle, minor, build); } -bool getGameCardControlNacp(u64 titleID, char *nameBuf, int nameBufSize, char *authorBuf, int authorBufSize) +bool getGameCardControlNacp(u64 titleID, char *nameBuf, int nameBufSize, char *authorBuf, int authorBufSize, u8 **iconBuf) { - if (titleID == 0) return false; + if (!titleID || !nameBuf || !nameBufSize || !authorBuf || !authorBufSize || !iconBuf) + { + uiStatusMsg("getGameCardControlNacp: invalid parameters to retrieve Control.nacp."); + return false; + } - bool success = false; + bool getNameAndAuthor = false, getIcon = false, success = false; Result result; size_t outsize = 0; NsApplicationControlData *buf = NULL; NacpLanguageEntry *langentry = NULL; - buf = (NsApplicationControlData*)calloc(1, sizeof(NsApplicationControlData)); + buf = calloc(1, sizeof(NsApplicationControlData)); if (buf) { if (R_SUCCEEDED(result = nsGetApplicationControlData(1, titleID, buf, sizeof(NsApplicationControlData), &outsize))) @@ -285,10 +387,15 @@ bool getGameCardControlNacp(u64 titleID, char *nameBuf, int nameBufSize, char *a { strncpy(nameBuf, langentry->name, nameBufSize); strncpy(authorBuf, langentry->author, authorBufSize); - success = true; + getNameAndAuthor = true; } else { uiStatusMsg("getGameCardControlNacp: 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("getGameCardControlNacp: Control.nacp buffer size (%u bytes) is too small! Expected: %u bytes", outsize, sizeof(buf->nacp)); } @@ -334,7 +441,7 @@ void strtrim(char *str) void freeStringsPtr(char **var) { - if (var) + if (var) { u64 i; for(i = 0; var[i]; i++) free(var[i]); @@ -342,6 +449,41 @@ void freeStringsPtr(char **var) } } +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) @@ -371,6 +513,8 @@ void freeGameCardInfo() trimmedCardSize = 0; memset(trimmedCardSizeStr, 0, sizeof(trimmedCardSizeStr)); + gameCardAppCount = 0; + if (gameCardTitleID != NULL) { free(gameCardTitleID); @@ -383,6 +527,34 @@ void freeGameCardInfo() gameCardVersion = NULL; } + gameCardPatchCount = 0; + + if (gameCardPatchTitleID != NULL) + { + free(gameCardPatchTitleID); + gameCardPatchTitleID = NULL; + } + + if (gameCardPatchVersion != NULL) + { + free(gameCardPatchVersion); + gameCardPatchVersion = NULL; + } + + gameCardAddOnCount = 0; + + if (gameCardAddOnTitleID != NULL) + { + free(gameCardAddOnTitleID); + gameCardAddOnTitleID = NULL; + } + + if (gameCardAddOnVersion != NULL) + { + free(gameCardAddOnVersion); + gameCardAddOnVersion = NULL; + } + if (gameCardName != NULL) { freeStringsPtr(gameCardName); @@ -407,9 +579,23 @@ void freeGameCardInfo() gameCardVersionStr = NULL; } + if (gameCardIcon != NULL) + { + freeStringsPtr((char**)gameCardIcon); + gameCardIcon = NULL; + } + gameCardUpdateTitleID = 0; gameCardUpdateVersion = 0; memset(gameCardUpdateVersionStr, 0, sizeof(gameCardUpdateVersionStr)); + + freeRomFsContext(); + + if (romFsBrowserEntries != NULL) + { + free(romFsBrowserEntries); + romFsBrowserEntries = NULL; + } } bool getRootHfs0Header() @@ -480,7 +666,7 @@ bool getRootHfs0Header() memcpy(&hfs0_offset, gamecard_header + HFS0_OFFSET_ADDR, sizeof(u64)); memcpy(&hfs0_size, gamecard_header + HFS0_SIZE_ADDR, sizeof(u64)); - hfs0_header = (char*)malloc(hfs0_size); + hfs0_header = malloc(hfs0_size); if (!hfs0_header) { uiStatusMsg("getRootHfs0Header: Unable to allocate memory for the root HFS0 header!"); @@ -671,8 +857,9 @@ void loadGameCardInfo() fixedGameCardName = calloc(gameCardAppCount + 1, sizeof(char*)); gameCardAuthor = calloc(gameCardAppCount + 1, sizeof(char*)); gameCardVersionStr = calloc(gameCardAppCount + 1, sizeof(char*)); + gameCardIcon = calloc(gameCardAppCount + 1, sizeof(u8*)); - if (gameCardName != NULL && fixedGameCardName != NULL && gameCardAuthor != NULL && gameCardVersionStr != NULL) + if (gameCardName != NULL && fixedGameCardName != NULL && gameCardAuthor != NULL && gameCardVersionStr != NULL && gameCardIcon != NULL) { u32 i; for(i = 0; i < gameCardAppCount; i++) @@ -686,28 +873,30 @@ void loadGameCardInfo() { convertTitleVersionToDecimal(gameCardVersion[i], gameCardVersionStr[i], VERSION_STR_LEN); - getGameCardControlNacp(gameCardTitleID[i], gameCardName[i], NACP_APPNAME_LEN, gameCardAuthor[i], NACP_AUTHOR_LEN); - strtrim(gameCardName[i]); - strtrim(gameCardAuthor[i]); - - if (strlen(gameCardName[i])) + if (getGameCardControlNacp(gameCardTitleID[i], gameCardName[i], NACP_APPNAME_LEN, gameCardAuthor[i], NACP_AUTHOR_LEN, &(gameCardIcon[i]))) { + strtrim(gameCardName[i]); + strtrim(gameCardAuthor[i]); + snprintf(fixedGameCardName[i], NACP_APPNAME_LEN, gameCardName[i]); removeIllegalCharacters(fixedGameCardName[i]); + } else { + freeBuf = true; + break; } } else { + uiStatusMsg("loadGameCardInfo: error allocating memory for gamecard information (application #%u).", i + 1); freeBuf = true; break; } } } else { + uiStatusMsg("loadGameCardInfo: error allocating memory for gamecard information."); freeBuf = true; } if (freeBuf) { - uiStatusMsg("loadGameCardInfo: error allocating memory for gamecard information."); - if (gameCardName != NULL) { freeStringsPtr(gameCardName); @@ -731,6 +920,12 @@ void loadGameCardInfo() freeStringsPtr(gameCardVersionStr); gameCardVersionStr = NULL; } + + if (gameCardIcon != NULL) + { + freeStringsPtr((char**)gameCardIcon); + gameCardIcon = NULL; + } } } } @@ -742,7 +937,7 @@ void loadGameCardInfo() } } -bool getHfs0EntryDetails(char *hfs0Header, u64 hfs0HeaderOffset, u64 hfs0HeaderSize, u32 num_entries, u32 entry_idx, bool isRoot, u32 partitionIndex, u64 *out_offset, u64 *out_size) +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; @@ -750,7 +945,7 @@ bool getHfs0EntryDetails(char *hfs0Header, u64 hfs0HeaderOffset, u64 hfs0HeaderS if ((HFS0_ENTRY_TABLE_ADDR + (sizeof(hfs0_entry_table) * num_entries)) > hfs0HeaderSize) return false; - hfs0_entry_table *entryTable = (hfs0_entry_table*)malloc(sizeof(hfs0_entry_table) * num_entries); + 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); @@ -814,7 +1009,7 @@ bool getPartitionHfs0Header(u32 partition) partitionHfs0StrTableSize = 0; } - char *buf = NULL; + u8 buf[MEDIA_UNIT_SIZE] = {0}; Result result; FsGameCardHandle handle; FsStorage gameCardStorage; @@ -829,120 +1024,111 @@ bool getPartitionHfs0Header(u32 partition) if (R_SUCCEEDED(result = fsDeviceOperatorGetGameCardHandle(&fsOperatorInstance, &handle))) { /*snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "GetGameCardHandle succeeded: 0x%08X", handle.value); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++;*/ - // Same ugly hack from dumpRawPartition() + // 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, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++;*/ - buf = (char*)malloc(MEDIA_UNIT_SIZE); - if (buf) + // First read MEDIA_UNIT_SIZE bytes + if (R_SUCCEEDED(result = fsStorageRead(&gameCardStorage, partitionHfs0HeaderOffset, buf, MEDIA_UNIT_SIZE))) { - // 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) { - // 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) { - // 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, 0, breaks * font_height, 255, 255, 255); - breaks++; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Partition #%u HFS0 header size: %lu bytes", partition, partitionHfs0HeaderSize); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks++; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Partition #%u file count: %u", partition, partitionHfs0FileCount); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); - breaks++; - - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Partition #%u string table size: %u bytes", partition, partitionHfs0StrTableSize); - uiDrawString(strbuf, 0, breaks * font_height, 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 = (char*)malloc(partitionHfs0HeaderSize); - if (partitionHfs0Header) + // Check if we were dealing with the correct header size all along + if (partitionHfs0HeaderSize == MEDIA_UNIT_SIZE) { - // 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))) { - // 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, 0, breaks * font_height, 255, 0, 0); - } + 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, 0, breaks * font_height, 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, 0, breaks * font_height, 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 { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Magic word mismatch! 0x%08X != 0x%08X", magic, HFS0_MAGIC); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + 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]), "StorageRead failed (0x%08X) at offset 0x%016lX", result, partitionHfs0HeaderOffset); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + 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); } - - free(buf); } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Failed to allocate memory for the HFS0 header from partition #%u!", partition); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 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); } fsStorageClose(&gameCardStorage); } else { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "OpenGameCardStorage failed! (0x%08X)", result); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + 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, 0, breaks * font_height, 255, 0, 0); + 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!", 0, breaks * font_height, 255, 0, 0); + 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); } if (!success) breaks += 2; @@ -956,27 +1142,31 @@ bool getHfs0FileList(u32 partition) if (!partitionHfs0Header) { - uiDrawString("HFS0 partition header information unavailable!", 0, breaks * font_height, 255, 0, 0); + 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!", 0, breaks * font_height, 255, 0, 0); + 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, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + breaks += 2; return false; } - hfs0_entry_table *entryTable = (hfs0_entry_table*)malloc(sizeof(hfs0_entry_table) * partitionHfs0FileCount); + hfs0_entry_table *entryTable = calloc(partitionHfs0FileCount, sizeof(hfs0_entry_table)); if (!entryTable) { - uiDrawString("Unable to allocate memory for the HFS0 file entries!", 0, breaks * font_height, 255, 0, 0); + 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; } @@ -993,25 +1183,490 @@ bool getHfs0FileList(u32 partition) for(i = 0; i < max_elements; i++) { u32 filename_offset = (HFS0_ENTRY_TABLE_ADDR + (sizeof(hfs0_entry_table) * partitionHfs0FileCount) + entryTable[i].filename_offset); - addStringToFilenameBuffer(partitionHfs0Header + filename_offset, &nextFilename); + addStringToFilenameBuffer((char*)partitionHfs0Header + filename_offset, &nextFilename); } free(entryTable); + breaks += 2; + + return true; +} + +u8 *getPartitionHfs0FileByName(FsStorage *gameCardStorage, const char *filename, u64 *outSize) +{ + if (!partitionHfs0Header || !partitionHfs0FileCount || !partitionHfs0HeaderSize || !gameCardStorage || !filename || !outSize) + { + 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 proceed = true, found = false; + + u32 i; + u8 *buf = NULL; + 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; + + buf = malloc(tmp_hfs0_entry.file_size); + if (!buf) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unable to allocate memory for file \"%s\"!", filename); + 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, buf, tmp_hfs0_entry.file_size))) + { + 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); + free(buf); + buf = NULL; + proceed = false; + break; + } + + *outSize = tmp_hfs0_entry.file_size; + 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 buf; +} + +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 readProgramNcaRomFs(u32 appIndex) +{ + Result result; + u32 i = 0; + u32 written = 0; + u32 total = 0; + u32 appNcaCount = 0; + u32 partition = (hfs0_partition_cnt - 1); // Select the secure partition + + NcmContentMetaDatabase ncmDb; + memset(&ncmDb, 0, sizeof(NcmContentMetaDatabase)); + + NcmContentStorage ncmStorage; + memset(&ncmStorage, 0, sizeof(NcmContentStorage)); + + NcmApplicationContentMetaKey *appList = NULL; + NcmContentRecord *appContentRecords = NULL; + size_t appListSize = (sizeof(NcmApplicationContentMetaKey) * gameCardAppCount); + + 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; + + workaroundPartitionZeroAccess(&fsOperatorInstance); + + if (!getPartitionHfs0Header(partition)) 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(FsStorageId_GameCard, &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 || written != gameCardAppCount) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: title count mismatch in ncmContentMetaDatabaseListApplication (%u != %u)", written, gameCardAppCount); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); + goto out; + } + + appContentRecords = calloc(partitionHfs0FileCount, 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[appIndex].metaRecord), 0, appContentRecords, partitionHfs0FileCount * 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; + } + + appNcaCount = written; + + if (R_FAILED(result = ncmOpenContentStorage(FsStorageId_GameCard, &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; + + uiDrawString("Retrieving RomFS entry tables...", 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]), "Error: ncmContentStorageReadContentIdFile failed! (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)) 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 (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; + } + + // Read directory and file tables from the RomFS section + if (!readRomFsEntriesFromNca(&ncmStorage, &ncaId, &dec_nca_header, decrypted_nca_keys)) goto out; + + success = true; + +out: + if (appContentRecords) free(appContentRecords); + + serviceClose(&(ncmDb.s)); + + if (appList) free(appList); + + if (partitionHfs0Header) + { + free(partitionHfs0Header); + partitionHfs0Header = NULL; + partitionHfs0HeaderOffset = 0; + partitionHfs0HeaderSize = 0; + partitionHfs0FileCount = 0; + partitionHfs0StrTableSize = 0; + } + + return success; +} + +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); + 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); + addStringToFilenameBuffer(curName, &nextFilename); + + i++; + } + + entryOffset += round_up(ROMFS_NONAME_FILEENTRY_SIZE + entry->nameLen, 4); + } + } + return true; } int getSdCardFreeSpace(u64 *out) { - struct statvfs st; - int rc; + Result result; + FsFileSystem *sdfs = NULL; + u64 size = 0; + int rc = 0; - rc = statvfs("sdmc:/", &st); - if (rc != 0) + sdfs = fsdevGetDefaultFileSystem(); + if (!sdfs) { - uiStatusMsg("getSdCardFreeSpace: Unable to get SD card filesystem stats! statvfs: %d (%s).", errno, strerror(errno)); + uiStatusMsg("getSdCardFreeSpace: fsdevGetDefaultFileSystem failed!"); + return rc; + } + + if (R_SUCCEEDED(result = fsFsGetFreeSpace(sdfs, "/", &size))) + { + *out = size; + rc = 1; } else { - *out = (u64)(st.f_bsize * st.f_bfree); + uiStatusMsg("getSdCardFreeSpace: fsFsGetFreeSpace failed! (0x%08X)", result); } return rc; @@ -1064,36 +1719,46 @@ void convertSize(u64 size, char *out, int bufsize) snprintf(out, bufsize, "%s", buffer); } -char *generateDumpName() +char *generateDumpFullName() { - if (!gameCardAppCount || !fixedGameCardName || !gameCardVersion || !gameCardTitleID) return NULL; + if (!gameCardAppCount || !fixedGameCardName || !gameCardTitleID || !gameCardVersion) return NULL; - u32 i; + u32 i, j; char tmp[512] = {'\0'}; char *fullname = NULL; char *fullnameTmp = NULL; size_t strsize = NAME_BUF_LEN; - fullname = (char*)malloc(strsize); + fullname = calloc(strsize, sizeof(char)); if (!fullname) return NULL; - memset(fullname, 0, strsize); - for(i = 0; i < gameCardAppCount; i++) { - snprintf(tmp, sizeof(tmp) / sizeof(tmp[0]), "%s v%u (%016lX)", fixedGameCardName[i], gameCardVersion[i], gameCardTitleID[i]); + u32 highestVersion = gameCardVersion[i]; + + // Check if our current gamecard has any bundled updates for this application. If so, use the highest update version available + if (gameCardPatchCount > 0 && gameCardPatchTitleID != NULL && gameCardPatchVersion != NULL) + { + for(j = 0; j < gameCardPatchCount; j++) + { + if (gameCardPatchTitleID[j] == (gameCardTitleID[i] | APPLICATION_PATCH_BITMASK) && gameCardPatchVersion[j] > highestVersion) highestVersion = gameCardPatchVersion[j]; + } + } + + snprintf(tmp, sizeof(tmp) / sizeof(tmp[0]), "%s v%u (%016lX)", fixedGameCardName[i], highestVersion, gameCardTitleID[i]); if ((strlen(fullname) + strlen(tmp) + 4) > strsize) { size_t fullname_len = strlen(fullname); strsize = (fullname_len + strlen(tmp) + 4); - fullnameTmp = (char*)realloc(fullname, strsize); + + fullnameTmp = realloc(fullname, strsize); if (fullnameTmp) { fullname = fullnameTmp; - memset(fullname + fullname_len, 0, strlen(tmp) + 1); + memset(fullname + fullname_len, 0, strlen(tmp) + 4); } else { free(fullname); fullname = NULL; @@ -1109,21 +1774,170 @@ char *generateDumpName() return fullname; } +char *generateNSPDumpName(nspDumpType selectedNspDumpType, u32 titleIndex) +{ + if (!gameCardAppCount || !fixedGameCardName || !gameCardTitleID || (selectedNspDumpType == DUMP_APP_NSP && !gameCardVersion) || (selectedNspDumpType == DUMP_PATCH_NSP && (!gameCardPatchCount || !gameCardPatchTitleID || !gameCardPatchVersion)) || (selectedNspDumpType == DUMP_ADDON_NSP && (!gameCardAddOnCount || !gameCardAddOnTitleID || !gameCardAddOnVersion))) 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)", fixedGameCardName[titleIndex], gameCardVersion[titleIndex], gameCardTitleID[titleIndex]); + } else + if (selectedNspDumpType == DUMP_PATCH_NSP) + { + for(app = 0; app < gameCardAppCount; app++) + { + if (gameCardPatchTitleID[titleIndex] == (gameCardTitleID[app] | APPLICATION_PATCH_BITMASK)) + { + foundApp = true; + break; + } + } + + if (foundApp) + { + snprintf(fullname, strsize, "%s v%u (%016lX) (UPD)", fixedGameCardName[app], gameCardPatchVersion[titleIndex], gameCardPatchTitleID[titleIndex]); + } else { + snprintf(fullname, strsize, "%016lX v%u (UPD)", gameCardPatchTitleID[titleIndex], gameCardPatchVersion[titleIndex]); + } + } else + if (selectedNspDumpType == DUMP_ADDON_NSP) + { + for(app = 0; app < gameCardAppCount; app++) + { + if ((gameCardAddOnTitleID[titleIndex] & APPLICATION_ADDON_BITMASK) == (gameCardTitleID[app] & APPLICATION_ADDON_BITMASK)) + { + foundApp = true; + break; + } + } + + if (foundApp) + { + snprintf(fullname, strsize, "%s v%u (%016lX) (DLC)", fixedGameCardName[app], gameCardAddOnVersion[titleIndex], gameCardAddOnTitleID[titleIndex]); + } else { + snprintf(fullname, strsize, "%016lX v%u (DLC)", gameCardAddOnTitleID[titleIndex], gameCardAddOnVersion[titleIndex]); + } + } else { + free(fullname); + fullname = NULL; + } + + return fullname; +} + +void retrieveDescriptionForPatchOrAddOn(u64 titleID, u32 version, bool addOn, const char *prefix) +{ + char versionStr[128] = {'\0'}; + convertTitleVersionToDecimal(version, versionStr, sizeof(versionStr)); + + if (!gameCardAppCount || !gameCardTitleID || !*gameCardTitleID || !gameCardName || !*gameCardName) + { + if (prefix) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%016lX v%s", prefix, titleID, versionStr); + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%016lX v%s", titleID, versionStr); + } + + return; + } + + u32 app; + bool foundApp = false; + + for(app = 0; app < gameCardAppCount; app++) + { + if ((!addOn && titleID == (gameCardTitleID[app] | APPLICATION_PATCH_BITMASK)) || (addOn && (titleID & APPLICATION_ADDON_BITMASK) == (gameCardTitleID[app] & APPLICATION_ADDON_BITMASK))) + { + foundApp = true; + break; + } + } + + if (foundApp) + { + if (prefix) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%s | %016lX v%s", prefix, gameCardName[app], titleID, versionStr); + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s | %016lX v%s", gameCardName[app], titleID, versionStr); + } + } else { + if (prefix) + { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%s%016lX v%s", prefix, titleID, versionStr); + } else { + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "%016lX v%s", titleID, versionStr); + } + } +} + void waitForButtonPress() { - uiDrawString("Press any button to continue", 0, breaks * font_height, 255, 255, 255); - + 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; @@ -1140,447 +1954,6 @@ void convertDataToHexString(const u8 *data, const u32 dataSize, char *outBuf, co } } -void convertNcaSizeToU64(const u8 size[0x6], u64 *out) -{ - if (!size || !out) return; - - u64 tmp = 0; - - tmp |= (((u64)size[5] << 40) & (u64)0xFF0000000000); - tmp |= (((u64)size[4] << 32) & (u64)0x00FF00000000); - tmp |= (((u64)size[3] << 24) & (u64)0x0000FF000000); - tmp |= (((u64)size[2] << 16) & (u64)0x000000FF0000); - tmp |= (((u64)size[1] << 8) & (u64)0x00000000FF00); - tmp |= ((u64)size[0] & (u64)0x0000000000FF); - - *out = tmp; -} - -bool loadNcaKeyset() -{ - // Keyset already loaded - if (nca_keyset.key_cnt > 0) return true; - - // Open keys file - FILE *keysFile = fopen(keysFilePath, "rb"); - if (!keysFile) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unable to open \"%s\" to retrieve NCA keyset!", keysFilePath); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return false; - } - - // Load keys - int ret = extkeys_initialize_keyset(&nca_keyset, keysFile); - fclose(keysFile); - - if (ret < 1) - { - if (ret == -1) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: unable to parse necessary keys from \"%s\"! (keys file empty?)", keysFilePath); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - } - - return false; - } - - return true; -} - -bool encryptNcaHeader(nca_header_t *input, u8 *outBuf, u64 outBufSize) -{ - if (!input || !outBuf || !outBufSize || outBufSize < NCA_FULL_HEADER_LENGTH || (bswap_32(input->magic) != NCA3_MAGIC && bswap_32(input->magic) != NCA2_MAGIC)) - { - uiDrawString("Error: invalid NCA header encryption parameters.", 0, breaks * font_height, 255, 0, 0); - return false; - } - - if (!loadNcaKeyset()) return false; - - u32 i; - aes_ctx_t *hdr_aes_ctx = NULL; - aes_ctx_t *aes_ctx = NULL; - - u8 crypto_type = (input->crypto_type2 > input->crypto_type ? input->crypto_type2 : input->crypto_type); - if (crypto_type) crypto_type--; - - aes_ctx = new_aes_ctx(nca_keyset.key_area_keys[crypto_type][input->kaek_ind], 16, AES_MODE_ECB); - if (!aes_ctx) return false; - - if (!aes_encrypt(aes_ctx, input->nca_keys, input->nca_keys, NCA_KEY_AREA_SIZE)) - { - free_aes_ctx(aes_ctx); - return false; - } - - free_aes_ctx(aes_ctx); - - hdr_aes_ctx = new_aes_ctx(nca_keyset.header_key, 32, AES_MODE_XTS); - if (!hdr_aes_ctx) return false; - - if (bswap_32(input->magic) == NCA3_MAGIC) - { - if (!aes_xts_encrypt(hdr_aes_ctx, outBuf, input, NCA_FULL_HEADER_LENGTH, 0, NCA_AES_XTS_SECTOR_SIZE)) - { - free_aes_ctx(hdr_aes_ctx); - return false; - } - } else - if (bswap_32(input->magic) == NCA2_MAGIC) - { - if (!aes_xts_encrypt(hdr_aes_ctx, outBuf, input, NCA_HEADER_LENGTH, 0, NCA_AES_XTS_SECTOR_SIZE)) - { - free_aes_ctx(hdr_aes_ctx); - return false; - } - - for(i = 0; i < NCA_SECTION_HEADER_CNT; i++) - { - if (!aes_xts_encrypt(hdr_aes_ctx, outBuf + NCA_HEADER_LENGTH + (i * NCA_SECTION_HEADER_LENGTH), &(input->fs_headers[i]), NCA_SECTION_HEADER_LENGTH, 0, NCA_AES_XTS_SECTOR_SIZE)) - { - free_aes_ctx(hdr_aes_ctx); - return false; - } - } - } - - free_aes_ctx(hdr_aes_ctx); - - return true; -} - -bool decryptNcaHeader(const char *ncaBuf, u64 ncaBufSize, nca_header_t *out) -{ - if (!ncaBuf || !ncaBufSize || ncaBufSize < NCA_FULL_HEADER_LENGTH) - { - uiDrawString("Error: invalid NCA header decryption parameters.", 0, breaks * font_height, 255, 0, 0); - return false; - } - - if (!loadNcaKeyset()) return false; - - u32 i; - aes_ctx_t *hdr_aes_ctx = NULL; - aes_ctx_t *aes_ctx = NULL; - - u8 crypto_type; - bool has_rights_id = false; - - u64 section_offset; - u64 section_size; - - hdr_aes_ctx = new_aes_ctx(nca_keyset.header_key, 32, AES_MODE_XTS); - if (!hdr_aes_ctx) return false; - - if (!aes_xts_decrypt(hdr_aes_ctx, out, ncaBuf, NCA_HEADER_LENGTH, 0, NCA_AES_XTS_SECTOR_SIZE)) - { - free_aes_ctx(hdr_aes_ctx); - return false; - } - - if (bswap_32(out->magic) == NCA3_MAGIC) - { - if (!aes_xts_decrypt(hdr_aes_ctx, out, ncaBuf, NCA_FULL_HEADER_LENGTH, 0, NCA_AES_XTS_SECTOR_SIZE)) - { - free_aes_ctx(hdr_aes_ctx); - return false; - } - } else - if (bswap_32(out->magic) == NCA2_MAGIC) - { - for(i = 0; i < NCA_SECTION_HEADER_CNT; i++) - { - if (out->fs_headers[i]._0x148[0] != 0 || memcmp(out->fs_headers[i]._0x148, out->fs_headers[i]._0x148 + 1, 0xB7)) - { - if (!aes_xts_decrypt(hdr_aes_ctx, &(out->fs_headers[i]), ncaBuf + NCA_HEADER_LENGTH + (i * NCA_SECTION_HEADER_LENGTH), NCA_SECTION_HEADER_LENGTH, 0, NCA_AES_XTS_SECTOR_SIZE)) - { - free_aes_ctx(hdr_aes_ctx); - return false; - } - } else { - memset(&(out->fs_headers[i]), 0, sizeof(nca_fs_header_t)); - } - } - } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid NCA magic word! Wrong keys? (0x%08X)", out->magic); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - free_aes_ctx(hdr_aes_ctx); - return false; - } - - free_aes_ctx(hdr_aes_ctx); - - crypto_type = (out->crypto_type2 > out->crypto_type ? out->crypto_type2 : out->crypto_type); - if (crypto_type) crypto_type--; - - for(i = 0; i < 0x10; i++) - { - if (out->rights_id[i] != 0) - { - has_rights_id = true; - break; - } - } - - if (has_rights_id) - { - uiDrawString("Error: Rights ID field in NCA header not empty!", 0, breaks * font_height, 255, 0, 0); - return false; - } - - section_offset = (out->section_entries[0].media_start_offset * MEDIA_UNIT_SIZE); - section_size = ((out->section_entries[0].media_end_offset * MEDIA_UNIT_SIZE) - section_offset); - - if (!section_offset || !section_size || section_offset < NCA_FULL_HEADER_LENGTH) - { - uiDrawString("Error: invalid size/offset for NCA section #0!", 0, breaks * font_height, 255, 0, 0); - return false; - } - - aes_ctx = new_aes_ctx(nca_keyset.key_area_keys[crypto_type][out->kaek_ind], 16, AES_MODE_ECB); - if (!aes_ctx) return false; - - if (!aes_decrypt(aes_ctx, out->nca_keys, out->nca_keys, NCA_KEY_AREA_SIZE)) - { - free_aes_ctx(aes_ctx); - return false; - } - - free_aes_ctx(aes_ctx); - - return true; -} - -bool decryptCnmtNca(char *ncaBuf, u64 ncaBufSize) -{ - u32 i; - nca_header_t dec_header; - aes_ctx_t *aes_ctx = NULL; - - u8 crypto_type; - - u64 section_offset; - u64 section_size; - char *section_data = NULL; - - pfs0_header nca_pfs0_header; - - if (!decryptNcaHeader(ncaBuf, ncaBufSize, &dec_header)) return false; - - if (dec_header.fs_headers[0].partition_type != NCA_FS_HEADER_PARTITION_PFS0 || dec_header.fs_headers[0].fs_type != NCA_FS_HEADER_FSTYPE_PFS0) - { - uiDrawString("Error: CNMT NCA section #0 doesn't hold a PFS0 partition.", 0, breaks * font_height, 255, 0, 0); - return false; - } - - if (!dec_header.fs_headers[0].pfs0_superblock.pfs0_size) - { - uiDrawString("Error: invalid size for PFS0 partition in CNMT NCA section #0.", 0, breaks * font_height, 255, 0, 0); - return false; - } - - crypto_type = (dec_header.crypto_type2 > dec_header.crypto_type ? dec_header.crypto_type2 : dec_header.crypto_type); - if (crypto_type) crypto_type--; - - section_offset = (dec_header.section_entries[0].media_start_offset * MEDIA_UNIT_SIZE); - section_size = ((dec_header.section_entries[0].media_end_offset * MEDIA_UNIT_SIZE) - section_offset); - - section_data = (char*)calloc(section_size, sizeof(char)); - if (!section_data) - { - uiDrawString("Error: unable to allocate memory for the decrypted CNMT NCA section #0.", 0, breaks * font_height, 255, 0, 0); - return false; - } - - if (dec_header.fs_headers[0].crypt_type != NCA_FS_HEADER_CRYPT_NONE) - { - if (dec_header.fs_headers[0].crypt_type == NCA_FS_HEADER_CRYPT_CTR || dec_header.fs_headers[0].crypt_type == NCA_FS_HEADER_CRYPT_BKTR) - { - aes_ctx = new_aes_ctx(dec_header.nca_keys[2], 16, AES_MODE_CTR); - if (!aes_ctx) - { - free(section_data); - return false; - } - - unsigned char ctr[0x10]; - u64 ofs = (section_offset >> 4); - - for(i = 0; i < 0x8; i++) - { - ctr[i] = dec_header.fs_headers[0].section_ctr[0x08 - i - 1]; - ctr[0x10 - i - 1] = (unsigned char)(ofs & 0xFF); - ofs >>= 8; - } - - if (!aes_setiv(aes_ctx, ctr, 0x10)) - { - free_aes_ctx(aes_ctx); - free(section_data); - return false; - } - - if (!aes_decrypt(aes_ctx, section_data, ncaBuf + section_offset, section_size)) - { - free_aes_ctx(aes_ctx); - free(section_data); - return false; - } - - free_aes_ctx(aes_ctx); - } else - if (dec_header.fs_headers[0].crypt_type == NCA_FS_HEADER_CRYPT_XTS) - { - aes_ctx = new_aes_ctx(dec_header.nca_keys, 32, AES_MODE_XTS); - if (!aes_ctx) - { - free(section_data); - return false; - } - - if (!aes_xts_decrypt(aes_ctx, section_data, ncaBuf + section_offset, section_size, 0, NCA_AES_XTS_SECTOR_SIZE)) - { - free_aes_ctx(aes_ctx); - free(section_data); - return false; - } - - free_aes_ctx(aes_ctx); - } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid crypt type for CNMT NCA section #0! (0x%02X)", dec_header.fs_headers[0].crypt_type); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - free(section_data); - return false; - } - } else { - memcpy(section_data, ncaBuf + section_offset, section_size); - } - - memcpy(&nca_pfs0_header, section_data + dec_header.fs_headers[0].pfs0_superblock.hash_table_offset + dec_header.fs_headers[0].pfs0_superblock.pfs0_offset, sizeof(pfs0_header)); - - if (bswap_32(nca_pfs0_header.magic) != PFS0_MAGIC) - { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: invalid magic word for CNMT NCA section #0 PFS0 partition! Wrong keys? (0x%08X)", nca_pfs0_header.magic); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - free(section_data); - return false; - } - - if (!nca_pfs0_header.file_cnt || !nca_pfs0_header.str_table_size) - { - uiDrawString("Error: CNMT NCA section #0 PFS0 partition is empty! Wrong keys? (0x%08X)", 0, breaks * font_height, 255, 0, 0); - free(section_data); - return false; - } - - // Replace input buffer data in-place - memset(ncaBuf, 0, ncaBufSize); - memcpy(ncaBuf, &dec_header, sizeof(nca_header_t)); - memcpy(ncaBuf + section_offset, section_data, section_size); - - /*FILE *nca_file = fopen("sdmc:/decrypted_cnmt_nca.bin", "wb"); - if (nca_file) - { - fwrite(ncaBuf, 1, ncaBufSize, nca_file); - fclose(nca_file); - }*/ - - free(section_data); - - return true; -} - -bool calculateSHA256(const u8 *data, const u32 dataSize, u8 out[32]) -{ - int ret; - mbedtls_sha256_context sha256_ctx; - - mbedtls_sha256_init(&sha256_ctx); - - ret = mbedtls_sha256_starts_ret(&sha256_ctx, 0); - if (ret < 0) - { - mbedtls_sha256_free(&sha256_ctx); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to start SHA-256 calculation! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return false; - } - - ret = mbedtls_sha256_update_ret(&sha256_ctx, data, dataSize); - if (ret < 0) - { - mbedtls_sha256_free(&sha256_ctx); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to update SHA-256 calculation! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return false; - } - - ret = mbedtls_sha256_finish_ret(&sha256_ctx, out); - if (ret < 0) - { - mbedtls_sha256_free(&sha256_ctx); - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to finish SHA-256 calculation! (%d)", ret); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); - return false; - } - - mbedtls_sha256_free(&sha256_ctx); - - return true; -} - -void generateCnmtMetadataXml(cnmt_xml_program_info *xml_program_info, cnmt_xml_content_info *xml_content_info, char *out) -{ - if (!xml_program_info || !xml_content_info || !xml_program_info->nca_cnt || !out) return; - - u32 i; - char tmp[NAME_BUF_LEN] = {'\0'}; - - const char *contentTypes[] = { "Meta", "Program", "Data", "Control", "HtmlDocument", "LegalInformation", "DeltaFragment" }; - const u32 contentTypesCnt = (sizeof(contentTypes) / sizeof(contentTypes[0])); - - sprintf(out, "\r\n" \ - "\r\n" \ - " Application\r\n" \ - " 0x%016lx\r\n" \ - " %u\r\n" \ - " %u\r\n", \ - xml_program_info->title_id, \ - xml_program_info->version, \ - xml_program_info->required_dl_sysver); - - for(i = 0; i < xml_program_info->nca_cnt; i++) - { - sprintf(tmp, " \r\n" \ - " %s\r\n" \ - " %s\r\n" \ - " %lu\r\n" \ - " %s\r\n" \ - " %u\r\n" \ - " \r\n", \ - (xml_content_info[i].type < contentTypesCnt ? contentTypes[xml_content_info[i].type] : "Unknown"), \ - xml_content_info[i].nca_id_str, \ - xml_content_info[i].size, \ - xml_content_info[i].hash_str, \ - xml_content_info[i].keyblob); \ - - strcat(out, tmp); - } - - sprintf(tmp, " %s\r\n" \ - " %u\r\n" \ - " %u\r\n" \ - " 0x%016lx\r\n" \ - "", \ - xml_program_info->digest_str, \ - xml_program_info->min_keyblob, \ - xml_program_info->min_sysver, \ - xml_program_info->patch_tid); - - strcat(out, tmp); -} - void addStringToFilenameBuffer(const char *string, char **nextFilename) { filenames[filenamesCount++] = *nextFilename; @@ -1590,13 +1963,13 @@ void addStringToFilenameBuffer(const char *string, char **nextFilename) void removeDirectory(const char *path) { - struct dirent* ent; + struct dirent *ent; char cur_path[NAME_BUF_LEN] = {'\0'}; DIR *dir = opendir(path); if (dir) { - while ((ent = readdir(dir)) != NULL) + 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; @@ -1606,7 +1979,7 @@ void removeDirectory(const char *path) { removeDirectory(cur_path); } else { - remove(cur_path); + unlink(cur_path); } } @@ -1677,30 +2050,30 @@ bool parseNSWDBRelease(xmlDocPtr doc, xmlNodePtr cur, u64 gc_tid, u32 crc) /*if (xmlImageSize && xmlTitleID && strlen(xmlReleaseName)) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "XML Image Size: %u.", xmlImageSize); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + 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, 0, breaks * font_height, 255, 255, 255); + 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, 0, breaks * font_height, 255, 255, 255); + 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, 0, breaks * font_height, 255, 255, 255); + 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, 0, breaks * font_height, 0, 255, 0); + 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, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } breaks++; @@ -1743,7 +2116,7 @@ void gameCardDumpNSWDBCheck(u32 crc) 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"), gameCardTitleID[i]); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++; uiRefreshDisplay(); @@ -1762,7 +2135,7 @@ void gameCardDumpNSWDBCheck(u32 crc) if (!found) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "No checksum matches found in XML document for Title ID \"%016lX\"!", gameCardTitleID[i]); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); if ((i + 1) < gameCardAppCount) breaks += 2; } else { breaks--; @@ -1770,7 +2143,7 @@ void gameCardDumpNSWDBCheck(u32 crc) } } else { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "No records with Title ID \"%016lX\" found within the XML document!", gameCardTitleID[i]); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); if ((i + 1) < gameCardAppCount) breaks += 2; } } @@ -1780,11 +2153,11 @@ void gameCardDumpNSWDBCheck(u32 crc) if (!found) { breaks++; - uiDrawString("This could either be a bad dump or an undumped cartridge.", 0, breaks * font_height, 255, 0, 0); + 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, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } } @@ -1816,7 +2189,7 @@ static size_t writeCurlBuffer(char *buffer, size_t size, size_t number_of_items, if (result_sz == 0 || !result_buf) { result_sz = 0x1000; - result_buf = (char*)malloc(result_sz); + result_buf = malloc(result_sz); if (!result_buf) return 0; } @@ -1830,7 +2203,7 @@ static size_t writeCurlBuffer(char *buffer, size_t size, size_t number_of_items, if (need_realloc) { - char *new_buf = (char*)realloc(result_buf, result_sz); + char *new_buf = realloc(result_buf, result_sz); if (!new_buf) return 0; result_buf = new_buf; } @@ -1858,12 +2231,12 @@ void updateNSWDBXml() if (nswdbXml) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Downloading XML database from \"%s\", please wait...", nswReleasesXmlUrl); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + 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 HOME button. Doing so could corrupt the SD card filesystem.", 0, breaks * font_height, 255, 0, 0); + 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; } @@ -1890,36 +2263,36 @@ void updateNSWDBXml() 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, 0, breaks * font_height, 0, 255, 0); + 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, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } fclose(nswdbXml); if (success) { - remove(nswReleasesXmlPath); + unlink(nswReleasesXmlPath); rename(nswReleasesXmlTmpPath, nswReleasesXmlPath); } else { - remove(nswReleasesXmlTmpPath); + unlink(nswReleasesXmlTmpPath); } } else { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to open \"%s\" in write mode!", nswReleasesXmlTmpPath); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + 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!", 0, breaks * font_height, 255, 0, 0); + 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, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } breaks += 2; @@ -2037,6 +2410,13 @@ int versionNumCmp(char *ver1, char *ver2) 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; @@ -2046,6 +2426,7 @@ void updateApplication() bool success = false; struct json_object *jobj, *name, *assets; FILE *gcDumpToolNro = NULL; + char nroPath[NAME_BUF_LEN] = {'\0'}; if (R_SUCCEEDED(result = networkInit())) { @@ -2053,7 +2434,7 @@ void updateApplication() if (curl) { snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Requesting latest release information from \"%s\"...", githubReleasesApiUrl); - uiDrawString(strbuf, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++; uiRefreshDisplay(); @@ -2079,7 +2460,7 @@ void updateApplication() 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, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++; uiRefreshDisplay(); @@ -2092,7 +2473,7 @@ void updateApplication() 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, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++; uiRefreshDisplay(); @@ -2117,21 +2498,23 @@ void updateApplication() 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, 0, breaks * font_height, 255, 255, 255); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 255, 255); breaks++; - uiDrawString("Please wait...", 0, breaks * font_height, 255, 255, 255); + 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 HOME button. Doing so could corrupt the SD card filesystem.", 0, breaks * font_height, 255, 0, 0); + 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(); - gcDumpToolNro = fopen(gcDumpToolTmpPath, "wb"); + snprintf(nroPath, sizeof(nroPath) / sizeof(nroPath[0]), "%s.tmp", (strlen(appLaunchPath) ? appLaunchPath : gcDumpToolPath)); + + gcDumpToolNro = fopen(nroPath, "wb"); if (gcDumpToolNro) { curl_easy_reset(curl); @@ -2160,64 +2543,67 @@ void updateApplication() success = true; snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Successfully downloaded %.0lf bytes!", size); - uiDrawString(strbuf, 0, breaks * font_height, 0, 255, 0); + 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.", 0, breaks * font_height, 0, 255, 0); + 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, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } fclose(gcDumpToolNro); if (success) { - remove(gcDumpToolPath); - rename(gcDumpToolTmpPath, gcDumpToolPath); + snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), nroPath); + nroPath[strlen(nroPath) - 4] = '\0'; + + unlink(nroPath); + rename(strbuf, nroPath); } else { - remove(gcDumpToolTmpPath); + unlink(nroPath); } } else { - snprintf(strbuf, sizeof(strbuf) / sizeof(strbuf[0]), "Error: failed to open \"%s\" in write mode!", gcDumpToolTmpPath); - uiDrawString(strbuf, 0, breaks * font_height, 255, 0, 0); + 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!", 0, breaks * font_height, 255, 0, 0); + 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!", 0, breaks * font_height, 255, 0, 0); + 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!", 0, breaks * font_height, 255, 0, 0); + 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!", 0, breaks * font_height, 255, 255, 255); + 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!", 0, breaks * font_height, 255, 0, 0); + 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!", 0, breaks * font_height, 255, 0, 0); + 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, 0, breaks * font_height, 255, 0, 0); + 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!", 0, breaks * font_height, 255, 0, 0); + 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, 0, breaks * font_height, 255, 0, 0); + uiDrawString(strbuf, 8, (breaks * (font_height + (font_height / 4))) + (font_height / 8), 255, 0, 0); } breaks += 2; diff --git a/source/util.h b/source/util.h index aa7ac35..4cb04cc 100644 --- a/source/util.h +++ b/source/util.h @@ -4,8 +4,7 @@ #define __UTIL_H__ #include - -#define APP_VERSION "1.0.8" +#include "nca.h" #define KiB (1024.0) #define MiB (1024.0 * KiB) @@ -16,15 +15,20 @@ #define SOCK_BUFFERSIZE 65536 #define META_DB_REGULAR_APPLICATION 0x80 +#define META_DB_PATCH 0x81 +#define META_DB_ADDON 0x82 -#define FILENAME_BUFFER_SIZE (1024 * 512) // 512 KiB -#define FILENAME_MAX_CNT 2048 +#define APPLICATION_PATCH_BITMASK (u64)0x800 +#define APPLICATION_ADDON_BITMASK (u64)0xFFFFFFFFFFFF0000 + +#define FILENAME_MAX_CNT 20000 +#define FILENAME_BUFFER_SIZE (512 * FILENAME_MAX_CNT) // 10000 KiB #define NACP_APPNAME_LEN 0x200 #define NACP_AUTHOR_LEN 0x100 #define VERSION_STR_LEN 0x40 -#define GAMECARD_WAIT_TIME 3 // 3 seconds +#define GAMECARD_WAIT_TIME 3 // 3 seconds #define GAMECARD_HEADER_SIZE 0x200 #define GAMECARD_SIZE_ADDR 0x10D @@ -32,40 +36,15 @@ #define HFS0_OFFSET_ADDR 0x130 #define HFS0_SIZE_ADDR 0x138 -#define HFS0_MAGIC 0x48465330 // "HFS0" +#define HFS0_MAGIC (u32)0x48465330 // "HFS0" #define HFS0_FILE_COUNT_ADDR 0x04 #define HFS0_STR_TABLE_SIZE_ADDR 0x08 #define HFS0_ENTRY_TABLE_ADDR 0x10 -#define PFS0_MAGIC 0x50465330 // "PFS0" - #define MEDIA_UNIT_SIZE 0x200 -#define NCA3_MAGIC 0x4E434133 // "NCA3" -#define NCA2_MAGIC 0x4E434132 // "NCA2" - -#define NCA_HEADER_LENGTH 0x400 -#define NCA_SECTION_HEADER_LENGTH 0x200 -#define NCA_SECTION_HEADER_CNT 4 -#define NCA_FULL_HEADER_LENGTH (NCA_HEADER_LENGTH + (NCA_SECTION_HEADER_LENGTH * NCA_SECTION_HEADER_CNT)) - -#define NCA_AES_XTS_SECTOR_SIZE 0x200 - -#define NCA_KEY_AREA_KEY_CNT 4 -#define NCA_KEA_AREA_KEY_SIZE 0x10 -#define NCA_KEY_AREA_SIZE (NCA_KEY_AREA_KEY_CNT * NCA_KEA_AREA_KEY_SIZE) - -#define NCA_FS_HEADER_PARTITION_PFS0 0x01 -#define NCA_FS_HEADER_FSTYPE_PFS0 0x02 -#define NCA_FS_HEADER_CRYPT_NONE 0x01 -#define NCA_FS_HEADER_CRYPT_XTS 0x02 -#define NCA_FS_HEADER_CRYPT_CTR 0x03 -#define NCA_FS_HEADER_CRYPT_BKTR 0x04 - -#define NCA_CNMT_DIGEST_SIZE 0x20 - -#define GAMECARD_TYPE1_PARTITION_CNT 3 // "update" (0), "normal" (1), "update" (2) -#define GAMECARD_TYPE2_PARTITION_CNT 4 // "update" (0), "logo" (1), "normal" (2), "update" (3) +#define GAMECARD_TYPE1_PARTITION_CNT 3 // "update" (0), "normal" (1), "update" (2) +#define GAMECARD_TYPE2_PARTITION_CNT 4 // "update" (0), "logo" (1), "normal" (2), "update" (3) #define GAMECARD_TYPE(x) ((x) == GAMECARD_TYPE1_PARTITION_CNT ? "Type 0x01" : ((x) == GAMECARD_TYPE2_PARTITION_CNT ? "Type 0x02" : "Unknown")) #define GAMECARD_TYPE1_PART_NAMES(x) ((x) == 0 ? "Update" : ((x) == 1 ? "Normal" : ((x) == 2 ? "Secure" : "Unknown"))) #define GAMECARD_TYPE2_PART_NAMES(x) ((x) == 0 ? "Update" : ((x) == 1 ? "Logo" : ((x) == 2 ? "Normal" : ((x) == 3 ? "Secure" : "Unknown")))) @@ -106,11 +85,13 @@ #define SYSUPDATE_701 (u32)469827614 #define SYSUPDATE_800 (u32)536871442 +#define NACP_ICON_SQUARE_DIMENSION 256 +#define NACP_ICON_DOWNSCALED 96 + #define bswap_32(a) ((((a) << 24) & 0xff000000) | (((a) << 8) & 0xff0000) | (((a) >> 8) & 0xff00) | (((a) >> 24) & 0xff)) #define round_up(x, y) ((x) + (((y) - ((x) % (y))) % (y))) // Aligns 'x' bytes to a 'y' bytes boundary -typedef struct -{ +typedef struct { u64 file_offset; u64 file_size; u32 filename_offset; @@ -119,136 +100,20 @@ typedef struct u8 hashed_region_sha256[0x20]; } PACKED hfs0_entry_table; -typedef struct -{ - u32 magic; - u32 file_cnt; - u32 str_table_size; - u32 reserved; -} PACKED pfs0_header; - -typedef struct -{ - u64 file_offset; - u64 file_size; - u32 filename_offset; - u32 reserved; -} PACKED pfs0_entry_table; - typedef struct { - u32 media_start_offset; - u32 media_end_offset; - u8 _0x8[0x8]; /* Padding. */ -} PACKED nca_section_entry_t; - -typedef struct { - u8 master_hash[0x20]; /* SHA-256 hash of the hash table. */ - u32 block_size; /* In bytes. */ - u32 always_2; - u64 hash_table_offset; /* Normally zero. */ - u64 hash_table_size; - u64 pfs0_offset; - u64 pfs0_size; - u8 _0x48[0xF0]; -} PACKED pfs0_superblock_t; - -/* NCA FS header. */ -typedef struct { - u8 _0x0; - u8 _0x1; - u8 partition_type; - u8 fs_type; - u8 crypt_type; - u8 _0x5[0x3]; - pfs0_superblock_t pfs0_superblock; /* FS-specific superblock. Size = 0x138. */ - union { - u8 section_ctr[0x8]; - struct { - u32 section_ctr_low; - u32 section_ctr_high; - }; - }; - u8 _0x148[0xB8]; /* Padding. */ -} PACKED nca_fs_header_t; - -/* Nintendo content archive header. */ -typedef struct { - u8 fixed_key_sig[0x100]; /* RSA-PSS signature over header with fixed key. */ - u8 npdm_key_sig[0x100]; /* RSA-PSS signature over header with key in NPDM. */ - u32 magic; - u8 distribution; /* System vs gamecard. */ - u8 content_type; - u8 crypto_type; /* Which keyblob (field 1) */ - u8 kaek_ind; /* Which kaek index? */ - u64 nca_size; /* Entire archive size. */ - u64 title_id; - u8 _0x218[0x4]; /* Padding. */ - union { - u32 sdk_version; /* What SDK was this built with? */ - struct { - u8 sdk_revision; - u8 sdk_micro; - u8 sdk_minor; - u8 sdk_major; - }; - }; - u8 crypto_type2; /* Which keyblob (field 2) */ - u8 _0x221[0xF]; /* Padding. */ - u8 rights_id[0x10]; /* Rights ID (for titlekey crypto). */ - nca_section_entry_t section_entries[4]; /* Section entry metadata. */ - u8 section_hashes[4][0x20]; /* SHA-256 hashes for each section header. */ - u8 nca_keys[4][0x10]; /* Key area (encrypted, but later decrypted by decryptNcaHeader()) */ - u8 _0x340[0xC0]; /* Padding. */ - nca_fs_header_t fs_headers[4]; /* FS section headers. */ -} PACKED nca_header_t; - -typedef struct { - u64 title_id; - u32 version; - u8 type; - u8 unk1; - u16 table_offset; - u16 content_records_cnt; - u16 meta_records_cnt; - u8 unk2[12]; -} PACKED cnmt_header; - -typedef struct { - u64 patch_tid; - u64 min_sysver; -} PACKED cnmt_application_header; - -typedef struct { - u8 hash[0x20]; - u8 nca_id[0x10]; - u8 size[6]; - u8 type; - u8 unk; -} PACKED cnmt_content_record; - -typedef struct { - u8 type; - u64 title_id; - u32 version; - u32 required_dl_sysver; - u32 nca_cnt; - u8 digest[32]; - char digest_str[65]; - u8 min_keyblob; - u32 min_sysver; - u64 patch_tid; -} PACKED cnmt_xml_program_info; - -typedef struct { - u8 type; - u8 nca_id[16]; - char nca_id_str[33]; - u64 size; - u8 hash[32]; - char hash_str[65]; - u8 keyblob; - u8 encrypted_header_mod[NCA_FULL_HEADER_LENGTH]; -} PACKED cnmt_xml_content_info; + int line_offset; + u64 totalSize; + char totalSizeStr[32]; + u64 curOffset; + char curOffsetStr[32]; + u8 progress; + u64 start; + u64 now; + u64 remainingTime; + char etaInfo[32]; + double lastSpeed; + double averageSpeed; +} PACKED progress_ctx_t; bool isGameCardInserted(); @@ -256,7 +121,9 @@ void fsGameCardDetectionThreadFunc(void *arg); void delay(u8 seconds); -void convertTitleVersionToDecimal(u32 version, char *versionBuf, int versionBufSize); +void formatETAString(u64 curTime, char *output, u32 outSize); + +void convertTitleVersionToDecimal(u32 version, char *versionBuf, size_t versionBufSize); void removeIllegalCharacters(char *name); @@ -264,38 +131,46 @@ void strtrim(char *str); void freeStringsPtr(char **var); +void initRomFsContext(); + +void freeRomFsContext(); + void freeGameCardInfo(); void loadGameCardInfo(); -bool getHfs0EntryDetails(char *hfs0Header, u64 hfs0HeaderOffset, u64 hfs0HeaderSize, u32 num_entries, u32 entry_idx, bool isRoot, u32 partitionIndex, u64 *out_offset, u64 *out_size); +bool getHfs0EntryDetails(u8 *hfs0Header, u64 hfs0HeaderOffset, u64 hfs0HeaderSize, u32 num_entries, u32 entry_idx, bool isRoot, u32 partitionIndex, u64 *out_offset, u64 *out_size); bool getPartitionHfs0Header(u32 partition); bool getHfs0FileList(u32 partition); +u8 *getPartitionHfs0FileByName(FsStorage *gameCardStorage, const char *filename, u64 *outSize); + +bool calculateRomFsExtractedDataSize(u64 *out); + +bool readProgramNcaRomFs(u32 appIndex); + +bool getRomFsFileList(u32 dir_offset); + int getSdCardFreeSpace(u64 *out); void convertSize(u64 size, char *out, int bufsize); -char *generateDumpName(); +char *generateDumpFullName(); + +char *generateNSPDumpName(nspDumpType selectedNspDumpType, u32 titleIndex); + +void retrieveDescriptionForPatchOrAddOn(u64 titleID, u32 version, bool addOn, const char *prefix); void waitForButtonPress(); +void printProgressBar(progress_ctx_t *progressCtx, bool calcData, u64 chunkSize); + +void setProgressBarError(progress_ctx_t *progressCtx); + void convertDataToHexString(const u8 *data, const u32 dataSize, char *outBuf, const u32 outBufSize); -void convertNcaSizeToU64(const u8 size[0x6], u64 *out); - -bool encryptNcaHeader(nca_header_t *input, u8 *outBuf, u64 outBufSize); - -bool decryptNcaHeader(const char *ncaBuf, u64 ncaBufSize, nca_header_t *out); - -bool decryptCnmtNca(char *ncaBuf, u64 ncaBufSize); - -bool calculateSHA256(const u8 *data, const u32 dataSize, u8 out[32]); - -void generateCnmtMetadataXml(cnmt_xml_program_info *xml_program_info, cnmt_xml_content_info *xml_content_info, char *out); - void addStringToFilenameBuffer(const char *string, char **nextFilename); void removeDirectory(const char *path);