diff --git a/.gitignore b/.gitignore index 58c3e06..c878aa4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ build /*.pfs0 /*.lst /*.tar.bz2 +/code_templates/tmp/* +/source/main.c # Clion files .idea diff --git a/code_templates/system_title_dumper.c b/code_templates/system_title_dumper.c index 5d96641..06439ac 100644 --- a/code_templates/system_title_dumper.c +++ b/code_templates/system_title_dumper.c @@ -23,6 +23,7 @@ #include "title.h" #include "pfs.h" #include "romfs.h" +#include "cnmt.h" #define BLOCK_SIZE 0x800000 #define OUTPATH "sdmc:/systitle_dumps" @@ -354,6 +355,42 @@ int main(int argc, char *argv[]) { consolePrint("nca initialize ctx failed\n"); error = true; + } else { + if (nca_ctx->content_type == NcmContentType_Meta) + { + ContentMetaContext cnmt_ctx = {0}; + FILE *cnmt_fd = NULL; + size_t path_len = 0; + + snprintf(path, sizeof(path), OUTPATH "/%016lX - %s/%s (%s)", cur_title_info->meta_key.id, cur_title_info->app_metadata->lang_entry.name, nca_ctx->content_id_str, \ + titleGetNcmContentTypeName(nca_ctx->content_type)); + utilsCreateDirectoryTree(path, true); + path_len = strlen(path); + + if (cnmtInitializeContext(&cnmt_ctx, nca_ctx)) + { + snprintf(path + path_len, sizeof(path) - path_len, "/%s", cnmt_ctx.cnmt_filename); + cnmt_fd = fopen(path, "wb"); + if (cnmt_fd) + { + fwrite(cnmt_ctx.raw_data, 1, cnmt_ctx.raw_data_size, cnmt_fd); + fclose(cnmt_fd); + cnmt_fd = NULL; + } + + path[path_len] = '\0'; + snprintf(path + path_len, sizeof(path) - path_len, "/%s.ctx", cnmt_ctx.cnmt_filename); + cnmt_fd = fopen(path, "wb"); + if (cnmt_fd) + { + fwrite(&cnmt_ctx, 1, sizeof(ContentMetaContext), cnmt_fd); + fclose(cnmt_fd); + cnmt_fd = NULL; + } + + cnmtFreeContext(&cnmt_ctx); + } + } } } else if (menu == 3) diff --git a/source/cnmt.c b/source/cnmt.c new file mode 100644 index 0000000..514c806 --- /dev/null +++ b/source/cnmt.c @@ -0,0 +1,279 @@ +/* + * cnmt.c + * + * Copyright (c) 2020, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * nxdumptool is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "utils.h" +#include "cnmt.h" +#include "title.h" + +#define CNMT_MINIMUM_FILENAME_LENGTH 23 /* Content Meta Type + "_" + Title ID + ".cnmt". */ + +/* Function prototypes. */ + +static bool cnmtGetContentMetaTypeAndTitleIdFromFileName(const char *cnmt_filename, size_t cnmt_filename_len, u8 *out_content_meta_type, u64 *out_title_id); + +bool cnmtInitializeContext(ContentMetaContext *out, NcaContext *nca_ctx) +{ + if (!out || !nca_ctx || !strlen(nca_ctx->content_id_str) || nca_ctx->content_type != NcmContentType_Meta || nca_ctx->content_size < NCA_FULL_HEADER_LENGTH || \ + (nca_ctx->storage_id != NcmStorageId_GameCard && !nca_ctx->ncm_storage) || (nca_ctx->storage_id == NcmStorageId_GameCard && !nca_ctx->gamecard_offset) || \ + nca_ctx->header.content_type != NcaContentType_Meta || !out) + { + LOGFILE("Invalid parameters!"); + return false; + } + + u32 i = 0, pfs_entry_count = 0; + size_t cnmt_filename_len = 0; + + u8 content_meta_type = 0; + u64 title_id = 0, cur_offset = 0; + + bool success = false, invalid_ext_header_size = false, invalid_ext_data_size = false; + + /* Free output context beforehand. */ + cnmtFreeContext(out); + + /* Initialize Partition FS context. */ + if (!pfsInitializeContext(&(out->pfs_ctx), &(nca_ctx->fs_contexts[0]))) + { + LOGFILE("Failed to initialize Partition FS context!"); + goto end; + } + + /* Get Partition FS entry count. */ + if (!(pfs_entry_count = pfsGetEntryCount(&(out->pfs_ctx)))) + { + LOGFILE("Partition FS has no file entries!"); + goto end; + } + + /* Look for the '.cnmt' file entry index. */ + for(i = 0; i < pfs_entry_count; i++) + { + if ((out->cnmt_filename = pfsGetEntryNameByIndex(&(out->pfs_ctx), i)) && (cnmt_filename_len = strlen(out->cnmt_filename)) >= CNMT_MINIMUM_FILENAME_LENGTH && \ + !strncasecmp(out->cnmt_filename + cnmt_filename_len - 5, ".cnmt", 5)) break; + } + + if (i >= pfs_entry_count) + { + LOGFILE("'.cnmt' entry unavailable in Partition FS!"); + goto end; + } + + //LOGFILE("Found '.cnmt' entry \"%s\" in Meta NCA \"%s\".", out->cnmt_filename, nca_ctx->content_id_str); + + /* Retrieve content meta type and title ID from the '.cnmt' filename. */ + if (!cnmtGetContentMetaTypeAndTitleIdFromFileName(out->cnmt_filename, cnmt_filename_len, &content_meta_type, &title_id)) goto end; + + /* Get '.cnmt' file entry. */ + if (!(out->pfs_entry = pfsGetEntryByIndex(&(out->pfs_ctx), i))) + { + LOGFILE("Failed to get '.cnmt' entry from Partition FS!"); + goto end; + } + + /* Check raw CNMT size. */ + if (!out->pfs_entry->size) + { + LOGFILE("Invalid raw CNMT size!"); + goto end; + } + + /* Allocate memory for the raw CNMT data. */ + out->raw_data_size = out->pfs_entry->size; + out->raw_data = malloc(out->raw_data_size); + if (!out->raw_data) + { + LOGFILE("Failed to allocate memory for the raw CNMT data!"); + goto end; + } + + /* Read raw CNMT data into memory buffer. */ + if (!pfsReadEntryData(&(out->pfs_ctx), out->pfs_entry, out->raw_data, out->raw_data_size, 0)) + { + LOGFILE("Failed to read raw CNMT data!"); + goto end; + } + + /* Save pointer to NCA context to the output CNMT context. */ + out->nca_ctx = nca_ctx; + + /* Verify packaged header. */ + out->packaged_header = (ContentMetaPackagedHeader*)out->raw_data; + cur_offset += sizeof(ContentMetaPackagedHeader); + + if (out->packaged_header->title_id != title_id) + { + LOGFILE("CNMT title ID mismatch! (%016lX != %016lX).", out->packaged_header->title_id, title_id); + goto end; + } + + if (out->packaged_header->content_meta_type != content_meta_type) + { + LOGFILE("CNMT content meta type mismatch! (0x%02X != 0x%02X).", out->packaged_header->content_meta_type, content_meta_type); + goto end; + } + + if (!out->packaged_header->content_count) + { + LOGFILE("Invalid content count!"); + goto end; + } + + if ((out->packaged_header->content_meta_type == NcmContentMetaType_SystemUpdate && !out->packaged_header->content_meta_count) || \ + (out->packaged_header->content_meta_type != NcmContentMetaType_SystemUpdate && out->packaged_header->content_meta_count)) + { + LOGFILE("Invalid content meta count!"); + goto end; + } + + /* Save pointer to extended header */ + if (out->packaged_header->extended_header_size) + { + out->extended_header = (out->raw_data + cur_offset); + cur_offset += out->packaged_header->extended_header_size; + + switch(out->packaged_header->content_meta_type) + { + case NcmContentMetaType_SystemUpdate: + invalid_ext_header_size = (out->packaged_header->extended_header_size != (u16)sizeof(ContentMetaSystemUpdateMetaExtendedHeader)); + out->extended_data_size = (!invalid_ext_header_size ? ((ContentMetaSystemUpdateMetaExtendedHeader*)out->extended_header)->extended_data_size : 0); + invalid_ext_data_size = (out->extended_data_size <= sizeof(ContentMetaSystemUpdateMetaExtendedDataHeader)); + break; + case NcmContentMetaType_Application: + invalid_ext_header_size = (out->packaged_header->extended_header_size != (u16)sizeof(ContentMetaApplicationMetaExtendedHeader)); + break; + case NcmContentMetaType_Patch: + invalid_ext_header_size = (out->packaged_header->extended_header_size != (u16)sizeof(ContentMetaPatchMetaExtendedHeader)); + out->extended_data_size = (!invalid_ext_header_size ? ((ContentMetaPatchMetaExtendedHeader*)out->extended_header)->extended_data_size : 0); + invalid_ext_data_size = (out->extended_data_size <= sizeof(ContentMetaPatchMetaExtendedDataHeader)); + break; + case NcmContentMetaType_AddOnContent: + invalid_ext_header_size = (out->packaged_header->extended_header_size != (u16)sizeof(ContentMetaAddOnContentMetaExtendedHeader)); + break; + case NcmContentMetaType_Delta: + invalid_ext_header_size = (out->packaged_header->extended_header_size != (u16)sizeof(ContentMetaDeltaMetaExtendedHeader)); + out->extended_data_size = (!invalid_ext_header_size ? ((ContentMetaDeltaMetaExtendedHeader*)out->extended_header)->extended_data_size : 0); + invalid_ext_data_size = (out->extended_data_size <= sizeof(ContentMetaDeltaMetaExtendedDataHeader)); + break; + default: + invalid_ext_header_size = (out->packaged_header->extended_header_size > 0); + break; + } + + if (invalid_ext_header_size) + { + LOGFILE("Invalid extended header size!"); + goto end; + } + + if (invalid_ext_data_size) + { + LOGFILE("Invalid extended data size!"); + goto end; + } + } + + /* Save pointer to packaged content infos. */ + out->packaged_content_info = (NcmPackagedContentInfo*)(out->raw_data + cur_offset); + cur_offset += (out->packaged_header->content_count * sizeof(NcmPackagedContentInfo)); + + /* Save pointer to content meta infos. */ + if (out->packaged_header->content_meta_count) + { + out->content_meta_info = (NcmContentMetaInfo*)(out->raw_data + cur_offset); + cur_offset += (out->packaged_header->content_meta_count * sizeof(NcmContentMetaInfo)); + } + + /* Save pointer to the extended data block. */ + if (out->extended_data_size) + { + out->extended_data = (out->raw_data + cur_offset); + cur_offset += out->extended_data_size; + } + + /* Save pointer to digest. */ + out->digest = (out->raw_data + cur_offset); + cur_offset += CNMT_DIGEST_SIZE; + + /* Safety check: verify raw CNMT size. */ + if (cur_offset != out->raw_data_size) + { + LOGFILE("Raw CNMT size mismatch! (0x%X != 0x%X).", cur_offset, out->raw_data_size); + goto end; + } + + success = true; + +end: + if (!success) cnmtFreeContext(out); + + return success; +} + +static bool cnmtGetContentMetaTypeAndTitleIdFromFileName(const char *cnmt_filename, size_t cnmt_filename_len, u8 *out_content_meta_type, u64 *out_title_id) +{ + if (!cnmt_filename || cnmt_filename_len < CNMT_MINIMUM_FILENAME_LENGTH || !out_content_meta_type || !out_title_id) + { + LOGFILE("Invalid parameters!"); + return false; + } + + u8 i = 0; + const char *pch1 = NULL, *pch2 = NULL; + size_t content_meta_type_str_len = 0; + + pch1 = (const char*)strstr(cnmt_filename, "_"); + pch2 = (cnmt_filename + cnmt_filename_len - 5); + + if (!pch1 || !(content_meta_type_str_len = (pch1 - cnmt_filename)) || (pch2 - ++pch1) != 16) + { + LOGFILE("Invalid '.cnmt' filename in Partition FS!"); + return false; + } + + for(i = NcmContentMetaType_SystemProgram; i <= NcmContentMetaType_Delta; i++) + { + if (i > NcmContentMetaType_BootImagePackageSafe && i < NcmContentMetaType_Application) + { + i = (NcmContentMetaType_Application - 1); + continue; + } + + if (!strncasecmp(cnmt_filename, titleGetNcmContentMetaTypeName(i), content_meta_type_str_len)) + { + *out_content_meta_type = i; + break; + } + } + + if (i > NcmContentMetaType_Delta) + { + LOGFILE("Invalid content meta type \"%.*s\" in '.cnmt' filename!", (int)content_meta_type_str_len, cnmt_filename); + return false; + } + + if (!(*out_title_id = strtoull(pch1, NULL, 16))) + { + LOGFILE("Invalid title ID in '.cnmt' filename!"); + return false; + } + + return true; +} diff --git a/source/cnmt.h b/source/cnmt.h new file mode 100644 index 0000000..64888c6 --- /dev/null +++ b/source/cnmt.h @@ -0,0 +1,258 @@ +/* + * cnmt.h + * + * Copyright (c) 2020, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * nxdumptool is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#ifndef __CNMT_H__ +#define __CNMT_H__ + +#include "nca.h" +#include "pfs.h" + +#define CNMT_DIGEST_SIZE SHA256_HASH_SIZE + +/// Used to display version numbers in dot notation: "{Major}.{Minor}.{Micro}-{MajorRelstep}.{MinorRelstep}". +typedef struct { + u32 ContentMetaVersion_MinorRelstep : 8; + u32 ContentMetaVersion_MajorRelstep : 8; + u32 ContentMetaVersion_Micro : 4; + u32 ContentMetaVersion_Minor : 6; + u32 ContentMetaVersion_Major : 6; +} ContentMetaVersion; + +/// Equivalent to NcmContentMetaAttribute. +typedef enum { + ContentMetaAttribute_IncludesExFatDriver = BIT(0), + ContentMetaAttribute_Rebootless = BIT(1), + ContentMetaAttribute_Compacted = BIT(2) +} ContentMetaAttribute; + +typedef struct { + u8 ContentMetaInstallState_Committed : 1; + u8 ContentMetaInstallState_Reserved : 7; +} ContentMetaInstallState; + +/// Extended variation of NcmContentMetaHeader. This is essentially the start of every CNMT file. +/// Depending on the content meta type value, this header may be followed by an additional extended header for that specific content meta type. +/// NcmPackagedContentInfo and/or NcmContentMetaInfo entries may follow afterwards. +/// If the extended data size field from the extended header (if available) is non-zero, this data is saved after the content info entries. +/// Finally, a 0x20 byte long digest is appended to the EOF. +typedef struct { + u64 title_id; + ContentMetaVersion version; + u8 content_meta_type; ///< NcmContentMetaType. + u8 reserved_1; + u16 extended_header_size; ///< Must match the size from the extended header struct for this content meta type (SystemUpdate, Application, Patch, AddOnContent, Delta). + u16 content_count; ///< Determines how many NcmPackagedContentInfo entries are available after the extended header. + u16 content_meta_count; ///< Determines how many NcmContentMetaInfo entries are available after the NcmPackagedContentInfo entries. Only used for SystemUpdate. + u8 content_meta_attribute; ///< ContentMetaAttribute. + u8 storage_id; ///< NcmStorageId. + u8 content_install_type; ///< NcmContentInstallType. + ContentMetaInstallState install_state; + ContentMetaVersion required_download_system_version; + u8 reserved_2[0x4]; +} ContentMetaPackagedHeader; + +/// Extended header for the SystemUpdate title. +/// Equivalent to NcmSystemUpdateMetaExtendedHeader. +typedef struct { + u32 extended_data_size; +} ContentMetaSystemUpdateMetaExtendedHeader; + +/// Extended header for Application titles. +/// Equivalent to NcmApplicationMetaExtendedHeader, but using ContentMetaVersion structs. +typedef struct { + u64 patch_id; + ContentMetaVersion required_system_version; + ContentMetaVersion required_application_version; +} ContentMetaApplicationMetaExtendedHeader; + +/// Extended header for Patch titles. +/// Equivalent to NcmPatchMetaExtendedHeader, but using a ContentMetaVersion struct. +typedef struct { + u64 application_id; + ContentMetaVersion required_system_version; + u32 extended_data_size; + u8 reserved[0x8]; +} ContentMetaPatchMetaExtendedHeader; + +/// Extended header for AddOnContent titles. +/// Equivalent to NcmAddOnContentMetaExtendedHeader, but using a ContentMetaVersion struct. +typedef struct { + u64 application_id; + ContentMetaVersion required_application_version; + u8 reserved[0x4]; +} ContentMetaAddOnContentMetaExtendedHeader; + +/// Extended header for Delta titles. +typedef struct { + u64 application_id; + u32 extended_data_size; + u8 reserved[0x4]; +} ContentMetaDeltaMetaExtendedHeader; + +typedef enum { + ContentMetaFirmwareVariationVersion_Invalid = 0, + ContentMetaFirmwareVariationVersion_V1 = 1, + ContentMetaFirmwareVariationVersion_V2 = 2, + ContentMetaFirmwareVariationVersion_Unknown = 3 +} ContentMetaFirmwareVariationVersion; + +/// Header for the extended data region in the SystemUpdate title, pointed to by the extended header. +/// If version is ContentMetaFirmwareVariationVersion_V1, this is followed by 'variation_count' ContentMetaFirmwareVariationInfoV1 entries. +/// Otherwise, if version is ContentMetaFirmwareVariationVersion_V2, this is followed by: +/// * 'variation_count' firmware variation IDs. +/// * 'variation_count' ContentMetaFirmwareVariationInfoV2 entries. +/// * (Optionally) A variable number of NcmContentMetaInfo entries, which is the sum of all 'meta_count' values from ContentMetaFirmwareVariationInfoV2 entries where 'refer_to_base' is set to false. +typedef struct { + u32 version; ///< ContentMetaFirmwareVariationVersion. + u32 variation_count; ///< Determines how many firmware variation entries are available after this header. +} ContentMetaSystemUpdateMetaExtendedDataHeader; + +/// Used if the firmware variation version matches ContentMetaFirmwareVariationVersion_V1. +typedef struct { + u32 firmware_variation_id; + u8 reserved[0x1C]; +} ContentMetaFirmwareVariationInfoV1; + +/// Used if the firmware variation version matches ContentMetaFirmwareVariationVersion_V2. +typedef struct { + bool refer_to_base; + u8 reserved_1[0x3]; + u32 meta_count; + u8 reserved_2[0x18]; +} ContentMetaFirmwareVariationInfoV2; + +/// Header for the extended data region in Patch titles, pointed to by the extended header. +/// This is followed by: +/// * 'history_count' ContentMetaPatchHistoryHeader entries. +/// * 'delta_history_count' ContentMetaPatchDeltaHistory entries. +/// * 'delta_count' ContentMetaPatchDeltaHeader entries. +/// * 'fragment_set_count' ContentMetaFragmentSet entries. +/// * 'history_content_count' NcmContentInfo entries. +/// * 'delta_content_count' NcmPackagedContentInfo entries. +/// * A variable number of ContentMetaFragmentIndicator entries, which is the sum of all 'fragment_count' values from ContentMetaFragmentSet entries. +typedef struct { + u32 history_count; + u32 delta_history_count; + u32 delta_count; + u32 fragment_set_count; + u32 history_content_count; + u32 delta_content_count; + u8 reserved[0x4]; +} ContentMetaPatchMetaExtendedDataHeader; + +typedef struct { + NcmContentMetaKey content_meta_key; + u8 digest[CNMT_DIGEST_SIZE]; + u16 content_info_count; + u8 reserved[0x6]; +} ContentMetaPatchHistoryHeader; + +typedef struct { + u64 source_patch_id; + u64 destination_patch_id; + ContentMetaVersion source_version; + ContentMetaVersion destination_version; + u64 download_size; + u8 reserved[0x8]; +} ContentMetaPatchDeltaHistory; + +typedef struct { + u64 source_patch_id; + u64 destination_patch_id; + ContentMetaVersion source_version; + ContentMetaVersion destination_version; + u16 fragment_set_count; + u8 reserved_1[0x6]; + u16 content_info_count; + u8 reserved_2[0x6]; +} ContentMetaPatchDeltaHeader; + +typedef enum { + ContentMetaUpdateType_ApplyAsDelta = 0, + ContentMetaUpdateType_Overwrite = 1, + ContentMetaUpdateType_Create = 2 +} ContentMetaUpdateType; + +typedef struct { + NcmContentId source_content_id; + NcmContentId destination_content_id; + u32 source_size_low; + u16 source_size_high; + u32 destination_size_low; + u16 destination_size_high; + u16 fragment_count; + NcmContentType fragment_target_content_type; + u8 update_type; ///< ContentMetaUpdateType. + u8 reserved[0x4]; +} ContentMetaFragmentSet; + +typedef struct { + u16 content_info_index; + u16 fragment_index; +} ContentMetaFragmentIndicator; + +/// Header for the extended data region in Delta titles, pointed to by the extended header. +/// This is followed by: +/// * 'fragment_set_count' ContentMetaFragmentSet entries. +/// * A variable number of ContentMetaFragmentIndicator entries, which is the sum of all 'fragment_count' values from ContentMetaFragmentSet entries. +typedef struct { + u64 source_patch_id; + u64 destination_patch_id; + ContentMetaVersion source_version; + ContentMetaVersion destination_version; + u16 fragment_set_count; + u8 reserved[0x6]; +} ContentMetaDeltaMetaExtendedDataHeader; + +typedef struct { + NcaContext *nca_ctx; ///< Pointer to the NCA context for the Meta NCA from which CNMT data is retrieved. + PartitionFileSystemContext pfs_ctx; ///< PartitionFileSystemContext for the Meta NCA FS section #0, which is where the CNMT is stored. + PartitionFileSystemEntry *pfs_entry; ///< PartitionFileSystemEntry for the CNMT in the Meta NCA FS section #0. Used to generate a NcaHierarchicalSha256Patch if needed. + NcaHierarchicalSha256Patch nca_patch; ///< NcaHierarchicalSha256Patch generated if CNMT modifications are needed. Used to seamlessly replace Meta NCA data while writing it. + ///< Bear in mind that generating a patch modifies the NCA context. + char *cnmt_filename; ///< Pointer to the CNMT filename in the Meta NCA FS section #0. + u8 *raw_data; ///< Pointer to a dynamically allocated buffer that holds the raw CNMT. + u64 raw_data_size; ///< Raw CNMT size. + ContentMetaPackagedHeader *packaged_header; ///< Pointer to the ContentMetaPackagedHeader within 'raw_data'. + u8 *extended_header; ///< Pointer to the extended header within 'raw_data', if available. May be casted to other types. Its size is stored in 'packaged_header'. + NcmPackagedContentInfo *packaged_content_info; ///< Pointer to the NcmPackagedContentInfo entries within 'raw_data'. The content count is stored in 'packaged_header'. + NcmContentMetaInfo *content_meta_info; ///< Pointer to the NcmContentMetaInfo entries within 'raw_data', if available. The content meta count is stored in 'packaged_header'. + u8 *extended_data; ///< Pointer to the extended data block within 'raw_data', if available. + u32 extended_data_size; ///< Size of the extended data block within 'raw_data', if available. Kept here for convenience - this is part of the header in 'extended_data'. + u8 *digest; ///< Pointer to the digest within 'raw_data'. +} ContentMetaContext; + +/// Initializes a ContentMetaContext using a previously initialized NcaContext (which must belong to a Meta NCA). +bool cnmtInitializeContext(ContentMetaContext *out, NcaContext *nca_ctx); + +/// Helper inline functions. + +NX_INLINE void cnmtFreeContext(ContentMetaContext *cnmt_ctx) +{ + if (!cnmt_ctx) return; + pfsFreeContext(&(cnmt_ctx->pfs_ctx)); + pfsFreeEntryPatch(&(cnmt_ctx->nca_patch)); + if (cnmt_ctx->raw_data) free(cnmt_ctx->raw_data); + memset(cnmt_ctx, 0, sizeof(ContentMetaContext)); +} + +#endif /* __CNMT_H__ */