From 1c41ca7fc67694c4ff56e819a4f522c6da542988 Mon Sep 17 00:00:00 2001 From: Michael Scire Date: Thu, 17 Mar 2022 16:51:29 -0700 Subject: [PATCH] hac2l: add appfs target for fs-of-apps (subdir on pc, xci secure part, pfs0) --- source/hactool_application_list.hpp | 114 ++++++++++++++ source/hactool_fs_utils.cpp | 32 ++++ source/hactool_fs_utils.hpp | 11 ++ source/hactool_options.cpp | 2 + source/hactool_options.hpp | 4 + source/hactool_processor.app_fs.cpp | 234 ++++++++++++++++++++++++++++ source/hactool_processor.hpp | 16 ++ source/hactool_processor.main.cpp | 44 ++++-- source/hactool_processor.xci.cpp | 26 +++- 9 files changed, 465 insertions(+), 18 deletions(-) create mode 100644 source/hactool_application_list.hpp create mode 100644 source/hactool_processor.app_fs.cpp 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); }