diff --git a/source/hactool_application_list.hpp b/source/hactool_application_list.hpp
new file mode 100644
index 0000000..2bb38e0
--- /dev/null
+++ b/source/hactool_application_list.hpp
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) Atmosphère-NX
+ *
+ * This program 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.
+ *
+ * This program 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
+#include
+
+namespace ams::hactool {
+
+ template
+ class ApplicationContentTreeEntry : public util::IntrusiveRedBlackTreeBaseNode> {
+ private:
+ ncm::ApplicationId m_id;
+ u32 m_version;
+ u8 m_id_offset;
+ ncm::ContentType m_type;
+ UserData m_data;
+ public:
+ ApplicationContentTreeEntry(ncm::ApplicationId id, u32 v, u8 o, ncm::ContentType t) : m_id(id), m_version(v), m_id_offset(o), m_type(t), m_data() {
+ /* ... */
+ }
+
+ ncm::ApplicationId GetId() const {
+ return m_id;
+ }
+
+ u32 GetVersion() const {
+ return m_version;
+ }
+
+ u8 GetIdOffset() const {
+ return m_id_offset;
+ }
+
+ ncm::ContentType GetType() const {
+ return m_type;
+ }
+
+ const UserData &GetData() const { return m_data; }
+
+ UserData &GetData() { return m_data; }
+ };
+
+ template
+ struct ApplicationContentTreeEntryCompare {
+ static ALWAYS_INLINE int Compare(const ApplicationContentTreeEntry &a, const ApplicationContentTreeEntry &b) {
+ const auto a_i = a.GetId();
+ const auto a_v = a.GetVersion();
+ const auto a_o = a.GetIdOffset();
+ const auto a_t = a.GetType();
+ const auto b_i = b.GetId();
+ const auto b_v = b.GetVersion();
+ const auto b_o = b.GetIdOffset();
+ const auto b_t = b.GetType();
+ if (std::tie(a_i, a_v, a_o, a_t) < std::tie(b_i, b_v, b_o, b_t)) {
+ return -1;
+ } else if (std::tie(a_i, a_v, a_o, a_t) > std::tie(b_i, b_v, b_o, b_t)) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ };
+
+ template
+ using ApplicationContentTree = typename util::IntrusiveRedBlackTreeBaseTraits>::TreeType>;
+
+ template
+ struct ApplicationContentsHolder {
+ NON_COPYABLE(ApplicationContentsHolder);
+ NON_MOVEABLE(ApplicationContentsHolder);
+ private:
+ ApplicationContentTree m_tree;
+ public:
+ ApplicationContentsHolder() : m_tree() { /* ... */ }
+
+ ~ApplicationContentsHolder() {
+ while (!m_tree.empty()) {
+ auto it = m_tree.begin();
+ while (it != m_tree.end()) {
+ auto *entry = std::addressof(*it);
+ it = m_tree.erase(it);
+ delete entry;
+ }
+ }
+ }
+
+ ApplicationContentTreeEntry *Insert(ncm::ApplicationId id, u32 v, u8 o, ncm::ContentType t) {
+ auto *entry = new ApplicationContentTreeEntry(id, v, o, t);
+ m_tree.insert(*entry);
+ return entry;
+ }
+
+ auto begin() const { return m_tree.begin(); }
+ auto end() const { return m_tree.end(); }
+
+ auto Find(ncm::ApplicationId id, u32 v, u8 o, ncm::ContentType t) {
+ ApplicationContentTreeEntry dummy(id, v, o, t);
+ return m_tree.find(dummy);
+ }
+ };
+
+}
\ No newline at end of file
diff --git a/source/hactool_fs_utils.cpp b/source/hactool_fs_utils.cpp
index f212d4c..a66b706 100644
--- a/source/hactool_fs_utils.cpp
+++ b/source/hactool_fs_utils.cpp
@@ -63,6 +63,14 @@ namespace ams::hactool {
}
+ bool PathView::HasPrefix(util::string_view prefix) const {
+ return m_path.compare(0, prefix.length(), prefix) == 0;
+ }
+
+ bool PathView::HasSuffix(util::string_view suffix) const {
+ return m_path.compare(m_path.length() - suffix.length(), suffix.length(), suffix) == 0;
+ }
+
Result OpenFileStorage(std::shared_ptr *out, std::shared_ptr &fs, const char *path) {
/* Open the file storage. */
std::shared_ptr file_storage = fssystem::AllocateShared();
@@ -81,6 +89,30 @@ namespace ams::hactool {
R_SUCCEED();
}
+ Result OpenSubDirectoryFileSystem(std::shared_ptr *out, std::shared_ptr &fs, const char *path) {
+ /* Get the fs path. */
+ ams::fs::Path fs_path;
+ R_UNLESS(path != nullptr, fs::ResultNullptrArgument());
+ R_TRY(fs_path.SetShallowBuffer(path));
+
+ /* Verify that we can open the directory on the base filesystem. */
+ {
+ std::unique_ptr sub_dir;
+ R_TRY(fs->OpenDirectory(std::addressof(sub_dir), fs_path, fs::OpenDirectoryMode_Directory));
+ }
+
+ /* Allocate the subdirectory filesystem. */
+ auto subdir_fs = fssystem::AllocateShared(fs);
+ R_UNLESS(subdir_fs != nullptr, fs::ResultAllocationMemoryFailedAllocateShared());
+
+ /* Initialize the subdirectory filesystem. */
+ R_TRY(subdir_fs->Initialize(fs_path));
+
+ /* Set the output. */
+ *out = std::move(subdir_fs);
+ R_SUCCEED();
+ }
+
Result PrintDirectory(std::shared_ptr &fs, const char *prefix, const char *path) {
/* Get the fs path. */
ams::fs::Path fs_path;
diff --git a/source/hactool_fs_utils.hpp b/source/hactool_fs_utils.hpp
index 2eeeb54..0b5af76 100644
--- a/source/hactool_fs_utils.hpp
+++ b/source/hactool_fs_utils.hpp
@@ -18,8 +18,19 @@
namespace ams::hactool {
+ class PathView {
+ private:
+ util::string_view m_path;
+ public:
+ PathView(util::string_view p) : m_path(p) { /* ...*/ }
+ bool HasPrefix(util::string_view prefix) const;
+ bool HasSuffix(util::string_view suffix) const;
+ };
+
Result OpenFileStorage(std::shared_ptr *out, std::shared_ptr &fs, const char *path);
+ Result OpenSubDirectoryFileSystem(std::shared_ptr *out, std::shared_ptr &fs, const char *path);
+
Result PrintDirectory(std::shared_ptr &fs, const char *prefix, const char *path);
Result ExtractDirectory(std::shared_ptr &dst_fs, std::shared_ptr &src_fs, const char *prefix, const char *dst_path, const char *src_path);
diff --git a/source/hactool_options.cpp b/source/hactool_options.cpp
index 1eb246b..c830207 100644
--- a/source/hactool_options.cpp
+++ b/source/hactool_options.cpp
@@ -110,6 +110,8 @@ namespace ams::hactool {
options.file_type = FileType::Nca;
} else if (std::strcmp(arg, "xci") == 0) {
options.file_type = FileType::Xci;
+ } else if (std::strcmp(arg, "appfs") == 0) {
+ options.file_type = FileType::AppFs;
} else {
return false;
}
diff --git a/source/hactool_options.hpp b/source/hactool_options.hpp
index 590c199..f257bc4 100644
--- a/source/hactool_options.hpp
+++ b/source/hactool_options.hpp
@@ -30,6 +30,7 @@ namespace ams::hactool {
Kip,
Ini,
Npdm,
+ AppFs,
};
struct Options {
@@ -41,6 +42,9 @@ namespace ams::hactool {
bool dev = false;
bool enable_hash = false;
bool disable_key_warns = false;
+ int preferred_app_index = -1;
+ int preferred_program_index = -1;
+ int preferred_version = -1;
const char *key_file_path = nullptr;
const char *titlekey_path = nullptr;
const char *consolekey_path = nullptr;
diff --git a/source/hactool_processor.app_fs.cpp b/source/hactool_processor.app_fs.cpp
new file mode 100644
index 0000000..f2807a0
--- /dev/null
+++ b/source/hactool_processor.app_fs.cpp
@@ -0,0 +1,234 @@
+/*
+ * Copyright (c) Atmosphère-NX
+ *
+ * This program 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.
+ *
+ * This program 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
+#include "hactool_processor.hpp"
+#include "hactool_fs_utils.hpp"
+
+namespace ams::hactool {
+
+ namespace {
+
+ constexpr const s32 MetaFileSystemPartitionIndex = 0;
+
+ constexpr const char MetaNcaFileNameExtension[] = ".cnmt.nca";
+ constexpr const char NcaFileNameExtension[] = ".nca";
+
+ constexpr const char ContentMetaFileNameExtension[] = ".cnmt";
+
+ Result ReadContentMetaFile(std::unique_ptr *out, size_t *out_size, std::shared_ptr &fs) {
+ bool found = false;
+ R_RETURN(fssystem::IterateDirectoryRecursively(fs.get(),
+ [&] (const fs::Path &, const fs::DirectoryEntry &) -> Result { R_SUCCEED(); },
+ [&] (const fs::Path &, const fs::DirectoryEntry &) -> Result { R_SUCCEED(); },
+ [&] (const fs::Path &path, const fs::DirectoryEntry &entry) -> Result {
+ /* If we already found the content meta, finish. */
+ R_SUCCEED_IF(found);
+
+ /* If the path isn't a meta nca, finish. */
+ R_SUCCEED_IF(!PathView(entry.name).HasSuffix(ContentMetaFileNameExtension));
+
+ /* Open the file storage. */
+ std::shared_ptr storage;
+ R_TRY(OpenFileStorage(std::addressof(storage), fs, path.GetString()));
+
+ /* Get the meta file size. */
+ s64 size;
+ R_TRY(storage->GetSize(std::addressof(size)));
+
+ /* Allocate buffer. */
+ auto data = std::make_unique(static_cast(size));
+ R_UNLESS(data != nullptr, fs::ResultAllocationMemoryFailedMakeUnique());
+
+ /* Read the meta into the buffer. */
+ R_TRY(storage->Read(0, data.get(), size));
+
+ /* Return the output buffer. */
+ *out = std::move(data);
+ *out_size = static_cast(size);
+ found = true;
+
+ R_SUCCEED();
+ }
+ ));
+
+ R_THROW(ncm::ResultContentMetaNotFound());
+ }
+
+ }
+
+ Result Processor::ProcessAsApplicationFileSystem(std::shared_ptr fs, ProcessAsApplicationFileSystemCtx *ctx) {
+ /* Ensure we have a context. */
+ ProcessAsApplicationFileSystemCtx local_ctx{};
+ if (ctx == nullptr) {
+ ctx = std::addressof(local_ctx);
+ }
+
+ /* Set the fs. */
+ ctx->fs = std::move(fs);
+
+ /* Iterate all files in the filesystem. */
+ {
+ /* Iterate, printing the contents of the directory. */
+ const auto iter_result = fssystem::IterateDirectoryRecursively(ctx->fs.get(),
+ [&] (const fs::Path &, const fs::DirectoryEntry &) -> Result { R_SUCCEED(); },
+ [&] (const fs::Path &, const fs::DirectoryEntry &) -> Result { R_SUCCEED(); },
+ [&] (const fs::Path &path, const fs::DirectoryEntry &entry) -> Result {
+ /* If the path isn't a meta nca, finish. */
+ R_SUCCEED_IF(!PathView(entry.name).HasSuffix(MetaNcaFileNameExtension));
+
+ /* Try opening the meta. */
+ std::shared_ptr meta_nca_storage;
+ if (const auto res = OpenFileStorage(std::addressof(meta_nca_storage), ctx->fs, path.GetString()); R_FAILED(res)) {
+ fprintf(stderr, "[Warning]: Failed to open meta nca (%s): 2%03d-%04d\n", path.GetString(), res.GetModule(), res.GetDescription());
+ R_SUCCEED();
+ }
+
+ ProcessAsNcaContext meta_nca_ctx = {};
+ if (const auto res = this->ProcessAsNca(std::move(meta_nca_storage), std::addressof(meta_nca_ctx)); R_FAILED(res)) {
+ fprintf(stderr, "[Warning]: Failed to process meta nca (%s): 2%03d-%04d\n", path.GetString(), res.GetModule(), res.GetDescription());
+ R_SUCCEED();
+ }
+
+ /* We only care about meta ncas. */
+ if (meta_nca_ctx.reader->GetContentType() != fssystem::NcaHeader::ContentType::Meta) {
+ fprintf(stderr, "[Warning]: Expected %s to be Meta, was %s\n", path.GetString(), fs::impl::IdString().ToString(meta_nca_ctx.reader->GetContentType()));
+ R_SUCCEED();
+ }
+
+ /* Clarification: we only care about meta ncas which are mountable. */
+ if (!meta_nca_ctx.is_mounted[MetaFileSystemPartitionIndex]) {
+ fprintf(stderr, "[Warning]: Expected to mount meta nca partition for %s, but didn't.\n", path.GetString());
+ R_SUCCEED();
+ }
+
+ /* Read the content meta file. */
+ std::unique_ptr meta_data;
+ size_t meta_size;
+ if (const auto res = ReadContentMetaFile(std::addressof(meta_data), std::addressof(meta_size), meta_nca_ctx.file_systems[MetaFileSystemPartitionIndex]); R_FAILED(res)) {
+ fprintf(stderr, "[Warning]: Failed to read cnmt from %s: 2%03d-%04d\n", path.GetString(), res.GetModule(), res.GetDescription());
+ R_SUCCEED();
+ }
+
+ /* Parse the cnmt. */
+ const auto meta_reader = ncm::PackagedContentMetaReader(meta_data.get(), meta_size);
+ const auto * const meta_header = meta_reader.GetHeader();
+
+ /* We only care about applications/patches. */
+ R_SUCCEED_IF(meta_header->type != ncm::ContentMetaType::Application && meta_header->type != ncm::ContentMetaType::Patch);
+
+ /* Get the key. */
+ const auto app_id = meta_reader.GetApplicationId();
+ AMS_ABORT_UNLESS(app_id.has_value());
+
+ /* Get the version. */
+ const auto version = meta_header->version;
+
+ /* Add all the content metas. */
+ for (size_t i = 0; i < meta_reader.GetContentCount(); ++i) {
+ const auto &info = *meta_reader.GetContentInfo(i);
+
+ /* Check that the type isn't a delta. */
+ if (info.GetType() == ncm::ContentType::DeltaFragment) {
+ continue;
+ }
+
+ /* Check that we don't already have an info for the content. */
+ if (auto existing = ctx->apps.Find(*app_id, version, info.GetIdOffset(), info.GetType()); existing != ctx->apps.end()) {
+ fprintf(stderr, "[Warning]: Ignoring duplicate entry { %016" PRIX64 ", %" PRIu32 ", %d, %d }\n", app_id->value, version, static_cast(info.GetIdOffset()), static_cast(info.GetType()));
+ continue;
+ }
+
+ /* Try to open the storage for the specified file. */
+ std::shared_ptr storage;
+ {
+ const auto cid_str = ncm::GetContentIdString(info.GetId());
+ char file_name[ncm::ContentIdStringLength + 0x10];
+ util::TSNPrintf(file_name, sizeof(file_name), "%s%s", cid_str.data, NcaFileNameExtension);
+
+ const auto res = [&] () -> Result {
+ ams::fs::Path fs_path;
+ R_TRY(fs_path.Initialize(path));
+ R_TRY(fs_path.RemoveChild());
+ R_TRY(fs_path.AppendChild(file_name));
+
+ R_RETURN(OpenFileStorage(std::addressof(storage), ctx->fs, fs_path.GetString()));
+ }();
+ if (R_FAILED(res)) {
+ fprintf(stderr, "[Warning]: Failed to open NCA (type %d) specified by %s: 2%03d-%04d\n", static_cast(info.GetType()), path.GetString(), res.GetModule(), res.GetDescription());
+ R_SUCCEED();
+ }
+ }
+
+ /* Add the new version for the content. */
+ auto *entry = ctx->apps.Insert(*app_id, version, info.GetIdOffset(), info.GetType());
+ entry->GetData().storage = std::move(storage);
+ }
+
+ R_SUCCEED();
+ }
+ );
+ if (R_FAILED(iter_result)) {
+ fprintf(stderr, "[Warning]: Failed to parse application filesystem: 2%03d-%04d\n", iter_result.GetModule(), iter_result.GetDescription());
+ }
+ }
+
+ /* TODO: Recursive processing? */
+
+ /* Print. */
+ if (ctx == std::addressof(local_ctx)) {
+ this->PrintAsApplicationFileSystem(*ctx);
+ }
+
+ /* Save. */
+ if (ctx == std::addressof(local_ctx)) {
+ this->SaveAsApplicationFileSystem(*ctx);
+ }
+
+ R_SUCCEED();
+ }
+
+ void Processor::PrintAsApplicationFileSystem(ProcessAsApplicationFileSystemCtx &ctx) {
+ auto _ = this->PrintHeader("Application File System");
+
+ {
+ s32 app_idx = -1;
+ ncm::ApplicationId cur_app_id{};
+ const char *field_name = "Programs";
+ for (const auto &entry : ctx.apps) {
+ if (entry.GetType() != ncm::ContentType::Program) {
+ continue;
+ }
+
+ if (app_idx == -1 || cur_app_id != entry.GetId()) {
+ ++app_idx;
+ cur_app_id = entry.GetId();
+ }
+
+ this->PrintFormat(field_name, "{ Idx=%d, ProgramId=%016" PRIX64 ", Version=0x%08" PRIX32 ", IdOffset=%02" PRIX32 " }", app_idx, entry.GetId().value, entry.GetVersion(), entry.GetIdOffset());
+ field_name = "";
+ }
+ }
+
+ /* TODO */
+ AMS_UNUSED(ctx);
+ }
+
+ void Processor::SaveAsApplicationFileSystem(ProcessAsApplicationFileSystemCtx &ctx) {
+ /* TODO */
+ AMS_UNUSED(ctx);
+ }
+
+}
\ No newline at end of file
diff --git a/source/hactool_processor.hpp b/source/hactool_processor.hpp
index ae10213..79ea988 100644
--- a/source/hactool_processor.hpp
+++ b/source/hactool_processor.hpp
@@ -16,6 +16,7 @@
#pragma once
#include
#include "hactool_options.hpp"
+#include "hactool_application_list.hpp"
namespace ams::hactool {
@@ -74,6 +75,16 @@ namespace ams::hactool {
ProcessAsNpdmContext npdm_ctx;
};
+ struct ProcessAsApplicationFileSystemCtx {
+ std::shared_ptr fs;
+
+ struct ApplicationEntryData {
+ std::shared_ptr storage;
+ };
+
+ ApplicationContentsHolder apps;
+ };
+
struct ProcessAsXciContext {
std::shared_ptr storage;
@@ -102,6 +113,8 @@ namespace ams::hactool {
PartitionData logo_partition;
PartitionData normal_partition;
PartitionData secure_partition;
+
+ ProcessAsApplicationFileSystemCtx app_ctx;
};
private:
Options m_options;
@@ -193,16 +206,19 @@ namespace ams::hactool {
Result ProcessAsNca(std::shared_ptr storage, ProcessAsNcaContext *ctx = nullptr);
Result ProcessAsNpdm(std::shared_ptr storage, ProcessAsNpdmContext *ctx = nullptr);
Result ProcessAsXci(std::shared_ptr storage, ProcessAsXciContext *ctx = nullptr);
+ Result ProcessAsApplicationFileSystem(std::shared_ptr fs, ProcessAsApplicationFileSystemCtx *ctx = nullptr);
/* Printing. */
void PrintAsNca(ProcessAsNcaContext &ctx);
void PrintAsNpdm(ProcessAsNpdmContext &ctx);
void PrintAsXci(ProcessAsXciContext &ctx);
+ void PrintAsApplicationFileSystem(ProcessAsApplicationFileSystemCtx &ctx);
/* Saving. */
void SaveAsNca(ProcessAsNcaContext &ctx);
void SaveAsNpdm(ProcessAsNpdmContext &ctx);
void SaveAsXci(ProcessAsXciContext &ctx);
+ void SaveAsApplicationFileSystem(ProcessAsApplicationFileSystemCtx &ctx);
};
inline void Processor::PrintLineImpl(const char *fmt, ...) const {
diff --git a/source/hactool_processor.main.cpp b/source/hactool_processor.main.cpp
index a6afb7d..16fcb46 100644
--- a/source/hactool_processor.main.cpp
+++ b/source/hactool_processor.main.cpp
@@ -33,24 +33,34 @@ namespace ams::hactool {
/* Setup our internal keys. */
this->PresetInternalKeys();
- /* Open the file storage. */
- std::shared_ptr input = nullptr;
- if (m_options.in_file_path != nullptr) {
- R_TRY(OpenFileStorage(std::addressof(input), m_local_fs, m_options.in_file_path));
- }
+ if (m_options.file_type == FileType::AppFs) {
+ /* Open the filesystem. */
+ std::shared_ptr input = nullptr;
+ if (m_options.in_file_path != nullptr) {
+ R_TRY(OpenSubDirectoryFileSystem(std::addressof(input), m_local_fs, m_options.in_file_path));
+ }
- /* Process for the specific file type. */
- switch (m_options.file_type) {
- case FileType::Nca:
- R_TRY(this->ProcessAsNca(std::move(input)));
- break;
- case FileType::Npdm:
- R_TRY(this->ProcessAsNpdm(std::move(input)));
- break;
- case FileType::Xci:
- R_TRY(this->ProcessAsXci(std::move(input)));
- break;
- AMS_UNREACHABLE_DEFAULT_CASE();
+ R_TRY(this->ProcessAsApplicationFileSystem(std::move(input)));
+ } else {
+ /* Open the file storage. */
+ std::shared_ptr input = nullptr;
+ if (m_options.in_file_path != nullptr) {
+ R_TRY(OpenFileStorage(std::addressof(input), m_local_fs, m_options.in_file_path));
+ }
+
+ /* Process for the specific file type. */
+ switch (m_options.file_type) {
+ case FileType::Nca:
+ R_TRY(this->ProcessAsNca(std::move(input)));
+ break;
+ case FileType::Npdm:
+ R_TRY(this->ProcessAsNpdm(std::move(input)));
+ break;
+ case FileType::Xci:
+ R_TRY(this->ProcessAsXci(std::move(input)));
+ break;
+ AMS_UNREACHABLE_DEFAULT_CASE();
+ }
}
R_SUCCEED();
diff --git a/source/hactool_processor.xci.cpp b/source/hactool_processor.xci.cpp
index e6dfa00..ec8e432 100644
--- a/source/hactool_processor.xci.cpp
+++ b/source/hactool_processor.xci.cpp
@@ -207,7 +207,12 @@ namespace ams::hactool {
}
}
- /* TODO: Recursive processing? */
+ /* If we have applications, process them. */
+ if (ctx->secure_partition.fs != nullptr) {
+ if (const auto process_app_res = this->ProcessAsApplicationFileSystem(ctx->secure_partition.fs, std::addressof(ctx->app_ctx)); R_FAILED(process_app_res)) {
+ fprintf(stderr, "[Warning]: Failed to process game card's applications: 2%03d-%04d\n", process_app_res.GetModule(), process_app_res.GetDescription());
+ }
+ }
/* Print. */
if (ctx == std::addressof(local_ctx)) {
@@ -353,6 +358,25 @@ namespace ams::hactool {
PrintGamecardPartition("Update Partition", "update:", ctx.update_partition);
}
+ if (ctx.secure_partition.fs != nullptr) {
+ s32 app_idx = -1;
+ ncm::ApplicationId cur_app_id{};
+ const char *field_name = "Programs";
+ for (const auto &entry : ctx.app_ctx.apps) {
+ if (entry.GetType() != ncm::ContentType::Program) {
+ continue;
+ }
+
+ if (app_idx == -1 || cur_app_id != entry.GetId()) {
+ ++app_idx;
+ cur_app_id = entry.GetId();
+ }
+
+ this->PrintFormat(field_name, "{ Idx=%d, ProgramId=%016" PRIX64 ", Version=0x%08" PRIX32 ", IdOffset=%02" PRIX32 " }", app_idx, entry.GetId().value, entry.GetVersion(), entry.GetIdOffset());
+ field_name = "";
+ }
+ }
+
AMS_UNUSED(ctx);
}