2020-11-08 19:08:30 +00:00
/*
* main . c
*
* Copyright ( c ) 2020 , DarkMatterCore < pabloacurielz @ gmail . com > .
*
* This file is part of nxdumptool ( https : //github.com/DarkMatterCore/nxdumptool).
*
2021-03-25 19:26:58 +00:00
* nxdumptool is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
2020-11-08 19:08:30 +00:00
*
2021-03-25 19:26:58 +00:00
* nxdumptool is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
2020-11-08 19:08:30 +00:00
*
* You should have received a copy of the GNU General Public License
2021-03-25 19:26:58 +00:00
* along with this program . If not , see < https : //www.gnu.org/licenses/>.
2020-11-08 19:08:30 +00:00
*/
2021-03-26 04:35:14 +00:00
# include "nxdt_utils.h"
2020-11-08 19:08:30 +00:00
# include "bktr.h"
# include "gamecard.h"
# include "title.h"
# define BLOCK_SIZE 0x800000
2021-03-08 11:11:28 +00:00
int g_argc = 0 ;
char * * g_argv = NULL ;
const char * g_appLaunchPath = NULL ;
2020-11-08 19:08:30 +00:00
static Mutex g_fileMutex = 0 ;
static CondVar g_readCondvar = 0 , g_writeCondvar = 0 ;
typedef struct
{
FILE * fd ;
RomFileSystemContext * romfs_ctx ;
BktrContext * bktr_ctx ;
void * data ;
size_t data_size ;
size_t data_written ;
size_t total_size ;
bool read_error ;
bool write_error ;
bool transfer_cancelled ;
} ThreadSharedData ;
static void consolePrint ( const char * text , . . . )
{
va_list v ;
va_start ( v , text ) ;
vfprintf ( stdout , text , v ) ;
va_end ( v ) ;
consoleUpdate ( NULL ) ;
}
static void read_thread_func ( void * arg )
{
ThreadSharedData * shared_data = ( ThreadSharedData * ) arg ;
if ( ! shared_data | | shared_data - > fd | | ! shared_data - > data | | ! shared_data - > total_size | | ( ! shared_data - > romfs_ctx & & ! shared_data - > bktr_ctx ) )
{
shared_data - > read_error = true ;
goto end ;
}
u8 * buf = malloc ( BLOCK_SIZE ) ;
if ( ! buf )
{
shared_data - > read_error = true ;
goto end ;
}
u64 file_table_offset = 0 ;
u64 file_table_size = ( shared_data - > bktr_ctx ? shared_data - > bktr_ctx - > patch_romfs_ctx . file_table_size : shared_data - > romfs_ctx - > file_table_size ) ;
RomFileSystemFileEntry * file_entry = NULL ;
char path [ FS_MAX_PATH ] = { 0 } ;
sprintf ( path , " sdmc:/romfs " ) ;
while ( file_table_offset < file_table_size )
{
/* Check if the transfer has been cancelled by the user. */
if ( shared_data - > transfer_cancelled )
{
condvarWakeAll ( & g_writeCondvar ) ;
break ;
}
/* Wait until the previous file data chunk has been written. */
mutexLock ( & g_fileMutex ) ;
if ( shared_data - > data_size & & ! shared_data - > write_error ) condvarWait ( & g_readCondvar , & g_fileMutex ) ;
mutexUnlock ( & g_fileMutex ) ;
if ( shared_data - > write_error ) break ;
/* Close file. */
if ( shared_data - > fd )
{
fclose ( shared_data - > fd ) ;
shared_data - > fd = NULL ;
}
/* Retrieve RomFS file entry information. */
if ( shared_data - > bktr_ctx )
{
shared_data - > read_error = ( ! ( file_entry = bktrGetFileEntryByOffset ( shared_data - > bktr_ctx , file_table_offset ) ) | | \
! bktrGeneratePathFromFileEntry ( shared_data - > bktr_ctx , file_entry , path + 11 , FS_MAX_PATH - 11 , RomFileSystemPathIllegalCharReplaceType_KeepAsciiCharsOnly ) ) ;
} else {
shared_data - > read_error = ( ! ( file_entry = romfsGetFileEntryByOffset ( shared_data - > romfs_ctx , file_table_offset ) ) | | \
! romfsGeneratePathFromFileEntry ( shared_data - > romfs_ctx , file_entry , path + 11 , FS_MAX_PATH - 11 , RomFileSystemPathIllegalCharReplaceType_KeepAsciiCharsOnly ) ) ;
}
if ( shared_data - > read_error )
{
condvarWakeAll ( & g_writeCondvar ) ;
break ;
}
/* Create directory tree. */
utilsCreateDirectoryTree ( path , false ) ;
/* Create file. */
shared_data - > read_error = ( ( shared_data - > fd = fopen ( path , " wb " ) ) = = NULL ) ;
if ( shared_data - > read_error )
{
condvarWakeAll ( & g_writeCondvar ) ;
break ;
}
for ( u64 offset = 0 , blksize = BLOCK_SIZE ; offset < file_entry - > size ; offset + = blksize )
{
if ( blksize > ( file_entry - > size - offset ) ) blksize = ( file_entry - > size - offset ) ;
/* Check if the transfer has been cancelled by the user. */
if ( shared_data - > transfer_cancelled )
{
condvarWakeAll ( & g_writeCondvar ) ;
break ;
}
/* Read current file data chunk. */
shared_data - > read_error = ( shared_data - > bktr_ctx ? ! bktrReadFileEntryData ( shared_data - > bktr_ctx , file_entry , buf , blksize , offset ) : \
! romfsReadFileEntryData ( shared_data - > romfs_ctx , file_entry , buf , blksize , offset ) ) ;
if ( shared_data - > read_error )
{
condvarWakeAll ( & g_writeCondvar ) ;
break ;
}
/* Wait until the previous file data chunk has been written. */
mutexLock ( & g_fileMutex ) ;
if ( shared_data - > data_size & & ! shared_data - > write_error ) condvarWait ( & g_readCondvar , & g_fileMutex ) ;
if ( shared_data - > write_error )
{
mutexUnlock ( & g_fileMutex ) ;
break ;
}
/* Copy current file data chunk to the shared buffer. */
memcpy ( shared_data - > data , buf , blksize ) ;
shared_data - > data_size = blksize ;
/* Wake up the write thread to continue writing data. */
mutexUnlock ( & g_fileMutex ) ;
condvarWakeAll ( & g_writeCondvar ) ;
}
if ( shared_data - > read_error | | shared_data - > write_error | | shared_data - > transfer_cancelled ) break ;
file_table_offset + = ALIGN_UP ( sizeof ( RomFileSystemFileEntry ) + file_entry - > name_length , 4 ) ;
}
/* Wait until the previous file data chunk has been written. */
mutexLock ( & g_fileMutex ) ;
if ( shared_data - > data_size & & ! shared_data - > write_error ) condvarWait ( & g_readCondvar , & g_fileMutex ) ;
mutexUnlock ( & g_fileMutex ) ;
if ( shared_data - > fd )
{
fclose ( shared_data - > fd ) ;
shared_data - > fd = NULL ;
}
if ( ( shared_data - > read_error | | shared_data - > write_error | | shared_data - > transfer_cancelled ) & & * path ) remove ( path ) ;
free ( buf ) ;
end :
threadExit ( ) ;
}
static void write_thread_func ( void * arg )
{
ThreadSharedData * shared_data = ( ThreadSharedData * ) arg ;
if ( ! shared_data | | ! shared_data - > data )
{
shared_data - > write_error = true ;
goto end ;
}
while ( shared_data - > data_written < shared_data - > total_size )
{
/* Wait until the current file data chunk has been read */
mutexLock ( & g_fileMutex ) ;
if ( ! shared_data - > data_size & & ! shared_data - > read_error ) condvarWait ( & g_writeCondvar , & g_fileMutex ) ;
if ( shared_data - > read_error | | shared_data - > transfer_cancelled | | ! shared_data - > fd )
{
mutexUnlock ( & g_fileMutex ) ;
break ;
}
/* Write current file data chunk */
shared_data - > write_error = ( fwrite ( shared_data - > data , 1 , shared_data - > data_size , shared_data - > fd ) ! = shared_data - > data_size ) ;
if ( ! shared_data - > write_error )
{
shared_data - > data_written + = shared_data - > data_size ;
shared_data - > data_size = 0 ;
}
/* Wake up the read thread to continue reading data */
mutexUnlock ( & g_fileMutex ) ;
condvarWakeAll ( & g_readCondvar ) ;
if ( shared_data - > write_error ) break ;
}
end :
threadExit ( ) ;
}
u8 get_program_id_offset ( TitleInfo * info , u32 program_count )
{
if ( program_count < = 1 ) return 0 ;
u8 id_offset = 0 ;
u32 selected_idx = 0 , page_size = 30 , scroll = 0 ;
char nca_id_str [ 0x21 ] = { 0 } ;
NcmContentInfo * * content_infos = calloc ( program_count , sizeof ( NcmContentInfo * ) ) ;
if ( ! content_infos ) return 0 ;
for ( u32 i = 0 , j = 0 ; i < info - > content_count & & j < program_count ; i + + )
{
if ( info - > content_infos [ i ] . content_type ! = NcmContentType_Program ) continue ;
content_infos [ j + + ] = & ( info - > content_infos [ i ] ) ;
}
while ( true )
{
consoleClear ( ) ;
printf ( " select a program nca to dump the romfs from. \n \n " ) ;
for ( u32 i = scroll ; i < program_count ; i + + )
{
if ( i > = ( scroll + page_size ) ) break ;
utilsGenerateHexStringFromData ( nca_id_str , sizeof ( nca_id_str ) , content_infos [ i ] - > content_id . c , SHA256_HASH_SIZE / 2 ) ;
printf ( " %s%s.nca (ID offset #%u) \n " , i = = selected_idx ? " -> " : " " , nca_id_str , content_infos [ i ] - > id_offset ) ;
}
printf ( " \n " ) ;
consoleUpdate ( NULL ) ;
u64 btn_down = 0 , btn_held = 0 ;
while ( true )
{
2020-12-04 06:38:44 +00:00
utilsScanPads ( ) ;
btn_down = utilsGetButtonsDown ( ) ;
btn_held = utilsGetButtonsHeld ( ) ;
2020-11-08 19:08:30 +00:00
if ( btn_down | | btn_held ) break ;
}
2020-12-04 06:38:44 +00:00
if ( btn_down & HidNpadButton_A )
2020-11-08 19:08:30 +00:00
{
id_offset = content_infos [ selected_idx ] - > id_offset ;
break ;
} else
2020-12-04 06:38:44 +00:00
if ( ( btn_down & HidNpadButton_Down ) | | ( btn_held & ( HidNpadButton_StickLDown | HidNpadButton_StickRDown ) ) )
2020-11-08 19:08:30 +00:00
{
selected_idx + + ;
if ( selected_idx > = program_count )
{
2020-12-04 06:38:44 +00:00
if ( btn_down & HidNpadButton_Down )
2020-11-08 19:08:30 +00:00
{
selected_idx = scroll = 0 ;
} else {
selected_idx = ( program_count - 1 ) ;
}
} else
if ( selected_idx > = ( scroll + ( page_size / 2 ) ) & & program_count > ( scroll + page_size ) )
{
scroll + + ;
}
} else
2020-12-04 06:38:44 +00:00
if ( ( btn_down & HidNpadButton_Up ) | | ( btn_held & ( HidNpadButton_StickLUp | HidNpadButton_StickRUp ) ) )
2020-11-08 19:08:30 +00:00
{
selected_idx - - ;
if ( selected_idx = = UINT32_MAX )
{
2020-12-04 06:38:44 +00:00
if ( btn_down & HidNpadButton_Up )
2020-11-08 19:08:30 +00:00
{
selected_idx = ( program_count - 1 ) ;
scroll = ( program_count > = page_size ? ( program_count - page_size ) : 0 ) ;
} else {
selected_idx = 0 ;
}
} else
if ( selected_idx < ( scroll + ( page_size / 2 ) ) & & scroll > 0 )
{
scroll - - ;
}
}
2020-12-04 06:38:44 +00:00
if ( btn_held & ( HidNpadButton_StickLDown | HidNpadButton_StickRDown | HidNpadButton_StickLUp | HidNpadButton_StickRUp ) ) svcSleepThread ( 50000000 ) ; // 50 ms
2020-11-08 19:08:30 +00:00
}
free ( content_infos ) ;
return id_offset ;
}
int main ( int argc , char * argv [ ] )
{
2021-03-08 11:11:28 +00:00
g_argc = argc ;
g_argv = argv ;
2020-11-08 19:08:30 +00:00
int ret = 0 ;
if ( ! utilsInitializeResources ( ) )
{
ret = - 1 ;
goto out ;
}
2021-03-08 14:44:11 +00:00
consoleInit ( NULL ) ;
2020-11-08 19:08:30 +00:00
u32 app_count = 0 ;
TitleApplicationMetadata * * app_metadata = NULL ;
TitleUserApplicationData user_app_data = { 0 } ;
u32 selected_idx = 0 , page_size = 30 , scroll = 0 ;
bool exit_prompt = true ;
u8 * buf = NULL ;
NcaContext * base_nca_ctx = NULL , * update_nca_ctx = NULL ;
RomFileSystemContext romfs_ctx = { 0 } ;
BktrContext bktr_ctx = { 0 } ;
ThreadSharedData shared_data = { 0 } ;
Thread read_thread = { 0 } , write_thread = { 0 } ;
app_metadata = titleGetApplicationMetadataEntries ( false , & app_count ) ;
if ( ! app_metadata | | ! app_count )
{
consolePrint ( " app metadata failed \n " ) ;
goto out2 ;
}
consolePrint ( " app metadata succeeded \n " ) ;
buf = malloc ( BLOCK_SIZE ) ;
if ( ! buf )
{
consolePrint ( " buf failed \n " ) ;
goto out2 ;
}
consolePrint ( " buf succeeded \n " ) ;
base_nca_ctx = calloc ( 1 , sizeof ( NcaContext ) ) ;
if ( ! base_nca_ctx )
{
consolePrint ( " base nca ctx buf failed \n " ) ;
goto out2 ;
}
consolePrint ( " base nca ctx buf succeeded \n " ) ;
update_nca_ctx = calloc ( 1 , sizeof ( NcaContext ) ) ;
if ( ! update_nca_ctx )
{
consolePrint ( " update nca ctx buf failed \n " ) ;
goto out2 ;
}
consolePrint ( " update nca ctx buf succeeded \n " ) ;
utilsSleep ( 1 ) ;
while ( true )
{
consoleClear ( ) ;
printf ( " select a user application to dump its romfs. \n if an update is available, patch romfs data will be dumped instead. \n data will be saved to \" sdmc:/romfs \" . \n press b to exit. \n \n " ) ;
printf ( " title: %u / %u \n " , selected_idx + 1 , app_count ) ;
printf ( " selected title: %016lX - %s \n \n " , app_metadata [ selected_idx ] - > title_id , app_metadata [ selected_idx ] - > lang_entry . name ) ;
for ( u32 i = scroll ; i < app_count ; i + + )
{
if ( i > = ( scroll + page_size ) ) break ;
printf ( " %s%016lX - %s \n " , i = = selected_idx ? " -> " : " " , app_metadata [ i ] - > title_id , app_metadata [ i ] - > lang_entry . name ) ;
}
printf ( " \n " ) ;
consoleUpdate ( NULL ) ;
2021-02-20 18:52:07 +00:00
bool gc_update = false ;
2020-11-08 19:08:30 +00:00
u64 btn_down = 0 , btn_held = 0 ;
2021-02-20 18:52:07 +00:00
2020-11-08 19:08:30 +00:00
while ( true )
{
2020-12-04 06:38:44 +00:00
utilsScanPads ( ) ;
btn_down = utilsGetButtonsDown ( ) ;
btn_held = utilsGetButtonsHeld ( ) ;
2020-11-08 19:08:30 +00:00
if ( btn_down | | btn_held ) break ;
if ( titleIsGameCardInfoUpdated ( ) )
{
free ( app_metadata ) ;
app_metadata = titleGetApplicationMetadataEntries ( false , & app_count ) ;
if ( ! app_metadata )
{
consolePrint ( " \n app metadata failed \n " ) ;
goto out2 ;
}
selected_idx = scroll = 0 ;
2021-02-20 18:52:07 +00:00
gc_update = true ;
2020-11-08 19:08:30 +00:00
break ;
}
}
2021-02-20 18:52:07 +00:00
if ( gc_update ) continue ;
2020-12-04 06:38:44 +00:00
if ( btn_down & HidNpadButton_A )
2020-11-08 19:08:30 +00:00
{
if ( ! titleGetUserApplicationData ( app_metadata [ selected_idx ] - > title_id , & user_app_data ) | | ! user_app_data . app_info )
{
consolePrint ( " \n the selected title doesn't have available base content. \n " ) ;
utilsSleep ( 3 ) ;
continue ;
}
break ;
} else
2020-12-04 06:38:44 +00:00
if ( ( btn_down & HidNpadButton_Down ) | | ( btn_held & ( HidNpadButton_StickLDown | HidNpadButton_StickRDown ) ) )
2020-11-08 19:08:30 +00:00
{
selected_idx + + ;
if ( selected_idx > = app_count )
{
2020-12-04 06:38:44 +00:00
if ( btn_down & HidNpadButton_Down )
2020-11-08 19:08:30 +00:00
{
selected_idx = scroll = 0 ;
} else {
selected_idx = ( app_count - 1 ) ;
}
} else
if ( selected_idx > = ( scroll + ( page_size / 2 ) ) & & app_count > ( scroll + page_size ) )
{
scroll + + ;
}
} else
2020-12-04 06:38:44 +00:00
if ( ( btn_down & HidNpadButton_Up ) | | ( btn_held & ( HidNpadButton_StickLUp | HidNpadButton_StickRUp ) ) )
2020-11-08 19:08:30 +00:00
{
selected_idx - - ;
if ( selected_idx = = UINT32_MAX )
{
2020-12-04 06:38:44 +00:00
if ( btn_down & HidNpadButton_Up )
2020-11-08 19:08:30 +00:00
{
selected_idx = ( app_count - 1 ) ;
scroll = ( app_count > = page_size ? ( app_count - page_size ) : 0 ) ;
} else {
selected_idx = 0 ;
}
} else
if ( selected_idx < ( scroll + ( page_size / 2 ) ) & & scroll > 0 )
{
scroll - - ;
}
} else
2020-12-04 06:38:44 +00:00
if ( btn_down & HidNpadButton_B )
2020-11-08 19:08:30 +00:00
{
exit_prompt = false ;
goto out2 ;
}
2020-12-04 06:38:44 +00:00
if ( btn_held & ( HidNpadButton_StickLDown | HidNpadButton_StickRDown | HidNpadButton_StickLUp | HidNpadButton_StickRUp ) ) svcSleepThread ( 50000000 ) ; // 50 ms
2020-11-08 19:08:30 +00:00
}
u32 program_count = titleGetContentCountByType ( user_app_data . app_info , NcmContentType_Program ) ;
if ( ! program_count )
{
consolePrint ( " base app has no program ncas! \n " ) ;
goto out2 ;
}
u8 program_id_offset = get_program_id_offset ( user_app_data . app_info , program_count ) ;
consoleClear ( ) ;
consolePrint ( " selected title: \n %s (%016lX) \n \n " , app_metadata [ selected_idx ] - > lang_entry . name , app_metadata [ selected_idx ] - > title_id + program_id_offset ) ;
if ( ! ncaInitializeContext ( base_nca_ctx , user_app_data . app_info - > storage_id , ( user_app_data . app_info - > storage_id = = NcmStorageId_GameCard ? GameCardHashFileSystemPartitionType_Secure : 0 ) , \
titleGetContentInfoByTypeAndIdOffset ( user_app_data . app_info , NcmContentType_Program , program_id_offset ) , NULL ) )
{
consolePrint ( " nca initialize base ctx failed \n " ) ;
goto out2 ;
}
if ( user_app_data . patch_info )
{
if ( ! ncaInitializeContext ( update_nca_ctx , user_app_data . patch_info - > storage_id , ( user_app_data . patch_info - > storage_id = = NcmStorageId_GameCard ? GameCardHashFileSystemPartitionType_Secure : 0 ) , \
titleGetContentInfoByTypeAndIdOffset ( user_app_data . patch_info , NcmContentType_Program , program_id_offset ) , NULL ) )
{
consolePrint ( " nca initialize update ctx failed \n " ) ;
goto out2 ;
}
if ( ! bktrInitializeContext ( & bktr_ctx , & ( base_nca_ctx - > fs_ctx [ 1 ] ) , & ( update_nca_ctx - > fs_ctx [ 1 ] ) ) )
{
consolePrint ( " bktr initialize ctx failed \n " ) ;
goto out2 ;
}
shared_data . bktr_ctx = & bktr_ctx ;
bktrGetTotalDataSize ( & bktr_ctx , & ( shared_data . total_size ) ) ;
consolePrint ( " bktr initialize ctx succeeded \n " ) ;
} else {
if ( ! romfsInitializeContext ( & romfs_ctx , & ( base_nca_ctx - > fs_ctx [ 1 ] ) ) )
{
consolePrint ( " romfs initialize ctx failed \n " ) ;
goto out2 ;
}
shared_data . romfs_ctx = & romfs_ctx ;
romfsGetTotalDataSize ( & romfs_ctx , & ( shared_data . total_size ) ) ;
consolePrint ( " romfs initialize ctx succeeded \n " ) ;
}
shared_data . fd = NULL ;
shared_data . data = buf ;
shared_data . data_size = 0 ;
shared_data . data_written = 0 ;
consolePrint ( " creating threads \n " ) ;
utilsCreateThread ( & read_thread , read_thread_func , & shared_data , 2 ) ;
utilsCreateThread ( & write_thread , write_thread_func , & shared_data , 2 ) ;
u8 prev_time = 0 ;
u64 prev_size = 0 ;
u8 percent = 0 ;
time_t btn_cancel_start_tmr = 0 , btn_cancel_end_tmr = 0 ;
bool btn_cancel_cur_state = false , btn_cancel_prev_state = false ;
utilsChangeHomeButtonBlockStatus ( true ) ;
consolePrint ( " hold b to cancel \n \n " ) ;
time_t start = time ( NULL ) ;
while ( shared_data . data_written < shared_data . total_size )
{
if ( shared_data . read_error | | shared_data . write_error ) break ;
time_t now = time ( NULL ) ;
struct tm * ts = localtime ( & now ) ;
size_t size = shared_data . data_written ;
2020-12-04 06:38:44 +00:00
utilsScanPads ( ) ;
btn_cancel_cur_state = ( utilsGetButtonsHeld ( ) & HidNpadButton_B ) ;
2020-11-08 19:08:30 +00:00
if ( btn_cancel_cur_state & & btn_cancel_cur_state ! = btn_cancel_prev_state )
{
btn_cancel_start_tmr = now ;
} else
if ( btn_cancel_cur_state & & btn_cancel_cur_state = = btn_cancel_prev_state )
{
btn_cancel_end_tmr = now ;
if ( ( btn_cancel_end_tmr - btn_cancel_start_tmr ) > = 3 )
{
mutexLock ( & g_fileMutex ) ;
shared_data . transfer_cancelled = true ;
mutexUnlock ( & g_fileMutex ) ;
break ;
}
} else {
btn_cancel_start_tmr = btn_cancel_end_tmr = 0 ;
}
btn_cancel_prev_state = btn_cancel_cur_state ;
if ( prev_time = = ts - > tm_sec | | prev_size = = size ) continue ;
percent = ( u8 ) ( ( size * 100 ) / shared_data . total_size ) ;
prev_time = ts - > tm_sec ;
prev_size = size ;
printf ( " %lu / %lu (%u%%) | Time elapsed: %lu \n " , size , shared_data . total_size , percent , ( now - start ) ) ;
consoleUpdate ( NULL ) ;
}
start = ( time ( NULL ) - start ) ;
consolePrint ( " \n waiting for threads to join \n " ) ;
utilsJoinThread ( & read_thread ) ;
consolePrint ( " read_thread done: %lu \n " , time ( NULL ) ) ;
utilsJoinThread ( & write_thread ) ;
consolePrint ( " write_thread done: %lu \n " , time ( NULL ) ) ;
utilsChangeHomeButtonBlockStatus ( false ) ;
if ( shared_data . read_error | | shared_data . write_error )
{
consolePrint ( " i/o error \n " ) ;
goto out2 ;
}
if ( shared_data . transfer_cancelled )
{
consolePrint ( " process cancelled \n " ) ;
goto out2 ;
}
consolePrint ( " process completed in %lu seconds \n " , start ) ;
out2 :
if ( exit_prompt )
{
consolePrint ( " press any button to exit \n " ) ;
2020-12-04 06:38:44 +00:00
utilsWaitForButtonPress ( 0 ) ;
2020-11-08 19:08:30 +00:00
}
romfsFreeContext ( & romfs_ctx ) ;
bktrFreeContext ( & bktr_ctx ) ;
if ( update_nca_ctx ) free ( update_nca_ctx ) ;
if ( base_nca_ctx ) free ( base_nca_ctx ) ;
if ( buf ) free ( buf ) ;
if ( app_metadata ) free ( app_metadata ) ;
out :
utilsCloseResources ( ) ;
consoleExit ( NULL ) ;
return ret ;
}