mirror of
https://github.com/DarkMatterCore/nxdumptool.git
synced 2024-11-22 02:06:41 +00:00
Uses a dynamically allocated buffer to hold the CSV data, which can then be written to an output file.
Changes include: * nxdt_utils: add utilsEscapeCharacters(). * title: add titleGenerateTitleRecordsCsv(). * title: move core logic from titleGetUserApplicationData() into _titleGetUserApplicationData(). titleGetUserApplicationData() now only takes care of duplicating the retrieved data. * title: update titleGetContentInfosByGameCardContentMetaContext() to make it write the forged NcmContentInfo entry for the Meta NCA at the end of the returned buffer. * title: fix a bug in titleRefreshGameCardTitleInfo() that prevented a title info's metadata pointer to be updated after retrieving application metadata via ns. * title: update titleGenerateGameCardApplicationMetadataArray() to add a logfile warning if an application entry doesn't have a valid application metadata pointer. * title: make _titleGenerateGameCardFileName() a bit easier to read.
This commit is contained in:
parent
9c7a57e028
commit
7ed5de9201
7 changed files with 413 additions and 91 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -16,6 +16,7 @@ host/nxdumptool
|
|||
*.exe
|
||||
*.7z
|
||||
*.code-workspace
|
||||
*.csv
|
||||
|
||||
# TODO: remove this after the PoC builds are no longer needed.
|
||||
main.cpp
|
||||
|
|
|
@ -22,7 +22,7 @@ Currently planned changes for this branch include:
|
|||
* Plaintext [gamecard CardInfo area](https://switchbrew.org/wiki/XCI#CardHeaderEncryptedData) dumps. :white_check_mark:
|
||||
* [Gamecard InitialData](https://switchbrew.org/wiki/XCI#InitialData) area dumps. :white_check_mark:
|
||||
* [Gamecard CardIdSet](https://switchbrew.org/wiki/Filesystem_services#GameCardIdSet) dumps. :white_check_mark:
|
||||
* [Gamecard Hash FS partition](https://switchbrew.org/wiki/XCI#PartitionFs) dumps (in both extracted and raw inage forms). :white_check_mark:
|
||||
* [Gamecard Hash FS partition](https://switchbrew.org/wiki/XCI#PartitionFs) dumps (in both extracted and raw image forms). :white_check_mark:
|
||||
* [Lotus ASIC firmware (LAFW) blob](https://switchbrew.org/wiki/Lotus3#User_firmware) dumping from RAM. :white_check_mark:
|
||||
* Properly detect if an inserted gamecard requires a LAFW update. :white_check_mark:
|
||||
* Nintendo Submission Package (NSP) dumps for both digital and gamecard-based titles.
|
||||
|
|
|
@ -1077,6 +1077,7 @@ int main(int argc, char *argv[])
|
|||
consolePrint("______________________________\n\n");
|
||||
if (cur_menu->parent) consolePrint("press b to go back\n");
|
||||
if (g_umsDeviceCount) consolePrint("press x to safely remove all ums devices\n");
|
||||
if ((cur_menu->id == MenuId_UserTitles || cur_menu->id == MenuId_SystemTitles) && element_count) consolePrint("press y to dump csv with title info to the sd card\n");
|
||||
consolePrint("use the sticks to scroll faster\n");
|
||||
consolePrint("press + to exit\n");
|
||||
consolePrint("______________________________\n\n");
|
||||
|
@ -1513,6 +1514,43 @@ int main(int argc, char *argv[])
|
|||
for(u32 i = 0; i < g_umsDeviceCount; i++) umsUnmountDevice(&(g_umsDevices[i]));
|
||||
updateStorageList();
|
||||
} else
|
||||
if ((btn_down & HidNpadButton_Y) && (cur_menu->id == MenuId_UserTitles || cur_menu->id == MenuId_SystemTitles) && element_count)
|
||||
{
|
||||
consoleClear();
|
||||
consolePrint("dumping title info to csv, please wait...\n");
|
||||
consoleRefresh();
|
||||
|
||||
sprintf(path, DEVOPTAB_SDMC_DEVICE "/" OUTDIR "/%s_title_records.csv", cur_menu->id == MenuId_UserTitles ? "user" : "system");
|
||||
|
||||
char *csv_buf = NULL;
|
||||
size_t csv_buf_size = 0;
|
||||
u32 proc_title_cnt = 0;
|
||||
|
||||
csv_buf = titleGenerateTitleRecordsCsv(&csv_buf_size, &proc_title_cnt, cur_menu->id == MenuId_SystemTitles, false);
|
||||
if (csv_buf)
|
||||
{
|
||||
utilsCreateDirectoryTree(path, false);
|
||||
|
||||
FILE *csv_fd = fopen(path, "wb");
|
||||
if (csv_fd)
|
||||
{
|
||||
fwrite(UTF8_BOM, 1, strlen(UTF8_BOM), csv_fd);
|
||||
fwrite(csv_buf, 1, csv_buf_size, csv_fd);
|
||||
fclose(csv_fd);
|
||||
|
||||
consolePrint("title info dumped to \"%s\". %u title record(s) processed.\n", path, proc_title_cnt);
|
||||
} else {
|
||||
consolePrint("failed to open \"%s\" for writing\n", path);
|
||||
}
|
||||
|
||||
free(csv_buf);
|
||||
} else {
|
||||
consolePrint("failed to generate csv data\n");
|
||||
}
|
||||
|
||||
consolePrint("press any button to go back");
|
||||
utilsWaitForButtonPress(0);
|
||||
} else
|
||||
if (((btn_down & (HidNpadButton_L)) || (btn_held & HidNpadButton_ZL)) && (cur_menu->id == MenuId_NSP || cur_menu->id == MenuId_Ticket || cur_menu->id == MenuId_Nca) && title_info->previous)
|
||||
{
|
||||
title_info = title_info->previous;
|
||||
|
|
|
@ -128,11 +128,17 @@ void utilsJoinThread(Thread *thread);
|
|||
__attribute__((format(printf, 3, 4))) bool utilsAppendFormattedStringToBuffer(char **dst, size_t *dst_size, const char *fmt, ...);
|
||||
|
||||
/// Replaces illegal filesystem characters in the provided NULL-terminated UTF-8 string with underscores ('_').
|
||||
/// If 'ascii_only' is set to true, all codepoints outside of the [0x20,0x7F) range will also be replaced with underscores.
|
||||
/// If 'ascii_only' is set to true, all codepoints outside of the [0x20,0x7E] range will also be replaced with underscores.
|
||||
/// Replacements are performed on a per-codepoint basis, which means the string size in bytes can be reduced by this function.
|
||||
/// Furthermore, if multiple, consecutive illegal characters are found, they will all get replaced by a single underscore.
|
||||
void utilsReplaceIllegalCharacters(char *str, bool ascii_only);
|
||||
|
||||
/// Returns a pointer to a dynamically allocated copy of the provided string with all required characters escaped using another specific character.
|
||||
/// 'chars_to_escape' must represent a NULL-terminated character string with all characters that need to be escaped.
|
||||
/// Furthermore, 'escape_char' must represent an ASCII character within the [0x20,0x7E] range, and it must also not be part of 'chars_to_escape'.
|
||||
/// Returns NULL if an error occurs.
|
||||
char *utilsEscapeCharacters(const char *str, const char *chars_to_escape, const char escape_char);
|
||||
|
||||
/// Trims whitespace characters from the provided string.
|
||||
void utilsTrimString(char *str);
|
||||
|
||||
|
|
|
@ -164,6 +164,15 @@ char *titleGenerateFileName(TitleInfo *title_info, u8 naming_convention, u8 ille
|
|||
/// A valid gamecard must be inserted, and title info must have been loaded from it accordingly.
|
||||
char *titleGenerateGameCardFileName(u8 naming_convention, u8 illegal_char_replace_type);
|
||||
|
||||
/// Returns a pointer to a dynamically allocated buffer that holds a CSV representation of all available user/system title records, depending on the 'is_system' argument.
|
||||
/// 'out_csv_size' must be a valid pointer. It is used to store the size of the allocated buffer.
|
||||
/// 'out_proc_title_cnt' may optionally be provided. If available, it will be used to store the number of processed title records.
|
||||
/// If 'is_system' is false and 'use_gamecard' is true, gamecard title records will be appended to the output buffer.
|
||||
/// Both 'is_system' and 'use_gamecard' may not be set to true at the same time.
|
||||
/// Furthermore, if 'is_system' is set to false and orphan titles are available, their records will get appended to the output buffer.
|
||||
/// Returns NULL if an error occurs.
|
||||
char *titleGenerateTitleRecordsCsv(size_t *out_csv_size, u32 *out_proc_title_cnt, bool is_system, bool use_gamecard);
|
||||
|
||||
/// Returns a pointer to a string holding a user-friendly name for the provided NcmStorageId value. Returns NULL if the provided value is invalid.
|
||||
const char *titleGetNcmStorageIdName(u8 storage_id);
|
||||
|
||||
|
|
|
@ -664,6 +664,76 @@ void utilsReplaceIllegalCharacters(char *str, bool ascii_only)
|
|||
*ptr2 = '\0';
|
||||
}
|
||||
|
||||
char *utilsEscapeCharacters(const char *str, const char *chars_to_escape, const char escape_char)
|
||||
{
|
||||
size_t str_size = 0, chars_to_escape_size = 0;
|
||||
|
||||
if (!str || !(str_size = strlen(str)) || !chars_to_escape || !(chars_to_escape_size = strlen(chars_to_escape)) || \
|
||||
escape_char < 0x20 || escape_char >= 0x7F || memchr(chars_to_escape, (int)escape_char, chars_to_escape_size))
|
||||
{
|
||||
LOG_MSG_ERROR("Invalid parameters!");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ssize_t units = 0;
|
||||
u32 code = 0, escape_cnt = 0;
|
||||
const u8 *ptr = (const u8*)str;
|
||||
size_t cur_pos = 0, escaped_str_size = 0;
|
||||
char *ret = NULL;
|
||||
|
||||
/* Determine the number of character we need to escape. */
|
||||
while(cur_pos < str_size)
|
||||
{
|
||||
units = decode_utf8(&code, ptr);
|
||||
if (units < 0) break;
|
||||
|
||||
if (memchr(chars_to_escape, (int)code, chars_to_escape_size)) escape_cnt++;
|
||||
|
||||
ptr += units;
|
||||
cur_pos += (size_t)units;
|
||||
}
|
||||
|
||||
/* Short-circuit: check if we don't have to escape anything. */
|
||||
/* If so, we'll just duplicate the provided string and call it a day. */
|
||||
if (!escape_cnt)
|
||||
{
|
||||
ret = strdup(str);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Calculate escaped string size. */
|
||||
escaped_str_size = (str_size + escape_cnt);
|
||||
|
||||
/* Allocate memory for the output string. */
|
||||
ret = calloc(sizeof(char), escaped_str_size + 1);
|
||||
if (!ret)
|
||||
{
|
||||
LOG_MSG_ERROR("Failed to allocate memory for the output string! (0x%lX).", escaped_str_size + 1);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Reset current position. */
|
||||
ptr = (const u8*)str;
|
||||
cur_pos = 0;
|
||||
|
||||
/* Copy characters and deal with the ones that need to be escaped. */
|
||||
while(cur_pos < escaped_str_size)
|
||||
{
|
||||
units = decode_utf8(&code, ptr);
|
||||
if (units < 0) break;
|
||||
|
||||
if (memchr(chars_to_escape, (int)code, chars_to_escape_size)) ret[cur_pos++] = escape_char;
|
||||
|
||||
for(ssize_t i = 0; i < units; i++) ret[cur_pos + (size_t)i] = ptr[i];
|
||||
|
||||
ptr += units;
|
||||
cur_pos += (size_t)units;
|
||||
}
|
||||
|
||||
end:
|
||||
return ret;
|
||||
}
|
||||
|
||||
void utilsTrimString(char *str)
|
||||
{
|
||||
size_t strsize = 0;
|
||||
|
|
|
@ -600,6 +600,8 @@ static void titleUpdateTitleInfoLinkedLists(void);
|
|||
|
||||
static TitleInfo *_titleGetTitleInfoEntryFromStorageByTitleId(u8 storage_id, u64 title_id);
|
||||
|
||||
static bool _titleGetUserApplicationData(u64 app_id, TitleUserApplicationData *out);
|
||||
|
||||
static TitleInfo *titleDuplicateTitleInfoFull(TitleInfo *title_info, TitleInfo *previous, TitleInfo *next);
|
||||
static TitleInfo *titleDuplicateTitleInfo(TitleInfo *title_info);
|
||||
|
||||
|
@ -859,68 +861,32 @@ bool titleGetUserApplicationData(u64 app_id, TitleUserApplicationData *out)
|
|||
break;
|
||||
}
|
||||
|
||||
bool error = false;
|
||||
TitleInfo *app_info = NULL, *patch_info = NULL, *aoc_info = NULL, *aoc_patch_info = NULL;
|
||||
/* Retrieve user application data. */
|
||||
TitleUserApplicationData user_app_data = {0};
|
||||
if (!_titleGetUserApplicationData(app_id, &user_app_data)) break;
|
||||
|
||||
/* Clear output. */
|
||||
titleFreeUserApplicationData(out);
|
||||
|
||||
#define TITLE_ALLOCATE_USER_APP_DATA(elem, msg, decl) \
|
||||
if (elem##_info && !out->elem##_info) { \
|
||||
out->elem##_info = titleDuplicateTitleInfoFull(elem##_info, NULL, NULL); \
|
||||
#define TITLE_DUPLICATE_USER_APP_DATA(elem, msg) \
|
||||
if (user_app_data.elem##_info) { \
|
||||
out->elem##_info = titleDuplicateTitleInfoFull(user_app_data.elem##_info, NULL, NULL); \
|
||||
if (!out->elem##_info) { \
|
||||
LOG_MSG_ERROR("Failed to duplicate %s info for %016lX!", msg, app_id); \
|
||||
decl; \
|
||||
break; \
|
||||
} \
|
||||
}
|
||||
|
||||
/* Get info for the first user application title. */
|
||||
app_info = _titleGetTitleInfoEntryFromStorageByTitleId(NcmStorageId_Any, app_id);
|
||||
TITLE_ALLOCATE_USER_APP_DATA(app, "user application", break);
|
||||
/* Duplicate user application data. */
|
||||
TITLE_DUPLICATE_USER_APP_DATA(app, "user application");
|
||||
TITLE_DUPLICATE_USER_APP_DATA(patch, "patch");
|
||||
TITLE_DUPLICATE_USER_APP_DATA(aoc, "add-on content");
|
||||
TITLE_DUPLICATE_USER_APP_DATA(aoc_patch, "add-on content patch");
|
||||
|
||||
/* Get info for the first patch title. */
|
||||
patch_info = _titleGetTitleInfoEntryFromStorageByTitleId(NcmStorageId_Any, titleGetPatchIdByApplicationId(app_id));
|
||||
TITLE_ALLOCATE_USER_APP_DATA(patch, "patch", break);
|
||||
#undef TITLE_DUPLICATE_USER_APP_DATA
|
||||
|
||||
/* Get info for the first add-on content and add-on content patch titles. */
|
||||
for(u8 i = NcmStorageId_GameCard; i <= NcmStorageId_SdCard; i++)
|
||||
{
|
||||
if (i == NcmStorageId_BuiltInSystem) continue;
|
||||
|
||||
TitleStorage *title_storage = &(g_titleStorage[TITLE_STORAGE_INDEX(i)]);
|
||||
if (!title_storage->titles || !title_storage->title_count) continue;
|
||||
|
||||
for(u32 j = 0; j < title_storage->title_count; j++)
|
||||
{
|
||||
TitleInfo *title_info = title_storage->titles[j];
|
||||
if (!title_info) continue;
|
||||
|
||||
if (title_info->meta_key.type == NcmContentMetaType_AddOnContent && titleCheckIfAddOnContentIdBelongsToApplicationId(app_id, title_info->meta_key.id))
|
||||
{
|
||||
aoc_info = title_info;
|
||||
break;
|
||||
} else
|
||||
if (title_info->meta_key.type == NcmContentMetaType_DataPatch && titleCheckIfDataPatchIdBelongsToApplicationId(app_id, title_info->meta_key.id))
|
||||
{
|
||||
aoc_patch_info = title_info;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TITLE_ALLOCATE_USER_APP_DATA(aoc, "add-on content", error = true; break);
|
||||
|
||||
TITLE_ALLOCATE_USER_APP_DATA(aoc_patch, "add-on content patch", error = true; break);
|
||||
|
||||
if (out->aoc_info && out->aoc_patch_info) break;
|
||||
}
|
||||
|
||||
if (error) break;
|
||||
|
||||
#undef TITLE_ALLOCATE_USER_APP_DATA
|
||||
|
||||
/* Check retrieved title info. */
|
||||
ret = (app_info || patch_info || aoc_info || aoc_patch_info);
|
||||
if (!ret) LOG_MSG_ERROR("Failed to retrieve user application data for ID \"%016lX\"!", app_id);
|
||||
/* Update return value. */
|
||||
ret = true;
|
||||
}
|
||||
|
||||
/* Clear output. */
|
||||
|
@ -1168,6 +1134,183 @@ char *titleGenerateGameCardFileName(u8 naming_convention, u8 illegal_char_replac
|
|||
return filename;
|
||||
}
|
||||
|
||||
char *titleGenerateTitleRecordsCsv(size_t *out_csv_size, u32 *out_proc_title_cnt, bool is_system, bool use_gamecard)
|
||||
{
|
||||
char *csv_buf = NULL;
|
||||
size_t csv_buf_size = 0;
|
||||
|
||||
SCOPED_LOCK(&g_titleMutex)
|
||||
{
|
||||
TitleApplicationMetadata **filtered_app_metadata = (is_system ? g_filteredSystemMetadata : g_filteredUserMetadata);
|
||||
u32 filtered_app_metadata_count = (is_system ? g_filteredSystemMetadataCount : g_filteredUserMetadataCount);
|
||||
|
||||
TitleApplicationMetadata *cur_app_metadata = NULL;
|
||||
TitleUserApplicationData user_app_data = {0};
|
||||
TitleInfo *title_info = NULL;
|
||||
char *escaped_title_name = NULL;
|
||||
|
||||
u32 proc_title_cnt = 0;
|
||||
|
||||
bool success = false;
|
||||
|
||||
if (!g_titleInterfaceInit || !filtered_app_metadata || !filtered_app_metadata_count || !out_csv_size || (is_system && use_gamecard))
|
||||
{
|
||||
LOG_MSG_ERROR("Invalid parameters!");
|
||||
break;
|
||||
}
|
||||
|
||||
#define TITLE_CSV_ADD_FMT_STR(fmt, ...) utilsAppendFormattedStringToBuffer(&csv_buf, &csv_buf_size, fmt, ##__VA_ARGS__)
|
||||
|
||||
/* Append CSV header. */
|
||||
if (!TITLE_CSV_ADD_FMT_STR("Name,Type,Title ID,Version,Source Storage,Content Count,Size\r\n")) goto end;
|
||||
|
||||
/* Loop through our filtered application metadata entries. */
|
||||
for(u32 i = 0; i < filtered_app_metadata_count; i++)
|
||||
{
|
||||
/* Get current application metadata entry. */
|
||||
cur_app_metadata = filtered_app_metadata[i];
|
||||
if (!cur_app_metadata) continue;
|
||||
|
||||
/* Escape title name, if needed. */
|
||||
if (strchr(cur_app_metadata->lang_entry.name, ',') != NULL || strchr(cur_app_metadata->lang_entry.name, '"') != NULL)
|
||||
{
|
||||
escaped_title_name = utilsEscapeCharacters(cur_app_metadata->lang_entry.name, "\"", '"');
|
||||
if (!escaped_title_name)
|
||||
{
|
||||
LOG_MSG_ERROR("Failed to generate escaped title name for %016lX!", cur_app_metadata->title_id);
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_system)
|
||||
{
|
||||
/* Get title info entry for our current system title. */
|
||||
title_info = _titleGetTitleInfoEntryFromStorageByTitleId(NcmStorageId_BuiltInSystem, cur_app_metadata->title_id);
|
||||
if (!title_info) continue;
|
||||
|
||||
/* Append title name. */
|
||||
if (!TITLE_CSV_ADD_FMT_STR(escaped_title_name ? "\"%s\"," : "%s,", escaped_title_name ? escaped_title_name : cur_app_metadata->lang_entry.name))
|
||||
{
|
||||
LOG_MSG_ERROR("Failed to append title name for %016lX!", cur_app_metadata->title_id);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Append title record to output CSV buffer. */
|
||||
if (!TITLE_CSV_ADD_FMT_STR("%s,%016lX,%u,%s,%u,%s (%lu bytes)\r\n", titleGetNcmContentMetaTypeName(title_info->meta_key.type), title_info->meta_key.id, \
|
||||
title_info->version.value, titleGetNcmStorageIdName(title_info->storage_id), \
|
||||
title_info->content_count, title_info->size_str, title_info->size))
|
||||
{
|
||||
LOG_MSG_ERROR("Failed to append title record for %016lX!", cur_app_metadata->title_id);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Increase processed titles counter. */
|
||||
proc_title_cnt++;
|
||||
} else {
|
||||
/* Retrieve user application data. */
|
||||
if (!_titleGetUserApplicationData(cur_app_metadata->title_id, &user_app_data)) continue;
|
||||
|
||||
/* Process all title types available in the retrieved user application data, in order. */
|
||||
for(u8 j = NcmContentMetaType_Application; j <= NcmContentMetaType_DataPatch; j++)
|
||||
{
|
||||
if (j == NcmContentMetaType_Delta) continue;
|
||||
|
||||
/* Get the right title info pointer for the current title type. */
|
||||
title_info = (j == NcmContentMetaType_Application ? user_app_data.app_info : \
|
||||
(j == NcmContentMetaType_Patch ? user_app_data.patch_info : \
|
||||
(j == NcmContentMetaType_AddOnContent ? user_app_data.aoc_info : user_app_data.aoc_patch_info)));
|
||||
|
||||
/* Process title info linked list. */
|
||||
while(title_info)
|
||||
{
|
||||
/* Skip current entry if we're not supposed to process gamecard-based titles. */
|
||||
if (title_info->storage_id == NcmStorageId_GameCard && !use_gamecard)
|
||||
{
|
||||
title_info = title_info->next;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Append title name. */
|
||||
if (!TITLE_CSV_ADD_FMT_STR(escaped_title_name ? "\"%s\"," : "%s,", escaped_title_name ? escaped_title_name : cur_app_metadata->lang_entry.name))
|
||||
{
|
||||
LOG_MSG_ERROR("Failed to append title name for %016lX!", cur_app_metadata->title_id);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Append title record to output CSV buffer. */
|
||||
if (!TITLE_CSV_ADD_FMT_STR("%s,%016lX,%u,%s,%u,%s (%lu bytes)\r\n", titleGetNcmContentMetaTypeName(title_info->meta_key.type), title_info->meta_key.id, \
|
||||
title_info->version.value, titleGetNcmStorageIdName(title_info->storage_id), \
|
||||
title_info->content_count, title_info->size_str, title_info->size))
|
||||
{
|
||||
LOG_MSG_ERROR("Failed to append title record for %016lX!", cur_app_metadata->title_id);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Increase processed titles counter. */
|
||||
proc_title_cnt++;
|
||||
|
||||
/* Get next pointer in the current linked list. */
|
||||
title_info = title_info->next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Free escaped title name. */
|
||||
if (escaped_title_name) free(escaped_title_name);
|
||||
escaped_title_name = NULL;
|
||||
}
|
||||
|
||||
/* Check if any orphan titles are available. */
|
||||
if (!is_system && titleAreOrphanTitlesAvailable())
|
||||
{
|
||||
/* Loop through our orphan title entries. */
|
||||
for(u32 i = 0; i < g_orphanTitleInfoCount; i++)
|
||||
{
|
||||
title_info = g_orphanTitleInfo[i];
|
||||
if (!title_info) continue;
|
||||
|
||||
/* Append title record to output CSV buffer. */
|
||||
if (!TITLE_CSV_ADD_FMT_STR("[UNKNOWN],%s,%016lX,%u,%s,%u,%s (%lu bytes)\r\n", titleGetNcmContentMetaTypeName(title_info->meta_key.type), title_info->meta_key.id, \
|
||||
title_info->version.value, titleGetNcmStorageIdName(title_info->storage_id), \
|
||||
title_info->content_count, title_info->size_str, title_info->size))
|
||||
{
|
||||
LOG_MSG_ERROR("Failed to append orphan title record for %016lX!", cur_app_metadata->title_id);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Increase processed titles counter. */
|
||||
proc_title_cnt++;
|
||||
}
|
||||
}
|
||||
|
||||
#undef TITLE_CSV_ADD_FMT_STR
|
||||
|
||||
/* Check if we actually processed any titles. */
|
||||
if (proc_title_cnt)
|
||||
{
|
||||
/* Update output. */
|
||||
*out_csv_size = strlen(csv_buf);
|
||||
if (out_proc_title_cnt) *out_proc_title_cnt = proc_title_cnt;
|
||||
|
||||
/* Update flag. */
|
||||
success = true;
|
||||
} else {
|
||||
LOG_MSG_INFO("No %s titles were processed.", is_system ? "system" : "user");
|
||||
}
|
||||
|
||||
end:
|
||||
if (escaped_title_name) free(escaped_title_name);
|
||||
|
||||
if (!success && csv_buf)
|
||||
{
|
||||
free(csv_buf);
|
||||
csv_buf = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return csv_buf;
|
||||
}
|
||||
|
||||
const char *titleGetNcmStorageIdName(u8 storage_id)
|
||||
{
|
||||
return (storage_id <= NcmStorageId_Any ? g_titleNcmStorageIdNames[storage_id] : NULL);
|
||||
|
@ -2804,43 +2947,36 @@ static bool titleGetContentInfosByGameCardContentMetaContext(TitleGameCardConten
|
|||
}
|
||||
|
||||
/* Loop through our NcmPackagedContentInfo entries. */
|
||||
for(u32 i = 0; i < content_count; i++)
|
||||
for(u32 i = 0; i < (content_count - 1); i++)
|
||||
{
|
||||
if (i == 0)
|
||||
char nca_filename[0x30] = {0};
|
||||
NcmContentInfo *cur_content_info = &(gc_meta_ctx->cnmt_ctx.packaged_content_info[i].info);
|
||||
|
||||
/* Make sure this content exists on the inserted gamecard. */
|
||||
utilsGenerateHexString(nca_filename, sizeof(nca_filename), cur_content_info->content_id.c, sizeof(cur_content_info->content_id.c), false);
|
||||
strcat(nca_filename, cur_content_info->content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
|
||||
|
||||
if (!hfsGetEntryByName(hfs_ctx, nca_filename))
|
||||
{
|
||||
/* Reserve the very first content info entry for our Meta NCA. */
|
||||
NcmContentInfo *cur_content_info = &(content_infos[0]);
|
||||
|
||||
memcpy(&(cur_content_info->content_id), &(gc_meta_ctx->nca_ctx.content_id), sizeof(NcmContentId));
|
||||
ncmU64ToContentInfoSize(gc_meta_ctx->nca_ctx.content_size, cur_content_info);
|
||||
cur_content_info->attr = 0;
|
||||
cur_content_info->content_type = NcmContentType_Meta;
|
||||
cur_content_info->id_offset = 0;
|
||||
|
||||
//LOG_DATA_DEBUG(cur_content_info, sizeof(NcmContentInfo), "Forged Meta content record:");
|
||||
} else {
|
||||
char nca_filename[0x30] = {0};
|
||||
NcmContentInfo *cur_content_info = &(gc_meta_ctx->cnmt_ctx.packaged_content_info[i - 1].info);
|
||||
|
||||
/* Make sure this content exists on the inserted gamecard. */
|
||||
utilsGenerateHexString(nca_filename, sizeof(nca_filename), cur_content_info->content_id.c, sizeof(cur_content_info->content_id.c), false);
|
||||
strcat(nca_filename, cur_content_info->content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
|
||||
|
||||
if (!hfsGetEntryByName(hfs_ctx, nca_filename))
|
||||
{
|
||||
LOG_MSG_DEBUG("Unable to locate %s NCA \"%s\" in Hash FS by name (%s partition).", titleGetNcmContentTypeName(cur_content_info->content_type), nca_filename, \
|
||||
hfsGetPartitionNameString(hfs_ctx->type));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Copy content info data. */
|
||||
memcpy(&(content_infos[available_count]), cur_content_info, sizeof(NcmContentInfo));
|
||||
LOG_MSG_DEBUG("Unable to locate %s NCA \"%s\" in Hash FS by name (%s partition).", titleGetNcmContentTypeName(cur_content_info->content_type), nca_filename, \
|
||||
hfsGetPartitionNameString(hfs_ctx->type));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Update available content count. */
|
||||
available_count++;
|
||||
/* Copy content info data and update available content count. */
|
||||
memcpy(&(content_infos[available_count++]), cur_content_info, sizeof(NcmContentInfo));
|
||||
}
|
||||
|
||||
/* Reserve the very last content info entry for our Meta NCA. Update the available content count while we're at it. */
|
||||
NcmContentInfo *meta_content_info = &(content_infos[available_count++]);
|
||||
memcpy(&(meta_content_info->content_id), &(gc_meta_ctx->nca_ctx.content_id), sizeof(NcmContentId));
|
||||
ncmU64ToContentInfoSize(gc_meta_ctx->nca_ctx.content_size, meta_content_info);
|
||||
meta_content_info->attr = 0;
|
||||
meta_content_info->content_type = NcmContentType_Meta;
|
||||
meta_content_info->id_offset = 0;
|
||||
|
||||
//LOG_DATA_DEBUG(meta_content_info, sizeof(NcmContentInfo), "Forged Meta content record:");
|
||||
|
||||
if (available_count < content_count)
|
||||
{
|
||||
/* Reallocate output buffer, if needed. */
|
||||
|
@ -2981,6 +3117,60 @@ static TitleInfo *_titleGetTitleInfoEntryFromStorageByTitleId(u8 storage_id, u64
|
|||
return out;
|
||||
}
|
||||
|
||||
static bool _titleGetUserApplicationData(u64 app_id, TitleUserApplicationData *out)
|
||||
{
|
||||
if (!app_id || !out)
|
||||
{
|
||||
LOG_MSG_ERROR("Invalid parameters!");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ret = false;
|
||||
|
||||
/* Clear output. */
|
||||
memset(out, 0, sizeof(TitleUserApplicationData));
|
||||
|
||||
/* Get info for the first user application title. */
|
||||
out->app_info = _titleGetTitleInfoEntryFromStorageByTitleId(NcmStorageId_Any, app_id);
|
||||
|
||||
/* Get info for the first patch title. */
|
||||
out->patch_info = _titleGetTitleInfoEntryFromStorageByTitleId(NcmStorageId_Any, titleGetPatchIdByApplicationId(app_id));
|
||||
|
||||
/* Get info for the first add-on content and add-on content patch titles. */
|
||||
for(u8 i = NcmStorageId_GameCard; i <= NcmStorageId_SdCard; i++)
|
||||
{
|
||||
if (i == NcmStorageId_BuiltInSystem) continue;
|
||||
|
||||
TitleStorage *title_storage = &(g_titleStorage[TITLE_STORAGE_INDEX(i)]);
|
||||
if (!title_storage->titles || !title_storage->title_count) continue;
|
||||
|
||||
for(u32 j = 0; j < title_storage->title_count; j++)
|
||||
{
|
||||
TitleInfo *title_info = title_storage->titles[j];
|
||||
if (!title_info) continue;
|
||||
|
||||
if (!out->aoc_info && title_info->meta_key.type == NcmContentMetaType_AddOnContent && titleCheckIfAddOnContentIdBelongsToApplicationId(app_id, title_info->meta_key.id))
|
||||
{
|
||||
out->aoc_info = title_info;
|
||||
} else
|
||||
if (!out->aoc_patch_info && title_info->meta_key.type == NcmContentMetaType_DataPatch && titleCheckIfDataPatchIdBelongsToApplicationId(app_id, title_info->meta_key.id))
|
||||
{
|
||||
out->aoc_patch_info = title_info;
|
||||
}
|
||||
|
||||
if (out->aoc_info && out->aoc_patch_info) break;
|
||||
}
|
||||
|
||||
if (out->aoc_info && out->aoc_patch_info) break;
|
||||
}
|
||||
|
||||
/* Check retrieved title info. */
|
||||
ret = (out->app_info || out->patch_info || out->aoc_info || out->aoc_patch_info);
|
||||
if (!ret) LOG_MSG_ERROR("Failed to retrieve user application data for ID \"%016lX\"!", app_id);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static TitleInfo *titleDuplicateTitleInfoFull(TitleInfo *title_info, TitleInfo *previous, TitleInfo *next)
|
||||
{
|
||||
if (!titleIsValidInfoBlock(title_info))
|
||||
|
@ -3317,11 +3507,11 @@ static bool titleRefreshGameCardTitleInfo(void)
|
|||
if (cur_title_info->app_metadata != NULL || (cur_title_info->app_metadata = titleFindApplicationMetadataByTitleId(app_id, false, extra_app_count)) != NULL) continue;
|
||||
|
||||
/* Retrieve application metadata. */
|
||||
TitleApplicationMetadata *cur_app_metadata = titleGenerateUserMetadataEntryFromNs(app_id);
|
||||
if (!cur_app_metadata) continue;
|
||||
cur_title_info->app_metadata = titleGenerateUserMetadataEntryFromNs(app_id);
|
||||
if (!cur_title_info->app_metadata) continue;
|
||||
|
||||
/* Set application metadata entry pointer. */
|
||||
g_userMetadata[g_userMetadataCount + extra_app_count] = cur_app_metadata;
|
||||
g_userMetadata[g_userMetadataCount + extra_app_count] = cur_title_info->app_metadata;
|
||||
|
||||
/* Increase extra application metadata counter. */
|
||||
extra_app_count++;
|
||||
|
@ -3398,6 +3588,13 @@ static void titleGenerateGameCardApplicationMetadataArray(void)
|
|||
TitleInfo *app_info = titles[i], *patch_info = NULL;
|
||||
if (!app_info || app_info->meta_key.type != NcmContentMetaType_Application) continue;
|
||||
|
||||
/* Don't proceed any further if, for some reason, we were unable to retrieve any metadata for this application. */
|
||||
if (!app_info->app_metadata)
|
||||
{
|
||||
LOG_MSG_WARNING("No application metadata record available for %016lX!", app_info->meta_key.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
u32 app_version = app_info->meta_key.version;
|
||||
u32 dlc_count = 0;
|
||||
|
||||
|
@ -3515,6 +3712,7 @@ static char *_titleGenerateGameCardFileName(u8 naming_convention)
|
|||
for(u32 i = 0; i < g_titleGameCardApplicationMetadataCount; i++)
|
||||
{
|
||||
const TitleGameCardApplicationMetadata *cur_gc_app_metadata = &(g_titleGameCardApplicationMetadata[i]);
|
||||
const TitleApplicationMetadata *cur_app_metadata = cur_gc_app_metadata->app_metadata;
|
||||
|
||||
/* Generate current user application name. */
|
||||
*app_name = '\0';
|
||||
|
@ -3523,10 +3721,10 @@ static char *_titleGenerateGameCardFileName(u8 naming_convention)
|
|||
{
|
||||
if (cur_filename_len) strcat(app_name, " + ");
|
||||
|
||||
if (cur_gc_app_metadata->app_metadata && cur_gc_app_metadata->app_metadata->lang_entry.name[0])
|
||||
if (cur_app_metadata->lang_entry.name[0])
|
||||
{
|
||||
app_name_len = strlen(app_name);
|
||||
snprintf(app_name + app_name_len, MAX_ELEMENTS(app_name) - app_name_len, "%s ", cur_gc_app_metadata->app_metadata->lang_entry.name);
|
||||
snprintf(app_name + app_name_len, MAX_ELEMENTS(app_name) - app_name_len, "%s ", cur_app_metadata->lang_entry.name);
|
||||
|
||||
/* Append display version string if the inserted gamecard holds a patch for the current user application. */
|
||||
if (cur_gc_app_metadata->has_patch && cur_gc_app_metadata->display_version[0])
|
||||
|
@ -3537,13 +3735,13 @@ static char *_titleGenerateGameCardFileName(u8 naming_convention)
|
|||
}
|
||||
|
||||
app_name_len = strlen(app_name);
|
||||
snprintf(app_name + app_name_len, MAX_ELEMENTS(app_name) - app_name_len, "[%016lX][v%u]", cur_gc_app_metadata->app_metadata->title_id, cur_gc_app_metadata->version.value);
|
||||
snprintf(app_name + app_name_len, MAX_ELEMENTS(app_name) - app_name_len, "[%016lX][v%u]", cur_app_metadata->title_id, cur_gc_app_metadata->version.value);
|
||||
} else
|
||||
if (naming_convention == TitleNamingConvention_IdAndVersionOnly)
|
||||
{
|
||||
if (cur_filename_len) strcat(app_name, "+");
|
||||
app_name_len = strlen(app_name);
|
||||
snprintf(app_name + app_name_len, MAX_ELEMENTS(app_name) - app_name_len, "%016lX_v%u", cur_gc_app_metadata->app_metadata->title_id, cur_gc_app_metadata->version.value);
|
||||
snprintf(app_name + app_name_len, MAX_ELEMENTS(app_name) - app_name_len, "%016lX_v%u", cur_app_metadata->title_id, cur_gc_app_metadata->version.value);
|
||||
}
|
||||
|
||||
/* Reallocate output buffer. */
|
||||
|
|
Loading…
Reference in a new issue