diff --git a/source/main.c b/source/main.c index df0b9b0..cff8d5a 100644 --- a/source/main.c +++ b/source/main.c @@ -213,13 +213,12 @@ int main(int argc, char *argv[]) goto out; } + u32 app_count = 0, selected_idx = 0; + TitleApplicationMetadata **app_metadata = NULL; + TitleUserApplicationData user_app_data = {0}; + u8 *buf = NULL; - // ACNH 0x01006F8002326000 | Smash 0x01006A800016E000 | Dark Souls 0x01004AB00A260000 | BotW 0x01007EF00011E000 | Untitled Goose Game 0x010082400BCC6000 | SMO 0x0100000000010000 - u64 base_tid = (u64)0x0100000000010000; - u64 update_tid = titleGetPatchIdByApplicationId(base_tid); - - TitleInfo *base_title_info = NULL, *update_title_info = NULL; NcaContext *base_nca_ctx = NULL, *update_nca_ctx = NULL; Ticket base_tik = {0}, update_tik = {0}; @@ -228,6 +227,15 @@ int main(int argc, char *argv[]) ThreadSharedData shared_data = {0}; thrd_t read_thread, write_thread; + app_metadata = titleGetApplicationMetadataEntries(false, &app_count); + if (!app_metadata || !app_count) + { + consolePrint("app metadata failed\n"); + goto out2; + } + + consolePrint("app metadata succeeded\n"); + buf = usbAllocatePageAlignedBuffer(TEST_BUF_SIZE); if (!buf) { @@ -255,26 +263,88 @@ int main(int argc, char *argv[]) consolePrint("update nca ctx buf succeeded\n"); - base_title_info = titleGetInfoFromStorageByTitleId(NcmStorageId_Any, base_tid); - update_title_info = titleGetInfoFromStorageByTitleId(NcmStorageId_Any, update_tid); + utilsSleep(1); - if (!base_title_info || !update_title_info) + while(true) { - consolePrint("title info failed\n"); - goto out2; + consoleClear(); + printf("select a base title with an available update.\nthe updated romfs will be dumped via usb.\npress b to cancel.\n\n"); + + for(u32 i = 0; i < app_count; i++) printf("%s%s (%016lX)\n", i == selected_idx ? " -> " : " ", app_metadata[i]->lang_entry.name, app_metadata[i]->title_id); + consoleUpdate(NULL); + + u64 btn = 0; + + while(true) + { + btn = utilsReadInput(UtilsInputType_Down); + if (btn) break; + + if (titleRefreshGameCardTitleInfo()) + { + free(app_metadata); + + app_metadata = titleGetApplicationMetadataEntries(false, &app_count); + if (!app_metadata) + { + consolePrint("\napp metadata failed\n"); + goto out2; + } + + selected_idx = 0; + + break; + } + } + + if (btn & KEY_A) + { + if (!titleGetUserApplicationData(app_metadata[selected_idx]->title_id, &user_app_data) || !user_app_data.app_info || !user_app_data.patch_info) + { + consolePrint("\nthe selected title either doesn't have available base content or update content.\n"); + utilsSleep(3); + continue; + } + + break; + } else + if (btn & KEY_DOWN) + { + if ((selected_idx + 1) < app_count) + { + selected_idx++; + } else { + selected_idx = 0; + } + } else + if (btn & KEY_UP) + { + if (selected_idx == 0) + { + selected_idx = (app_count - 1); + } else { + selected_idx--; + } + } else + if (btn & KEY_B) + { + consolePrint("\nprocess cancelled.\n"); + goto out2; + } } - consolePrint("title info succeeded\n"); + consoleClear(); + consolePrint("selected title:\n%s (%016lX)\n\n", app_metadata[selected_idx]->lang_entry.name, app_metadata[selected_idx]->title_id); - if (!ncaInitializeContext(base_nca_ctx, base_title_info->storage_id, (base_title_info->storage_id == NcmStorageId_GameCard ? GameCardHashFileSystemPartitionType_Secure : 0), \ - titleGetContentInfoByTypeAndIdOffset(base_title_info, NcmContentType_Program, 0), &base_tik)) + if (!ncaInitializeContext(base_nca_ctx, user_app_data.app_info->storage_id, (user_app_data.app_info->storage_id == NcmStorageId_GameCard ? GameCardHashFileSystemPartitionType_Secure : 0), \ + titleGetContentInfoByTypeAndIdOffset(user_app_data.app_info, NcmContentType_Program, 0), &base_tik)) { consolePrint("nca initialize base ctx failed\n"); goto out2; } - if (!ncaInitializeContext(update_nca_ctx, update_title_info->storage_id, (update_title_info->storage_id == NcmStorageId_GameCard ? GameCardHashFileSystemPartitionType_Secure : 0), \ - titleGetContentInfoByTypeAndIdOffset(update_title_info, NcmContentType_Program, 0), &update_tik)) + if (!ncaInitializeContext(update_nca_ctx, user_app_data.patch_info->storage_id, (user_app_data.patch_info->storage_id == NcmStorageId_GameCard ? GameCardHashFileSystemPartitionType_Secure : 0), \ + titleGetContentInfoByTypeAndIdOffset(user_app_data.patch_info, NcmContentType_Program, 0), &update_tik)) { consolePrint("nca initialize update ctx failed\n"); goto out2; @@ -407,6 +477,8 @@ out2: if (buf) free(buf); + if (app_metadata) free(app_metadata); + out: utilsCloseResources(); diff --git a/source/nca.c b/source/nca.c index cd639cb..d1a40f3 100644 --- a/source/nca.c +++ b/source/nca.c @@ -233,13 +233,9 @@ bool ncaInitializeContext(NcaContext *out, u8 storage_id, u8 hfs_partition_type, aes128XtsContextCreate(&(out->fs_contexts[i].xts_decrypt_ctx), out->decrypted_key_area.aes_xts_1, out->decrypted_key_area.aes_xts_2, false); aes128XtsContextCreate(&(out->fs_contexts[i].xts_encrypt_ctx), out->decrypted_key_area.aes_xts_1, out->decrypted_key_area.aes_xts_2, true); } else - if (out->fs_contexts[i].encryption_type == NcaEncryptionType_AesCtr) + if (out->fs_contexts[i].encryption_type == NcaEncryptionType_AesCtr || out->fs_contexts[i].encryption_type == NcaEncryptionType_AesCtrEx) { aes128CtrContextCreate(&(out->fs_contexts[i].ctr_ctx), out->decrypted_key_area.aes_ctr, out->fs_contexts[i].ctr); - } else - if (out->fs_contexts[i].encryption_type == NcaEncryptionType_AesCtrEx) - { - aes128CtrContextCreate(&(out->fs_contexts[i].ctr_ctx), out->decrypted_key_area.aes_ctr_ex, out->fs_contexts[i].ctr); } } } diff --git a/source/title.c b/source/title.c index b7bb7e6..68f374b 100644 --- a/source/title.c +++ b/source/title.c @@ -349,8 +349,8 @@ NX_INLINE void titleFreeTitleInfo(void); NX_INLINE TitleApplicationMetadata *titleFindApplicationMetadataByTitleId(u64 title_id); -static bool titleRetrieveApplicationMetadataFromNsRecords(void); -static bool titleGenerateMetadataEntriesForSystemTitles(void); +static bool titleGenerateMetadataEntriesFromSystemTitles(void); +static bool titleGenerateMetadataEntriesFromNsRecords(void); static bool titleRetrieveApplicationMetadataByTitleId(u64 title_id, TitleApplicationMetadata *out); static bool titleOpenNcmDatabases(void); @@ -369,6 +369,12 @@ static bool titleGetContentInfosFromTitle(u8 storage_id, const NcmContentMetaKey static bool _titleRefreshGameCardTitleInfo(bool lock); static void titleRemoveGameCardTitleInfoEntries(void); +static bool titleIsUserApplicationContentAvailable(u64 app_id); +static TitleInfo *_titleGetInfoFromStorageByTitleId(u8 storage_id, u64 title_id, bool lock); + +static int titleUserApplicationMetadataComparison(const void *a, const void *b); +static int titleSystemTitleMetadataComparison(const void *a, const void *b); + bool titleInitialize(void) { mutexLock(&g_titleMutex); @@ -385,19 +391,19 @@ bool titleInitialize(void) goto end; } - /* Retrieve application metadata from ns records. */ - /* Theoretically speaking, we should only need to do this once. */ - /* However, if any new gamecard is inserted while the application is running, we *will* have to retrieve the metadata from its application(s). */ - if (!titleRetrieveApplicationMetadataFromNsRecords()) + /* Generate application metadata entries from hardcoded system titles, since we can't retrieve their names via ns. */ + if (!titleGenerateMetadataEntriesFromSystemTitles()) { - LOGFILE("Failed to retrieve application metadata from ns records!"); + LOGFILE("Failed to generate application metadata from hardcoded system titles!"); goto end; } - /* Generate application metadata entries for system titles, since we can't retrieve their names via ns. */ - if (!titleGenerateMetadataEntriesForSystemTitles()) + /* Generate application metadata entries from ns records. */ + /* Theoretically speaking, we should only need to do this once. */ + /* However, if any new gamecard is inserted while the application is running, we *will* have to retrieve the metadata from its application(s). */ + if (!titleGenerateMetadataEntriesFromNsRecords()) { - LOGFILE("Failed to generate application metadata for system titles!"); + LOGFILE("Failed to generate application metadata from ns records!"); goto end; } @@ -431,7 +437,7 @@ bool titleInitialize(void) - + /* if (g_titleInfo && g_titleInfoCount) @@ -474,26 +480,36 @@ bool titleInitialize(void) fprintf(title_infos_txt, " ID Offset: 0x%02X\r\n", g_titleInfo[i].content_infos[j].id_offset); } - if (g_titleInfo[i].app_metadata) + if ((g_titleInfo[i].meta_key.type == NcmContentMetaType_Application && g_titleInfo[i].app_metadata) || ((g_titleInfo[i].meta_key.type == NcmContentMetaType_Patch || \ + g_titleInfo[i].meta_key.type == NcmContentMetaType_AddOnContent) && g_titleInfo[i].parent && g_titleInfo[i].parent->app_metadata)) { - if (strlen(g_titleInfo[i].app_metadata->lang_entry.name)) fprintf(title_infos_txt, "Application Name: %s\r\n", g_titleInfo[i].app_metadata->lang_entry.name); - if (strlen(g_titleInfo[i].app_metadata->lang_entry.author)) fprintf(title_infos_txt, "Application Author: %s\r\n", g_titleInfo[i].app_metadata->lang_entry.author); - if (g_titleInfo[i].app_metadata->icon_size) fprintf(title_infos_txt, "JPEG Icon Size: 0x%X\r\n", g_titleInfo[i].app_metadata->icon_size); + TitleApplicationMetadata *app_metadata = (g_titleInfo[i].meta_key.type == NcmContentMetaType_Application ? g_titleInfo[i].app_metadata : g_titleInfo[i].parent->app_metadata); - if (g_titleInfo[i].app_metadata->icon) + if (strlen(app_metadata->lang_entry.name)) fprintf(title_infos_txt, "Name: %s\r\n", app_metadata->lang_entry.name); + if (strlen(app_metadata->lang_entry.author)) fprintf(title_infos_txt, "Author: %s\r\n", app_metadata->lang_entry.author); + + if (g_titleInfo[i].meta_key.type == NcmContentMetaType_Application && app_metadata->icon_size && app_metadata->icon) { - sprintf(icon_path, "sdmc:/records/%016lX.jpg", g_titleInfo[i].app_metadata->title_id); + fprintf(title_infos_txt, "JPEG Icon Size: 0x%X\r\n", app_metadata->icon_size); + + sprintf(icon_path, "sdmc:/records/%016lX.jpg", app_metadata->title_id); icon_jpg = fopen(icon_path, "wb"); if (icon_jpg) { - fwrite(g_titleInfo[i].app_metadata->icon, 1, g_titleInfo[i].app_metadata->icon_size, icon_jpg); + fwrite(app_metadata->icon, 1, app_metadata->icon_size, icon_jpg); fclose(icon_jpg); icon_jpg = NULL; } } } + if (g_titleInfo[i].meta_key.type == NcmContentMetaType_Patch || g_titleInfo[i].meta_key.type == NcmContentMetaType_AddOnContent) + { + if (g_titleInfo[i].previous) fprintf(title_infos_txt, "Previous %s ID: %016lX\r\n", g_titleInfo[i].meta_key.type == NcmContentMetaType_Patch ? "Patch" : "AOC", g_titleInfo[i].previous->meta_key.id); + if (g_titleInfo[i].next) fprintf(title_infos_txt, "Next %s ID: %016lX\r\n", g_titleInfo[i].meta_key.type == NcmContentMetaType_Patch ? "Patch" : "AOC", g_titleInfo[i].next->meta_key.id); + } + fprintf(title_infos_txt, "\r\n"); fflush(title_infos_txt); @@ -506,7 +522,7 @@ bool titleInitialize(void) - + */ @@ -605,35 +621,117 @@ bool titleRefreshGameCardTitleInfo(void) return _titleRefreshGameCardTitleInfo(true); } -TitleInfo *titleGetInfoFromStorageByTitleId(u8 storage_id, u64 title_id) +TitleApplicationMetadata **titleGetApplicationMetadataEntries(bool is_system, u32 *out_count) { mutexLock(&g_titleMutex); - TitleInfo *info = NULL; + u32 start_idx = (is_system ? 0 : g_systemTitlesCount); + u32 max_val = (is_system ? g_systemTitlesCount : g_appMetadataCount); - if (!g_titleInfo || !g_titleInfoCount || storage_id < NcmStorageId_GameCard || storage_id > NcmStorageId_Any || !title_id) + u32 app_count = 0; + TitleApplicationMetadata **app_metadata = NULL, **tmp_app_metadata = NULL; + + if (!g_appMetadata || (is_system && g_appMetadataCount < g_systemTitlesCount) || (!is_system && g_appMetadataCount == g_systemTitlesCount) || !out_count) { LOGFILE("Invalid parameters!"); goto end; } - for(u32 i = 0; i < g_titleInfoCount; i++) + for(u32 i = start_idx; i < max_val; i++) { - if (g_titleInfo[i].meta_key.id == title_id && (storage_id == NcmStorageId_Any || (storage_id != NcmStorageId_Any && g_titleInfo[i].storage_id == storage_id))) + /* Skip current metadata entry if content data for this title isn't available. */ + if ((is_system && !_titleGetInfoFromStorageByTitleId(NcmStorageId_BuiltInSystem, g_appMetadata[i].title_id, false)) || \ + (!is_system && !titleIsUserApplicationContentAvailable(g_appMetadata[i].title_id))) continue; + + /* Reallocate pointer buffer. */ + tmp_app_metadata = realloc(app_metadata, (app_count + 1) * sizeof(TitleApplicationMetadata*)); + if (!tmp_app_metadata) { - info = &(g_titleInfo[i]); - break; + LOGFILE("Failed to reallocate application metadata pointer buffer!"); + if (app_metadata) free(app_metadata); + app_metadata = NULL; + goto end; } + + app_metadata = tmp_app_metadata; + tmp_app_metadata = NULL; + + /* Set current pointer and increase counter. */ + app_metadata[app_count] = &(g_appMetadata[i]); + app_count++; } - if (!info) LOGFILE("Unable to find TitleInfo entry with ID \"%016lX\"! (storage ID %u).", title_id, storage_id); + if (app_metadata && app_count) + { + /* Sort metadata entries alphabetically. */ + qsort(app_metadata, app_count, sizeof(TitleApplicationMetadata*), is_system ? &titleSystemTitleMetadataComparison : &titleUserApplicationMetadataComparison); + + /* Update output counter. */ + *out_count = app_count; + } else { + LOGFILE("No content data found for %s!", is_system ? "system titles" : "user applications"); + } end: mutexUnlock(&g_titleMutex); - return info; + return app_metadata; } +TitleInfo *titleGetInfoFromStorageByTitleId(u8 storage_id, u64 title_id) +{ + return _titleGetInfoFromStorageByTitleId(storage_id, title_id, true); +} + +bool titleGetUserApplicationData(u64 app_id, TitleUserApplicationData *out) +{ + mutexLock(&g_titleMutex); + + bool success = false; + + if (!titleIsUserApplicationContentAvailable(app_id) || !out) + { + LOGFILE("Invalid parameters!"); + goto end; + } + + /* Clear output. */ + memset(out, 0, sizeof(TitleUserApplicationData)); + + /* Get user application title info. */ + out->app_info = _titleGetInfoFromStorageByTitleId(NcmStorageId_Any, app_id, false); + + /* Get first patch title info. */ + out->patch_info = _titleGetInfoFromStorageByTitleId(NcmStorageId_Any, titleGetPatchIdByApplicationId(app_id), false); + + /* Get first add-on content title info. */ + for(u32 i = 0; i < g_titleInfoCount; i++) + { + if (g_titleInfo[i].meta_key.type == NcmContentMetaType_AddOnContent && titleCheckIfAddOnContentIdBelongsToApplicationId(app_id, g_titleInfo[i].meta_key.id)) + { + out->aoc_info = &(g_titleInfo[i]); + break; + } + } + + /* Check retrieved title info. */ + success = (out->app_info || out->patch_info || out->aoc_info); + if (!success) + { + LOGFILE("Failed to retrieve user application data for ID \"%016lX\"!", app_id); + goto end; + } + + /* Update 'gamecard_available' flag. */ + out->gamecard_available = (out->app_info && (out->app_info->storage_id == NcmStorageId_GameCard || _titleGetInfoFromStorageByTitleId(NcmStorageId_GameCard, app_id, false) != NULL)); + +end: + mutexUnlock(&g_titleMutex); + + return success; +} + + @@ -692,13 +790,48 @@ NX_INLINE TitleApplicationMetadata *titleFindApplicationMetadataByTitleId(u64 ti return NULL; } -static bool titleRetrieveApplicationMetadataFromNsRecords(void) +static bool titleGenerateMetadataEntriesFromSystemTitles(void) +{ + TitleApplicationMetadata *tmp_app_metadata = NULL; + + /* Reallocate application metadata buffer. */ + /* If g_appMetadata == NULL, realloc() will essentially act as a malloc(). */ + tmp_app_metadata = realloc(g_appMetadata, (g_appMetadataCount + g_systemTitlesCount) * sizeof(TitleApplicationMetadata)); + if (!tmp_app_metadata) + { + LOGFILE("Failed to reallocate application metadata buffer! (%u %s).", g_appMetadataCount + g_systemTitlesCount, (g_appMetadataCount + g_systemTitlesCount) > 1 ? "entries" : "entry"); + return false; + } + + g_appMetadata = tmp_app_metadata; + tmp_app_metadata = NULL; + + /* Clear new application metadata buffer area. */ + memset(g_appMetadata + g_appMetadataCount, 0, g_systemTitlesCount * sizeof(TitleApplicationMetadata)); + + /* Fill new application metadata entries. */ + for(u32 i = 0; i < g_systemTitlesCount; i++) + { + g_appMetadata[g_appMetadataCount + i].title_id = g_systemTitles[i].title_id; + sprintf(g_appMetadata[g_appMetadataCount + i].lang_entry.name, g_systemTitles[i].name); + } + + /* Update application metadata count. */ + g_appMetadataCount += g_systemTitlesCount; + + return true; +} + +static bool titleGenerateMetadataEntriesFromNsRecords(void) { Result rc = 0; NsApplicationRecord *app_records = NULL; u32 app_records_count = 0; + u32 cur_app_count = g_appMetadataCount; + TitleApplicationMetadata *tmp_app_metadata = NULL; + bool success = false; /* Allocate memory for the ns application records. */ @@ -724,16 +857,18 @@ static bool titleRetrieveApplicationMetadataFromNsRecords(void) goto end; } - /* Allocate memory for the application metadata. */ - g_appMetadata = calloc(app_records_count, sizeof(TitleApplicationMetadata)); - if (!g_appMetadata) + /* Reallocate application metadata buffer. */ + tmp_app_metadata = realloc(g_appMetadata, (g_appMetadataCount + app_records_count) * sizeof(TitleApplicationMetadata)); + if (!tmp_app_metadata) { - LOGFILE("Failed to allocate memory for application metadata! (%u %s).", app_records_count, app_records_count > 1 ? "entries" : "entry"); + LOGFILE("Failed to reallocate application metadata buffer! (%u %s).", g_appMetadataCount + app_records_count, (g_appMetadataCount + app_records_count) > 1 ? "entries" : "entry"); goto end; } + g_appMetadata = tmp_app_metadata; + tmp_app_metadata = NULL; + /* Retrieve application metadata for each ns application record. */ - g_appMetadataCount = 0; for(u32 i = 0; i < app_records_count; i++) { if (!titleRetrieveApplicationMetadataByTitleId(app_records[i].application_id, &(g_appMetadata[g_appMetadataCount]))) continue; @@ -741,14 +876,14 @@ static bool titleRetrieveApplicationMetadataFromNsRecords(void) } /* Check retrieved application metadata count. */ - if (!g_appMetadataCount) + if (g_appMetadataCount == cur_app_count) { LOGFILE("Unable to retrieve application metadata from ns application records! (%u %s).", app_records_count, app_records_count > 1 ? "entries" : "entry"); goto end; } /* Decrease application metadata buffer size if needed. */ - if (g_appMetadataCount < app_records_count) + if (g_appMetadataCount < (cur_app_count + app_records_count)) { TitleApplicationMetadata *tmp_app_metadata = realloc(g_appMetadata, g_appMetadataCount * sizeof(TitleApplicationMetadata)); if (!tmp_app_metadata) @@ -764,53 +899,11 @@ static bool titleRetrieveApplicationMetadataFromNsRecords(void) success = true; end: - if (!success) - { - if (g_appMetadata) - { - free(g_appMetadata); - g_appMetadata = NULL; - } - - g_appMetadataCount = 0; - } - if (app_records) free(app_records); return success; } -static bool titleGenerateMetadataEntriesForSystemTitles(void) -{ - TitleApplicationMetadata *tmp_app_metadata = NULL; - - /* Reallocate application metadata buffer */ - tmp_app_metadata = realloc(g_appMetadata, (g_appMetadataCount + g_systemTitlesCount) * sizeof(TitleApplicationMetadata)); - if (!tmp_app_metadata) - { - LOGFILE("Failed to reallocate application metadata buffer! (%u %s).", g_appMetadataCount + g_systemTitlesCount, (g_appMetadataCount + g_systemTitlesCount) > 1 ? "entries" : "entry"); - return false; - } - - g_appMetadata = tmp_app_metadata; - tmp_app_metadata = 0; - - /* Clear new application metadata buffer area. */ - memset(g_appMetadata + g_appMetadataCount, 0, g_systemTitlesCount * sizeof(TitleApplicationMetadata)); - - /* Fill new application metadata entries. */ - for(u32 i = 0; i < g_systemTitlesCount; i++) - { - g_appMetadata[g_appMetadataCount + i].title_id = g_systemTitles[i].title_id; - sprintf(g_appMetadata[g_appMetadataCount + i].lang_entry.name, g_systemTitles[i].name); - } - - /* Update application metadata count. */ - g_appMetadataCount += g_systemTitlesCount; - - return true; -} - static bool titleRetrieveApplicationMetadataByTitleId(u64 title_id, TitleApplicationMetadata *out) { if (!g_nsAppControlData || !title_id || !out) @@ -1137,9 +1230,9 @@ static bool titleRetrieveContentMetaKeysFromDatabase(u8 storage_id) if (titleGetContentInfosFromTitle(storage_id, &(meta_keys[i]), &(cur_title_info->content_infos), &(cur_title_info->content_count))) { /* Calculate title size. */ + u64 tmp_size = 0; for(u32 j = 0; j < cur_title_info->content_count; j++) { - u64 tmp_size = 0; titleConvertNcmContentSizeToU64(cur_title_info->content_infos[j].size, &tmp_size); cur_title_info->title_size += tmp_size; } @@ -1152,6 +1245,45 @@ static bool titleRetrieveContentMetaKeysFromDatabase(u8 storage_id) /* Update title info count. */ g_titleInfoCount += total; + /* Fill patch / add-on content specific info. */ + for(u32 i = 0; i < g_titleInfoCount; i++) + { + TitleInfo *child_info = &(g_titleInfo[i]); + if (child_info->meta_key.type != NcmContentMetaType_Patch && child_info->meta_key.type != NcmContentMetaType_AddOnContent) continue; + + /* Retrieve pointer to parent entry. */ + /* Since gamecard title info entries are always appended to the end of the buffer, this guarantees we will first retrieve an eMMC / SD card entry (if available). */ + for(u32 j = 0; j < g_titleInfoCount; j++) + { + TitleInfo *parent_info = &(g_titleInfo[j]); + + if (parent_info->meta_key.type == NcmContentMetaType_Application && \ + ((child_info->meta_key.type == NcmContentMetaType_Patch && titleCheckIfPatchIdBelongsToApplicationId(parent_info->meta_key.id, child_info->meta_key.id)) || \ + (child_info->meta_key.type == NcmContentMetaType_AddOnContent && titleCheckIfAddOnContentIdBelongsToApplicationId(parent_info->meta_key.id, child_info->meta_key.id)))) + { + child_info->parent = parent_info; + break; + } + } + + /* Locate previous patch / add-on content entry. */ + /* If it's found, we will update both its next pointer and the previous pointer from the current entry. */ + /* We will also update its parent entry pointer if its NULL. */ + for(u32 j = i; j > 0; j--) + { + TitleInfo *previous_info = &(g_titleInfo[j - 1]); + + if (previous_info->meta_key.type == child_info->meta_key.type && ((child_info->meta_key.type == NcmContentMetaType_Patch && previous_info->meta_key.id == child_info->meta_key.id) || \ + (child_info->meta_key.type == NcmContentMetaType_AddOnContent && titleCheckIfAddOnContentIdsAreSiblings(previous_info->meta_key.id, child_info->meta_key.id)))) + { + if (child_info->parent && !previous_info->parent) previous_info->parent = child_info->parent; + previous_info->next = child_info; + child_info->previous = previous_info; + break; + } + } + } + success = true; end: @@ -1248,7 +1380,7 @@ static bool _titleRefreshGameCardTitleInfo(bool lock) status = (gamecardGetStatus() == GameCardStatus_InsertedAndInfoLoaded); if (status == g_titleGameCardAvailable || !status) { - cleanup = (status != g_titleGameCardAvailable); + success = cleanup = (status != g_titleGameCardAvailable); goto end; } @@ -1370,13 +1502,25 @@ static void titleRemoveGameCardTitleInfoEntries(void) if (g_titleInfoGameCardCount == g_titleInfoCount) { + /* Free all title info entries. */ titleFreeTitleInfo(); } else { + /* Update parent, previous and next title info pointers from patches and add-on content entries. */ + for(u32 i = 0; i < (g_titleInfoCount - g_titleInfoGameCardCount); i++) + { + if (g_titleInfo[i].meta_key.type != NcmContentMetaType_Patch && g_titleInfo[i].meta_key.type != NcmContentMetaType_AddOnContent) continue; + if (g_titleInfo[i].parent && g_titleInfo[i].parent->storage_id == NcmStorageId_GameCard) g_titleInfo[i].parent = NULL; + if (g_titleInfo[i].previous && g_titleInfo[i].previous->storage_id == NcmStorageId_GameCard) g_titleInfo[i].previous = NULL; + if (g_titleInfo[i].next && g_titleInfo[i].next->storage_id == NcmStorageId_GameCard) g_titleInfo[i].next = NULL; + } + + /* Free content infos from gamecard title info entries. */ for(u32 i = (g_titleInfoCount - g_titleInfoGameCardCount); i < g_titleInfoCount; i++) { if (g_titleInfo[i].content_infos) free(g_titleInfo[i].content_infos); } + /* Reallocate title info buffer. */ TitleInfo *tmp_title_info = realloc(g_titleInfo, (g_titleInfoCount - g_titleInfoGameCardCount) * sizeof(TitleInfo)); if (tmp_title_info) { @@ -1384,7 +1528,81 @@ static void titleRemoveGameCardTitleInfoEntries(void) tmp_title_info = NULL; } + /* Update counters. */ g_titleInfoCount = (g_titleInfoCount - g_titleInfoGameCardCount); g_titleInfoGameCardStartIndex = g_titleInfoGameCardCount = 0; } } + +static bool titleIsUserApplicationContentAvailable(u64 app_id) +{ + if (!g_titleInfo || !g_titleInfoCount || !app_id) return false; + + for(u32 i = 0; i < g_titleInfoCount; i++) + { + if ((g_titleInfo[i].meta_key.type == NcmContentMetaType_Application && g_titleInfo[i].meta_key.id == app_id) || \ + (g_titleInfo[i].meta_key.type == NcmContentMetaType_Patch && titleCheckIfPatchIdBelongsToApplicationId(app_id, g_titleInfo[i].meta_key.id)) || \ + (g_titleInfo[i].meta_key.type == NcmContentMetaType_AddOnContent && titleCheckIfAddOnContentIdBelongsToApplicationId(app_id, g_titleInfo[i].meta_key.id))) return true; + } + + return false; +} + +static TitleInfo *_titleGetInfoFromStorageByTitleId(u8 storage_id, u64 title_id, bool lock) +{ + if (lock) mutexLock(&g_titleMutex); + + TitleInfo *info = NULL; + + if (!g_titleInfo || !g_titleInfoCount || storage_id < NcmStorageId_GameCard || storage_id > NcmStorageId_Any || (storage_id == NcmStorageId_GameCard && (!g_titleInfoGameCardCount || \ + g_titleInfoGameCardCount > g_titleInfoCount || g_titleInfoGameCardStartIndex != (g_titleInfoCount - g_titleInfoGameCardCount))) || !title_id) + { + LOGFILE("Invalid parameters!"); + goto end; + } + + /* Speed up gamecard lookups. */ + u32 start_idx = (storage_id == NcmStorageId_GameCard ? g_titleInfoGameCardStartIndex : 0); + u32 max_val = ((storage_id == NcmStorageId_GameCard || storage_id == NcmStorageId_Any) ? g_titleInfoCount : (g_titleInfoCount - g_titleInfoGameCardCount)); + + for(u32 i = start_idx; i < max_val; i++) + { + if (g_titleInfo[i].meta_key.id == title_id && (storage_id == NcmStorageId_Any || (storage_id != NcmStorageId_Any && g_titleInfo[i].storage_id == storage_id))) + { + info = &(g_titleInfo[i]); + break; + } + } + + //if (!info) LOGFILE("Unable to find TitleInfo entry with ID \"%016lX\"! (storage ID %u).", title_id, storage_id); + +end: + if (lock) mutexUnlock(&g_titleMutex); + + return info; +} + +static int titleUserApplicationMetadataComparison(const void *a, const void *b) +{ + const TitleApplicationMetadata *app_metadata_1 = *((const TitleApplicationMetadata**)a); + const TitleApplicationMetadata *app_metadata_2 = *((const TitleApplicationMetadata**)b); + + return strcasecmp(app_metadata_1->lang_entry.name, app_metadata_2->lang_entry.name); +} + +static int titleSystemTitleMetadataComparison(const void *a, const void *b) +{ + const TitleApplicationMetadata *app_metadata_1 = *((const TitleApplicationMetadata**)a); + const TitleApplicationMetadata *app_metadata_2 = *((const TitleApplicationMetadata**)b); + + if (app_metadata_1->title_id < app_metadata_2->title_id) + { + return -1; + } else + if (app_metadata_1->title_id > app_metadata_2->title_id) + { + return 1; + } + + return 0; +} diff --git a/source/title.h b/source/title.h index 18bc60b..0813dd0 100644 --- a/source/title.h +++ b/source/title.h @@ -23,9 +23,15 @@ #ifndef __TITLE_H__ #define __TITLE_H__ -#define TITLE_PATCH_ID_MASK (u64)0x800 -#define TITLE_ADDONCONTENT_ID_MASK (u64)0xFFFFFFFFFFFF0000 +#define TITLE_PATCH_BASE_ID (u64)0x800 +#define TITLE_ADDONCONTENT_BASE_ID (u64)0x1000 +#define TITLE_ADDONCONTENT_CONVERSION_MASK (u64)0xFFFFFFFFFFFFF000 +#define TITLE_ADDONCONTENT_MAX_ENTRIES 2000 + +#define TITLE_DELTA_BASE_ID (u64)0xC00 + +/// Used to display version numbers in dot notation (major.minor.micro-major_relstep.minor_relstep). typedef struct { u32 TitleVersion_MinorRelstep : 8; u32 TitleVersion_MajorRelstep : 8; @@ -35,30 +41,35 @@ typedef struct { } TitleVersion; /// Retrieved using ns application records and/or ncm content meta keys. +/// Used by the UI to display title lists. typedef struct { - u64 title_id; ///< Title ID from the application this data belongs to. + u64 title_id; ///< Title ID from the application / system title this data belongs to. NacpLanguageEntry lang_entry; ///< UTF-8 strings in the console language. u32 icon_size; ///< JPEG icon size. - u8 *icon; ///< JPEG icon data. + u8 *icon; ///< JPEG icon data. } TitleApplicationMetadata; /// Retrieved using ncm databases. -typedef struct { - u8 storage_id; ///< NcmStorageId. - TitleVersion dot_version; ///< Holds the same value from meta_key.version. Used to display version numbers in dot notation (major.minor.micro-major_relstep.minor_relstep). - NcmContentMetaKey meta_key; ///< Used with ncm calls. - u32 content_count; ///< Content info count. - NcmContentInfo *content_infos; ///< Content info entries from this title. - u64 title_size; ///< Total title size. - char title_size_str[32]; ///< Total title size string. - TitleApplicationMetadata *app_metadata; ///< Only available for system titles and applications. - - - /* Pointers to patches / AOC? */ - - +typedef struct _TitleInfo { + u8 storage_id; ///< NcmStorageId. + TitleVersion dot_version; ///< Holds the same value from meta_key.version. + NcmContentMetaKey meta_key; ///< Used with ncm calls. + u32 content_count; ///< Content info count. + NcmContentInfo *content_infos; ///< Content info entries from this title. + u64 title_size; ///< Total title size. + char title_size_str[32]; ///< Total title size string. + TitleApplicationMetadata *app_metadata; ///< Only available for system titles and applications. + struct _TitleInfo *parent, *previous, *next; ///< Used with TitleInfo entries from patches and add-on contents. } TitleInfo; +/// Used to deal with user applications stored in the eMMC, SD card and/or gamecard. +typedef struct { + bool gamecard_available; ///< Set to true if one or more titles matching this user application are stored in the inserted gamecard. + TitleInfo *app_info; ///< Pointer to a TitleInfo element for this application. + TitleInfo *patch_info; ///< Pointer to a TitleInfo element for the first detected patch. + TitleInfo *aoc_info; ///< Pointer to a TitleInfo element for the first detected add-on content. +} TitleUserApplicationData; + /// Initializes the title interface. bool titleInitialize(void); @@ -71,15 +82,31 @@ NcmContentMetaDatabase *titleGetNcmDatabaseByStorageId(u8 storage_id); /// Returns a pointer to a ncm storage handle using a NcmStorageId value. NcmContentStorage *titleGetNcmStorageByStorageId(u8 storage_id); -/// Returns true if gamecard title info could be loaded. +/// Returns true if gamecard title info has been (un)loaded. /// Suitable for being called between UI updates. bool titleRefreshGameCardTitleInfo(void); +/// Returns a pointer to a dynamically allocated buffer of pointers to TitleApplicationMetadata entries, as well as their count. The allocated buffer must be freed by the user. +/// If 'is_system' is true, TitleApplicationMetadata entries from available system titles will be returned. +/// Otherwise, TitleApplicationMetadata entries from user applications with available content data will be returned. +/// Returns NULL if an error occurs. +TitleApplicationMetadata **titleGetApplicationMetadataEntries(bool is_system, u32 *out_count); + /// Retrieves a pointer to a TitleInfo entry with a matching storage ID and title ID. /// If NcmStorageId_Any is used, the first entry with a matching title ID is returned. /// Returns NULL if an error occurs. TitleInfo *titleGetInfoFromStorageByTitleId(u8 storage_id, u64 title_id); +/// Populates a TitleUserApplicationData element using an user application ID. +bool titleGetUserApplicationData(u64 app_id, TitleUserApplicationData *out); + + + + + + + + @@ -106,22 +133,56 @@ NX_INLINE void titleConvertU64ToNcmContentSize(const u64 *size, u8 *out) NX_INLINE u64 titleGetPatchIdByApplicationId(u64 app_id) { - return (app_id | TITLE_PATCH_ID_MASK); + return (app_id + TITLE_PATCH_BASE_ID); } NX_INLINE u64 titleGetApplicationIdByPatchId(u64 patch_id) { - return (patch_id & ~TITLE_PATCH_ID_MASK); + return (patch_id - TITLE_PATCH_BASE_ID); } NX_INLINE bool titleCheckIfPatchIdBelongsToApplicationId(u64 app_id, u64 patch_id) { - return (titleGetPatchIdByApplicationId(app_id) == patch_id); + return (patch_id == titleGetPatchIdByApplicationId(app_id)); +} + +NX_INLINE u64 titleGetAddOnContentBaseIdByApplicationId(u64 app_id) +{ + return ((app_id & TITLE_ADDONCONTENT_CONVERSION_MASK) + TITLE_ADDONCONTENT_BASE_ID); +} + +NX_INLINE u64 titleGetAddOnContentIdWithIndexByApplicationId(u64 app_id, u16 idx) +{ + return (titleGetAddOnContentBaseIdByApplicationId(app_id) + idx + 1); +} + +NX_INLINE u64 titleGetApplicationIdByAddOnContentId(u64 aoc_id) +{ + return ((aoc_id - TITLE_ADDONCONTENT_BASE_ID) & TITLE_ADDONCONTENT_CONVERSION_MASK); +} + +NX_INLINE u64 titleGetAddOnContentMaxIdByBaseId(u64 aoc_base_id) +{ + return (aoc_base_id + TITLE_ADDONCONTENT_MAX_ENTRIES + 1); +} + +NX_INLINE bool titleIsAddOnContentIdValid(u64 aoc_id, u64 aoc_base_id, u64 aoc_max_id) +{ + return (aoc_id > aoc_base_id && aoc_id < aoc_max_id); } NX_INLINE bool titleCheckIfAddOnContentIdBelongsToApplicationId(u64 app_id, u64 aoc_id) { - return ((app_id & TITLE_ADDONCONTENT_ID_MASK) == (aoc_id & TITLE_ADDONCONTENT_ID_MASK)); + u64 aoc_base_id = titleGetAddOnContentBaseIdByApplicationId(app_id); + u64 aoc_max_id = titleGetAddOnContentMaxIdByBaseId(aoc_base_id); + return titleIsAddOnContentIdValid(aoc_id, aoc_base_id, aoc_max_id); +} + +NX_INLINE bool titleCheckIfAddOnContentIdsAreSiblings(u64 aoc_id_1, u64 aoc_id_2) +{ + u64 app_id_1 = titleGetApplicationIdByAddOnContentId(aoc_id_1); + u64 app_id_2 = titleGetApplicationIdByAddOnContentId(aoc_id_2); + return (app_id_1 == app_id_2 && titleCheckIfAddOnContentIdBelongsToApplicationId(app_id_1, aoc_id_1) && titleCheckIfAddOnContentIdBelongsToApplicationId(app_id_2, aoc_id_2)); } NX_INLINE NcmContentInfo *titleGetContentInfoByTypeAndIdOffset(TitleInfo *info, u8 content_type, u8 id_offset) diff --git a/todo.txt b/todo.txt index 26b327b..6e70b45 100644 --- a/todo.txt +++ b/todo.txt @@ -19,9 +19,9 @@ todo: bktr: filelist generation functions (wrappers for romfs filelist generation functions) - title: linked lists for patch / aoc info? + title: move gamecard stuff to another thread? title: more functions for title lookup (filters, patches / aoc, etc.) title: more functions for content lookup (based on id?) - title: find a nice way to deal with orphan content + title: find a nice way to deal with *true* orphan content (no ns records from parent base game) \ No newline at end of file