diff options
Diffstat (limited to 'src/core/loader')
| -rw-r--r-- | src/core/loader/deconstructed_rom_directory.cpp | 53 | ||||
| -rw-r--r-- | src/core/loader/deconstructed_rom_directory.h | 6 | ||||
| -rw-r--r-- | src/core/loader/loader.cpp | 35 | ||||
| -rw-r--r-- | src/core/loader/loader.h | 3 | ||||
| -rw-r--r-- | src/core/loader/nup.cpp | 280 | ||||
| -rw-r--r-- | src/core/loader/nup.h | 65 |
6 files changed, 411 insertions, 31 deletions
diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp index 94dd3395ba..463a8c8930 100644 --- a/src/core/loader/deconstructed_rom_directory.cpp +++ b/src/core/loader/deconstructed_rom_directory.cpp @@ -8,6 +8,8 @@ #include <cctype> #include <chrono> #include <cstring> +#include <map> +#include <memory> #include <mutex> #include <string_view> #include "common/logging.h" @@ -92,31 +94,24 @@ FileSys::VirtualFile FindLooseRomFS(const FileSys::VirtualDir& exefs_dir) { } if (const auto parent = exefs_dir->GetParentDirectory()) { if (const auto romfs_dir = parent->GetSubdirectory("romfs")) { - // Keep no ownership here: launch preparation hands its image to the real loader. + // Building it walks every file and keeps an object for each (title 1: 206k files, ~150 + // MiB), so loaders of the same directory share one build while any of them holds it. static std::mutex cache_mutex; - static std::string cached_path; - static std::weak_ptr<FileSys::VfsFile> cached_romfs; - const auto path = romfs_dir->GetFullPath(); - { - std::scoped_lock lock{cache_mutex}; - if (cached_path == path) { - if (auto cached = cached_romfs.lock()) { - return cached; - } - } + static std::map<std::string, std::weak_ptr<FileSys::VfsFile>> cache; + std::scoped_lock lk{cache_mutex}; + auto& cached = cache[romfs_dir->GetFullPath()]; + if (auto built = cached.lock()) { + return built; } + LOG_INFO(Loader, "Using RomFS directory next to the ExeFS"); const auto started = std::chrono::steady_clock::now(); - LOG_INFO(Loader, "Building loose RomFS directory image"); - auto image = FileSys::CreateRomFS(romfs_dir); + auto built = FileSys::CreateRomFS(romfs_dir); LOG_INFO(Loader, "Loose RomFS image ready: elapsed_ms={}", std::chrono::duration_cast<std::chrono::milliseconds>( - std::chrono::steady_clock::now() - started).count()); - { - std::scoped_lock lock{cache_mutex}; - cached_path = path; - cached_romfs = image; - } - return image; + std::chrono::steady_clock::now() - started) + .count()); + cached = built; + return built; } } return nullptr; @@ -258,6 +253,7 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys name = control->GetApplicationName(); } + romfs_exefs_dir = file_dir; if (const auto title_dir = file_dir->GetParentDirectory()) { romfs_version = RomFSProductVersion(title_dir->GetSubdirectory("romfs")); } @@ -269,13 +265,17 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory( , dir(std::move(directory)) , override_update(override_update_) { + romfs_exefs_dir = dir; } -void AppLoader_DeconstructedRomDirectory::EnsureRomFS() { - if (!romfs_initialized) { - romfs = FindLooseRomFS(dir ? dir : file->GetContainingDirectory()); - romfs_initialized = true; +// Loaders made only for metadata (the game list, title lookups) never need the RomFS, and several +// of them were alive with a whole copy each. +const FileSys::VirtualFile& AppLoader_DeconstructedRomDirectory::LooseRomFS() { + if (!romfs_resolved) { + romfs_resolved = true; + romfs = FindLooseRomFS(romfs_exefs_dir); } + return romfs; } FileType AppLoader_DeconstructedRomDirectory::IdentifyType(const FileSys::VirtualFile& dir_file) { @@ -336,7 +336,7 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect // Loose-ExeFS loads bypass the NCA/NSP loaders, which are the ones that normally apply // LayeredFS to the RomFS; apply it here so mods in user/load/<titleid>/ work too. - if (romfs != nullptr) { + if (LooseRomFS() != nullptr) { const FileSys::PatchManager patch_manager(metadata.GetTitleID(), system.GetFileSystemController(), system.GetContentProvider()); @@ -446,8 +446,7 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect } ResultStatus AppLoader_DeconstructedRomDirectory::ReadRomFS(FileSys::VirtualFile& out_dir) { - EnsureRomFS(); - if (romfs == nullptr) { + if (LooseRomFS() == nullptr) { return ResultStatus::ErrorNoRomFS; } diff --git a/src/core/loader/deconstructed_rom_directory.h b/src/core/loader/deconstructed_rom_directory.h index 477638defe..db0120f4ad 100644 --- a/src/core/loader/deconstructed_rom_directory.h +++ b/src/core/loader/deconstructed_rom_directory.h @@ -58,11 +58,13 @@ public: ResultStatus ReadNSOModules(Modules& out_modules) override; private: - void EnsureRomFS(); + /// The loose RomFS next to the ExeFS, found (and built) on first use. + const FileSys::VirtualFile& LooseRomFS(); FileSys::ProgramMetadata metadata; FileSys::VirtualFile romfs; - bool romfs_initialized{}; + FileSys::VirtualDir romfs_exefs_dir; ///< Where LooseRomFS looks + bool romfs_resolved{}; FileSys::VirtualDir dir; std::vector<u8> icon_data; diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp index 33a93573cf..839bb4d80b 100644 --- a/src/core/loader/loader.cpp +++ b/src/core/loader/loader.cpp @@ -29,6 +29,7 @@ #include "core/loader/nro.h" #include "core/loader/nso.h" #include "core/loader/nsp.h" +#include "core/loader/nup.h" #include "core/loader/xci.h" namespace Loader { @@ -130,6 +131,8 @@ FileType IdentifyFile(FileSys::VirtualFile file) { return *nsp_type; } else if (const auto xci_type = IdentifyFileLoader<AppLoader_XCI>(file)) { return *xci_type; + } else if (const auto nup_type = IdentifyFileLoader<AppLoader_NUP>(file)) { + return *nup_type; } else if (const auto nro_type = IdentifyFileLoader<AppLoader_NRO>(file)) { return *nro_type; } else if (const auto nca_type = IdentifyFileLoader<AppLoader_NCA>(file)) { @@ -150,7 +153,7 @@ FileType IdentifyFile(FileSys::VirtualFile file) { bool IsContainerType(FileType type) { return type == FileType::NSP || type == FileType::NSZ || type == FileType::XCI || - type == FileType::XCZ; + type == FileType::XCZ || type == FileType::NUP; } bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 program_id, @@ -163,6 +166,24 @@ bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 progr type = IdentifyFile(file); } + if (type == FileType::NUP) { + FileSys::NupContainer nup{file}; + if (nup.GetStatus() != ResultStatus::Success) { + return false; + } + // A base package boots normally; a standalone update (no base) carries + // a complete patched tree and boots on its own (SPEC section 4.1.2). + if (const auto base = nup.FindBasePackage(); + base.has_value() && nup.GetExeFSDir(*base) != nullptr) { + return true; + } + if (const auto update = nup.FindBestUpdatePackage(); + update.has_value() && nup.GetExeFSDir(*update) != nullptr) { + return true; + } + return false; + } + if (!IsContainerType(type)) { return false; } @@ -190,6 +211,8 @@ FileType GuessFromFilename(const std::string& name) { return FileType::XCZ; else if (extension == "nsp") return FileType::NSP; + else if (extension == "nup") + return FileType::NUP; else if (extension == "nsz") return FileType::NSZ; else if (extension == "kip") @@ -215,6 +238,8 @@ std::string GetFileTypeString(FileType type) { return "NSP"; case FileType::NSZ: return "NSZ"; + case FileType::NUP: + return "NUP"; case FileType::KIP: return "KIP"; case FileType::DeconstructedRomDirectory: @@ -227,7 +252,7 @@ std::string GetFileTypeString(FileType type) { return "unknown"; } -constexpr std::array<const char*, 68> RESULT_MESSAGES{ +constexpr std::array<const char*, 70> RESULT_MESSAGES{ "The operation completed successfully.", "The loader requested to load is already loaded.", "The operation is not implemented.", @@ -296,6 +321,8 @@ constexpr std::array<const char*, 68> RESULT_MESSAGES{ "The INI file contains more than the maximum allowable number of KIP files.", "Integrity verification could not be performed for this file.", "Integrity verification failed.", + "The NUP container is malformed or its TOC hash does not match.", + "The NUP container does not contain a loadable base package.", }; std::string GetResultStatusString(ResultStatus status) { @@ -380,6 +407,10 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileSys::V case FileType::KIP: return std::make_unique<AppLoader_KIP>(std::move(file)); + // NUP (NX Package) container format. + case FileType::NUP: + return std::make_unique<AppLoader_NUP>(std::move(file)); + // NX deconstructed ROM directory. case FileType::DeconstructedRomDirectory: return std::make_unique<AppLoader_DeconstructedRomDirectory>(std::move(file)); diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index 431aeace73..23be3597f5 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -47,6 +47,7 @@ enum class FileType { XCZ, NAX, KIP, + NUP, DeconstructedRomDirectory, }; @@ -157,6 +158,8 @@ enum class ResultStatus : u16 { ErrorINITooManyKIPs, ErrorIntegrityVerificationNotImplemented, ErrorIntegrityVerificationFailed, + ErrorNUPBadFormat, + ErrorNUPNoBasePackage, }; std::string GetResultStatusString(ResultStatus status); diff --git a/src/core/loader/nup.cpp b/src/core/loader/nup.cpp new file mode 100644 index 0000000000..7ddfb8947e --- /dev/null +++ b/src/core/loader/nup.cpp @@ -0,0 +1,280 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "core/loader/nup.h" + +#include <algorithm> +#include <cstring> + +#include "common/logging.h" +#include "common/string_util.h" +#include "core/core.h" +#include "core/file_sys/nup_container.h" +#include "core/file_sys/control_metadata.h" +#include "core/file_sys/patch_manager.h" +#include "core/file_sys/romfs.h" +#include "core/file_sys/romfs_factory.h" +#include "core/file_sys/vfs/vfs_vector.h" +#include "core/hle/kernel/k_process.h" +#include "core/hle/service/filesystem/filesystem.h" +#include "core/loader/deconstructed_rom_directory.h" + +namespace Loader { + +namespace { + +using FileSys::NupContainer; + +// NACP stores 16 uncompressed language entries; map BCP-47 tags onto them. +struct LanguageSlot { + std::string_view tag; + std::size_t index; +}; + +constexpr std::array<LanguageSlot, 18> LanguageSlots{{ + {"en-us", 0}, {"en-gb", 1}, {"ja", 2}, {"fr", 3}, {"de", 4}, + {"es-419", 5}, {"es", 6}, {"it", 7}, {"nl", 8}, {"fr-ca", 9}, + {"pt", 10}, {"ru", 11}, {"ko", 12}, {"zh-hant", 13}, {"zh-hans", 14}, + {"pt-br", 15}, {"pl", 16}, {"th", 17}, +}}; + +void CopyNameToEntry(FileSys::LanguageEntry& entry, std::string_view name) { + const std::size_t length = std::min(name.size(), entry.application_name.size() - 1); + std::memcpy(entry.application_name.data(), name.data(), length); + entry.application_name[length] = '\0'; +} + +std::unique_ptr<FileSys::NACP> BuildNacp(const NupContainer::PackageInfo& base, + const std::string& version) { + if (base.title_name.empty() && base.localized_titles.empty() && version.empty()) { + return nullptr; + } + auto nacp = std::make_unique<FileSys::NACP>(); + nacp->raw.supported_languages = static_cast<FileSys::SupportedLanguage>(0x3FFFF); + + for (const auto& [lang, name] : base.localized_titles) { + for (const auto& slot : LanguageSlots) { + // The uncompressed RawNACP only stores 16 entries; Polish/Thai exist + // only in the compressed format, so they fall back to the default. + if (slot.index < 16 && slot.tag == lang) { + CopyNameToEntry(nacp->raw.language_entries.language_entries[slot.index], name); + } + } + } + if (!base.title_name.empty()) { + CopyNameToEntry(nacp->raw.language_entries.language_entries[0], base.title_name); + } + if (!version.empty()) { + const std::size_t length = std::min(version.size(), nacp->raw.version_string.size() - 1); + std::memcpy(nacp->raw.version_string.data(), version.data(), length); + nacp->raw.version_string[length] = '\0'; + } + nacp->raw.save_data_owner_id = base.title_id; + + nacp->language_entries.resize(16); + std::memcpy(nacp->language_entries.data(), nacp->raw.language_entries.language_entries.data(), + sizeof(nacp->raw.language_entries.language_entries)); + return nacp; +} + +std::string DecodeVersion(u32 version) { + if (version == 0) { + return {}; + } + return fmt::format("{}.{}.{}", version >> 16, (version >> 8) & 0xFF, version & 0xFF); +} + +} // namespace + +AppLoader_NUP::AppLoader_NUP(FileSys::VirtualFile file_) + : AppLoader(std::move(file_)), nup(std::make_unique<NupContainer>(file)) { + if (nup->GetStatus() != ResultStatus::Success) { + return; + } + + const auto base_index = nup->FindBasePackage(); + const auto update_index = nup->FindBestUpdatePackage(); + + // The program comes from the base package; a standalone update container + // (SPEC §4.1.2) carries a complete patched tree and boots on its own. + std::optional<std::size_t> program_index; + std::optional<std::size_t> exefs_index; + if (base_index && nup->GetExeFSDir(*base_index) != nullptr) { + program_index = base_index; + exefs_index = update_index && nup->GetExeFSDir(*update_index) != nullptr + ? update_index + : base_index; + } else if (update_index && nup->GetExeFSDir(*update_index) != nullptr) { + LOG_INFO(Loader, + "NUP container is a standalone update; booting the update program directly"); + program_index = update_index; + exefs_index = update_index; + } + if (!program_index || !exefs_index) { + LOG_ERROR(Loader, "NUP container has no package with a usable ExeFS"); + return; + } + + const auto& program = nup->GetPackages()[*program_index]; + base_title_id = program.title_id; + has_program = true; + + const auto exefs = nup->GetExeFSDir(*exefs_index); + if (exefs == nullptr || exefs->GetFile("main") == nullptr || + exefs->GetFile("main.npdm") == nullptr) { + LOG_ERROR(Loader, "NUP package has no usable ExeFS"); + return; + } + secondary_loader = std::make_unique<AppLoader_DeconstructedRomDirectory>(exefs, true); + + if (update_index) { + const auto& update = nup->GetPackages()[*update_index]; + display_version = update.display_version.empty() ? DecodeVersion(update.version) + : update.display_version; + } else { + display_version = program.display_version.empty() ? DecodeVersion(program.version) + : program.display_version; + } + nacp_file = BuildNacp(program, display_version); + if (nacp_file != nullptr && !program.icon.empty()) { + icon_file = std::make_shared<FileSys::VectorVfsFile>(program.icon, "icon.dat"); + } else if (nacp_file != nullptr) { + for (const auto& [lang, icon] : program.localized_icons) { + if (lang == "en-us") { + icon_file = std::make_shared<FileSys::VectorVfsFile>(icon, "icon_" + lang + ".dat"); + break; + } + } + if (icon_file == nullptr && !program.localized_icons.empty()) { + icon_file = std::make_shared<FileSys::VectorVfsFile>( + program.localized_icons.front().second, "icon.dat"); + } + } + + const auto addon_count = std::count_if(nup->GetPackages().begin(), nup->GetPackages().end(), + [](const auto& package) { + return package.kind == + NupContainer::PackageKind::Addon; + }); + if (addon_count > 0) { + LOG_WARNING(Loader, + "NUP container has {} add-on content package(s); AoC mounting is not " + "supported yet", + addon_count); + } +} + +AppLoader_NUP::~AppLoader_NUP() = default; + +FileType AppLoader_NUP::IdentifyType(const FileSys::VirtualFile& nup_file) { + return FileSys::NupContainer::IsNupFile(nup_file) ? FileType::NUP : FileType::Error; +} + +AppLoader::LoadResult AppLoader_NUP::Load(Kernel::KProcess& process, Core::System& system) { + if (is_loaded) { + return {ResultStatus::ErrorAlreadyLoaded, {}}; + } + if (nup->GetStatus() != ResultStatus::Success) { + return {nup->GetStatus(), {}}; + } + if (!has_program || secondary_loader == nullptr) { + return {ResultStatus::ErrorNUPNoBasePackage, {}}; + } + + const auto result = secondary_loader->Load(process, system); + if (result.first != ResultStatus::Success) { + return result; + } + + system.GetFileSystemController().RegisterProcess( + process.GetProcessId(), base_title_id, + std::make_shared<FileSys::RomFSFactory>(*this, system.GetContentProvider(), + system.GetFileSystemController())); + + is_loaded = true; + return result; +} + +ResultStatus AppLoader_NUP::VerifyIntegrity( + std::function<bool(size_t, size_t)> progress_callback) { + if (nup->GetStatus() != ResultStatus::Success) { + return nup->GetStatus(); + } + return nup->VerifyIntegrity(std::move(progress_callback)); +} + +ResultStatus AppLoader_NUP::ReadRomFS(FileSys::VirtualFile& out_file) { + const auto base_index = nup->FindBasePackage(); + const auto base_romfs = base_index ? nup->GetRomFSDir(*base_index) : FileSys::VirtualDir{}; + const auto update_index = nup->FindBestUpdatePackage(); + const auto update_romfs = + update_index ? nup->GetRomFSDir(*update_index) : FileSys::VirtualDir{}; + // A standalone update carries the complete merged tree; a base+update pair + // overlays the update onto the base (SPEC §4.1.4). + const auto merged = FileSys::NupContainer::MergeShadowDirs(base_romfs, update_romfs); + if (merged == nullptr) { + return ResultStatus::ErrorNoRomFS; + } + out_file = FileSys::CreateRomFS(merged); + if (out_file == nullptr) { + return ResultStatus::ErrorNoRomFS; + } + return ResultStatus::Success; +} + +bool AppLoader_NUP::IsRomFSUpdatable() const { + // The update is already merged into the exposed RomFS. + return false; +} + +ResultStatus AppLoader_NUP::ReadProgramId(u64& out_program_id) { + if (!has_program) { + return ResultStatus::ErrorNotInitialized; + } + out_program_id = base_title_id; + return ResultStatus::Success; +} + +ResultStatus AppLoader_NUP::ReadProgramIds(std::vector<u64>& out_program_ids) { + if (!has_program) { + return ResultStatus::ErrorNotInitialized; + } + out_program_ids.push_back(base_title_id); + return ResultStatus::Success; +} + +ResultStatus AppLoader_NUP::ReadIcon(std::vector<u8>& buffer) { + if (icon_file == nullptr) { + return ResultStatus::ErrorNoIcon; + } + buffer = icon_file->ReadAllBytes(); + return ResultStatus::Success; +} + +ResultStatus AppLoader_NUP::ReadTitle(std::string& title) { + if (nacp_file == nullptr) { + return ResultStatus::ErrorNoControl; + } + title = nacp_file->GetApplicationName(); + if (title.empty()) { + return ResultStatus::ErrorNoControl; + } + return ResultStatus::Success; +} + +ResultStatus AppLoader_NUP::ReadControlData(FileSys::NACP& control) { + if (nacp_file == nullptr) { + return ResultStatus::ErrorNoControl; + } + control = *nacp_file; + return ResultStatus::Success; +} + +ResultStatus AppLoader_NUP::ReadNSOModules(Modules& modules) { + if (secondary_loader == nullptr) { + return ResultStatus::ErrorNotInitialized; + } + return secondary_loader->ReadNSOModules(modules); +} + +} // namespace Loader diff --git a/src/core/loader/nup.h b/src/core/loader/nup.h new file mode 100644 index 0000000000..1f9dd5922b --- /dev/null +++ b/src/core/loader/nup.h @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <memory> +#include <optional> +#include <string> + +#include "core/file_sys/nup_container.h" +#include "core/loader/loader.h" + +namespace FileSys { +class NACP; +class NupContainer; +} // namespace FileSys + +namespace Loader { + +/// Loads a NUP (NX Package) container: loose ExeFS/RomFS trees for a base +/// program plus its updates in one file. The newest update package is layered +/// over the base; add-on content packages are recognized but not yet mounted. +class AppLoader_NUP final : public AppLoader { +public: + explicit AppLoader_NUP(FileSys::VirtualFile file_); + ~AppLoader_NUP() override; + + /** + * Identifies whether or not the given file is a NUP container. + * + * @param nup_file The file to identify. + * + * @return FileType::NUP, or FileType::Error if the file is not a NUP. + */ + static FileType IdentifyType(const FileSys::VirtualFile& nup_file); + + FileType GetFileType() const override { + return FileType::NUP; + } + + LoadResult Load(Kernel::KProcess& process, Core::System& system) override; + + ResultStatus VerifyIntegrity(std::function<bool(size_t, size_t)> progress_callback) override; + + ResultStatus ReadRomFS(FileSys::VirtualFile& out_file) override; + bool IsRomFSUpdatable() const override; + ResultStatus ReadProgramId(u64& out_program_id) override; + ResultStatus ReadProgramIds(std::vector<u64>& out_program_ids) override; + ResultStatus ReadIcon(std::vector<u8>& buffer) override; + ResultStatus ReadTitle(std::string& title) override; + ResultStatus ReadControlData(FileSys::NACP& control) override; + ResultStatus ReadNSOModules(Modules& modules) override; + +private: + std::unique_ptr<FileSys::NupContainer> nup; + std::unique_ptr<AppLoader> secondary_loader; + + std::unique_ptr<FileSys::NACP> nacp_file; + FileSys::VirtualFile icon_file; + u64 base_title_id{}; + std::string display_version; + bool has_program{}; +}; + +} // namespace Loader |
