diff options
| author | Assuka <59313695+a55uka@users.noreply.github.com> | 2026-11-01 23:05:04 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-11-01 23:05:04 +0000 |
| commit | 8eaea9d3769fecff3dd7202884f66423ed2c83b1 (patch) | |
| tree | 789cce85b9d5bb3f5ba8a52cc0dab9701fc35826 /src/core/loader/nup.cpp | |
| parent | 350c64706510b610bef4f925fe64c3d40f4fb86a (diff) | |
| parent | f6d03bc69d5bcda45bd054edda24521820c42deb (diff) | |
Merge pull request #258 from Djihads80/nx2
bring nx2-experimental up to date
Diffstat (limited to 'src/core/loader/nup.cpp')
| -rw-r--r-- | src/core/loader/nup.cpp | 280 |
1 files changed, 280 insertions, 0 deletions
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 |
