From c600e2d4c0b288ffe9b62f1d9f4b427c53987bf5 Mon Sep 17 00:00:00 2001 From: Tony Wasserka Date: Sun, 24 Mar 2024 10:53:38 +0100 Subject: [PATCH] FS: Put files in standard locations --- source/framework/settings.hpp | 38 +++++++++++++++++++++++++- source/gui-sdl/main.cpp | 49 +++++++++++++++++++++++++++------- source/processes/fs.cpp | 34 +++++++++++------------ source/processes/fs.hpp | 1 + source/processes/fs_common.cpp | 4 +++ source/processes/fs_common.hpp | 2 ++ source/processes/pxi.cpp | 5 ++-- source/processes/pxi_fs.cpp | 20 +++++++------- source/settings.cpp | 10 +++++++ 9 files changed, 121 insertions(+), 42 deletions(-) diff --git a/source/framework/settings.hpp b/source/framework/settings.hpp index ba4eb82..0104f9a 100644 --- a/source/framework/settings.hpp +++ b/source/framework/settings.hpp @@ -9,6 +9,38 @@ namespace Settings { +// Path to use for configuration files +struct PathConfigDir : Config::Option { + static constexpr const char* name = "PathConfigDir"; + using type = std::string; + static type default_value() { return default_val; } + static type default_val; +}; + +// Path for read-only data files created during installation +struct PathImmutableDataDir : Config::Option { + static constexpr const char* name = "PathImmutableDataDir"; + using type = std::string; + static type default_value() { return default_val; } + static type default_val; +}; + +// Path for data that must be writable +struct PathDataDir : Config::Option { + static constexpr const char* name = "PathDataDir"; + using type = std::string; + static type default_value() { return default_val; } + static type default_val; +}; + +// Path for cache data that can be deleted without impacting normal operation +struct PathCacheDir : Config::Option { + static constexpr const char* name = "PathCacheDir"; + using type = std::string; + static type default_value() { return default_val; } + static type default_val; +}; + enum class CPUEngine { NARMive, }; @@ -96,7 +128,11 @@ struct EnableAudioEmulation : Config::BooleanOption { }; -struct Settings : Config::Options #include #include +#include +#include #include #include @@ -162,6 +164,30 @@ int main(int argc, char* argv[]) { return 0; } + // Prefer XDG_DATA_DIRS, then /usr/local/share, then /usr/share + auto data_dirs = std::string { getenv("XDG_DATA_DIRS") ? getenv("XDG_DATA_DIRS") : "/usr/local/share:/usr/share" }; + for (auto data_dir : data_dirs | ranges::views::split(':')) { + auto candidate = ranges::to(data_dir) + "/mikage"; + if (std::filesystem::exists(candidate)) { + settings.set(std::move(candidate)); + break; + } + } + + auto fill_home_path = [](std::string path) { + auto home_dir = getenv("HOME"); + if (!home_dir) { + throw std::runtime_error("Could not determine path to home directory. Please set the HOME environment variable"); + } + if (!path.starts_with("/HOME~/")) { + return path; + } + return path.replace(0, 6, home_dir); + }; + settings.set(fill_home_path(settings.get())); + settings.set(fill_home_path(settings.get())); + settings.set(fill_home_path(settings.get())); + settings.set(vm["dump_frames"].as()); settings.set(vm["launch_menu"].as()); if (enable_debugging) { @@ -234,6 +260,8 @@ if (bootstrap_nand) // Experimental system bootstrapper throw std::runtime_error("Failed to read update partition RomFS"); } + std::filesystem::path content_dir = settings.get() + "/data"; + for (uint32_t metadata_offset = 0; metadata_offset < level3_header.file_metadata_size;) { FileFormat::RomFSFileMetadata file_metadata; std::tie(result, bytes_read) = romfs->Read(file_context, lv3_offset + level3_header.file_metadata_offset + metadata_offset, sizeof(file_metadata), HLE::PXI::FS::FileBufferInHostMemory(file_metadata)); @@ -264,8 +292,7 @@ if (bootstrap_nand) // Experimental system bootstrapper fprintf(stderr, "CIA header size: %#x\n", cia_header.header_size); - std::filesystem::path content_dir = "./data"; - InstallCIA(std::move(content_dir), *frontend_logger, keydb, file_context, *cia_file); + InstallCIA(content_dir, *frontend_logger, keydb, file_context, *cia_file); romfs = cia_file->ReleaseParentAndClose(); } @@ -274,23 +301,27 @@ if (bootstrap_nand) // Experimental system bootstrapper metadata_offset += (file_metadata.name_size + 3) & ~3; } + // Copy substitute titles + std::filesystem::copy( settings.get() + "/nand/title", content_dir, + std::filesystem::copy_options::skip_existing | std::filesystem::copy_options::recursive); + // Initial system setup requires title 0004001000022400 to be present. // Normally, this is the Camera app, which isn't bundled in game update // partitions. Since the setup only reads the ExeFS icon that aren't // specific to the Camera app (likely to perform general checks), we // use the Download Play app as a drop-in replacement if needed. - if (!std::filesystem::exists("./data/00040010/00022400")) { - std::filesystem::copy("./data/00040010/00022100", "./data/00040010/00022400", std::filesystem::copy_options::recursive); + if (!std::filesystem::exists(content_dir / "00040010/00022400")) { + std::filesystem::copy(content_dir / "00040010/00022100", content_dir / "00040010/00022400", std::filesystem::copy_options::recursive); } - std::filesystem::create_directories("./data/ro/sys"); - std::filesystem::create_directories("./data/rw/sys"); - std::filesystem::create_directories("./data/twlp"); + std::filesystem::create_directories(content_dir / "ro/sys"); + std::filesystem::create_directories(content_dir / "rw/sys"); + std::filesystem::create_directories(content_dir / "twlp"); // Create dummy HWCAL0, required for cfg module to work without running initial system setup first char zero[128]{}; { - std::ofstream hwcal0("./data/ro/sys/HWCAL0.dat"); + std::ofstream hwcal0(content_dir / "ro/sys/HWCAL0.dat"); const auto size = 2512; for (unsigned i = 0; i < size / sizeof(zero); ++i) { hwcal0.write(zero, sizeof(zero)); @@ -301,7 +332,7 @@ if (bootstrap_nand) // Experimental system bootstrapper // Set up dummy rw/sys/SecureInfo_A with region EU // TODO: Let the user select the region { - std::ofstream info("./data/rw/sys/SecureInfo_A"); + std::ofstream info(content_dir / "rw/sys/SecureInfo_A"); info.write(zero, sizeof(zero)); info.write(zero, sizeof(zero)); char region = 2; // Europe diff --git a/source/processes/fs.cpp b/source/processes/fs.cpp index cfe2003..9a202a1 100644 --- a/source/processes/fs.cpp +++ b/source/processes/fs.cpp @@ -50,14 +50,9 @@ using OS::ClientSession; using OS::ServerSession; using OS::Thread; -static std::filesystem::path GetRootDataDirectory() { - return "./data/"; -} - // TODO: Steel diver fails to boot if this directory doesn't exist - should make sure to create it automatically! -static std::filesystem::path HostSdmcDirectory() { - auto path_str = "./data/sdmc"; - return std::filesystem::path(path_str); +static std::filesystem::path HostSdmcDirectory(Settings::Settings& settings) { + return GetRootDataDirectory(settings) / "sdmc/"; } /** @@ -100,13 +95,13 @@ class RomfsFile : public File { std::string filename; public: - RomfsFile(ProgramInfo& program_info) : program_info(program_info) { + RomfsFile(FSContext& context, ProgramInfo& program_info) : program_info(program_info) { if (program_info.media_type == 0) { // TODO: The content ID is hardcoded currently. uint32_t content_id = 0; filename = [&]() -> std::string { std::stringstream filename; - filename << "data/"; + filename << GetRootDataDirectory(context.settings).string() << "/"; filename << std::hex << std::setw(8) << std::setfill('0') << (program_info.program_id >> 32); filename << "/"; filename << std::hex << std::setw(8) << std::setfill('0') << (program_info.program_id & 0xFFFFFFFF); @@ -1061,7 +1056,7 @@ public: } static std::string GetBaseFileParentPath(FSContext& context, uint64_t saveid) { - return fmt::format("./data/data/{:016x}/sysdata/{:08x}", GetId0(context), saveid & 0xFFFFFFFF); + return (GetRootDataDirectory(context.settings) / fmt::format("data/{:016x}/sysdata/{:08x}", GetId0(context), saveid & 0xFFFFFFFF)).string(); } static std::string GetBaseFilePath(FSContext& context, uint64_t saveid) { @@ -1381,9 +1376,9 @@ public: uint32_t program_id_high = (program_info.program_id >> 32); uint32_t program_id_low = (program_info.program_id & 0xFFFFFFFF); if (program_info.media_type == 1 /* SD */) { - return fmt::format("data/sdmc/Nintendo 3DS/{:016x}/{:016x}/title/{:08x}/{:08x}/data", GetId0(context), GetId1(context), program_id_high, program_id_low); + return HostSdmcDirectory(context.settings) / fmt::format("Nintendo 3DS/{:016x}/{:016x}/title/{:08x}/{:08x}/data", GetId0(context), GetId1(context), program_id_high, program_id_low); } else if (program_info.media_type == 2 /* Gamecard */) { - return fmt::format("data/card_savedata/{:08x}/{:08x}/data", program_id_high, program_id_low); + return GetRootDataDirectory(context.settings) / fmt::format("card_savedata/{:08x}/{:08x}/data", program_id_high, program_id_low); } else { throw std::runtime_error("Unknown media type"); } @@ -1465,9 +1460,9 @@ private: uint32_t extdata_id_high = info.save_id >> 32; if (info.media_type == Platform::FS::MediaType::NAND) { - return fmt::format("data/data/{:016x}/extdata/{:08x}/{:08x}", GetId0(context), extdata_id_high, extdata_id_low); + return (GetRootDataDirectory(context.settings) / fmt::format("data/{:016x}/extdata/{:08x}/{:08x}", GetId0(context), extdata_id_high, extdata_id_low)).string(); } else { - return (HostSdmcDirectory() / fmt::format("Nintendo 3DS/{:016x}/{:016x}/extdata/{:08x}/{:08x}", GetId0(context), GetId1(context), extdata_id_high, extdata_id_low)).string(); + return (HostSdmcDirectory(context.settings) / fmt::format("Nintendo 3DS/{:016x}/{:016x}/extdata/{:08x}/{:08x}", GetId0(context), GetId1(context), extdata_id_high, extdata_id_low)).string(); } }); } @@ -1876,7 +1871,8 @@ static auto BindMemFn(Func f, Class* c) { FakeFS::FakeFS(FakeThread& thread) : process(thread.GetParentProcess()), - logger(*thread.GetLogger()) { + logger(*thread.GetLogger()), + settings(process.GetOS().settings) { OS::Result result; @@ -2548,7 +2544,7 @@ OpenArchive(FakeThread& thread, FSContext& context, ProcessId process_id, uint32 case 0x00000009: { - auto archive = std::unique_ptr(new ArchiveHostDir(HostSdmcDirectory())); + auto archive = std::unique_ptr(new ArchiveHostDir(HostSdmcDirectory(context.settings))); return std::make_tuple(RESULT_OK, std::move(archive)); } @@ -2577,13 +2573,13 @@ OpenArchive(FakeThread& thread, FSContext& context, ProcessId process_id, uint32 case 0x1234567d: { - auto archive = std::unique_ptr(new ArchiveHostDir(GetRootDataDirectory() / "rw")); + auto archive = std::unique_ptr(new ArchiveHostDir(GetRootDataDirectory(context.settings) / "rw")); return std::make_tuple(RESULT_OK, std::move(archive)); } case 0x1234567e: { - auto archive = std::unique_ptr(new ArchiveHostDir(GetRootDataDirectory() / "ro")); + auto archive = std::unique_ptr(new ArchiveHostDir(GetRootDataDirectory(context.settings) / "ro")); return std::make_tuple(RESULT_OK, std::move(archive)); } @@ -2597,7 +2593,7 @@ OpenArchive(FakeThread& thread, FSContext& context, ProcessId process_id, uint32 // TWL photo. Opened by mset during initial system setup case 0x567890ac: { - auto archive = std::unique_ptr(new ArchiveHostDir(GetRootDataDirectory() / "twlp")); + auto archive = std::unique_ptr(new ArchiveHostDir(GetRootDataDirectory(context.settings) / "twlp")); return std::make_tuple(RESULT_OK, std::move(archive)); } diff --git a/source/processes/fs.hpp b/source/processes/fs.hpp index 7162d20..2b227c0 100644 --- a/source/processes/fs.hpp +++ b/source/processes/fs.hpp @@ -214,6 +214,7 @@ public: // TODO: Un-public-ize FakeProcess& process; spdlog::logger& logger; FileContext file_context { logger }; + Settings::Settings& settings; public: FakeFS(FakeThread& thread); diff --git a/source/processes/fs_common.cpp b/source/processes/fs_common.cpp index 60c16bd..818965b 100644 --- a/source/processes/fs_common.cpp +++ b/source/processes/fs_common.cpp @@ -137,4 +137,8 @@ ValidatedHostPath PathValidator::ValidateAndGetSandboxedTreePath(const Utf8PathT return ValidatedHostPath { std::move(new_path) }; } +std::filesystem::path GetRootDataDirectory(Settings::Settings& settings) { + return std::filesystem::path { settings.get() } / "data/"; +} + } diff --git a/source/processes/fs_common.hpp b/source/processes/fs_common.hpp index 18645c1..619edb3 100644 --- a/source/processes/fs_common.hpp +++ b/source/processes/fs_common.hpp @@ -95,4 +95,6 @@ protected: virtual std::filesystem::path GetSandboxHostRoot() const = 0; }; +std::filesystem::path GetRootDataDirectory(Settings::Settings& settings); + } // namespace HLE diff --git a/source/processes/pxi.cpp b/source/processes/pxi.cpp index 554f8ff..f2fa16e 100644 --- a/source/processes/pxi.cpp +++ b/source/processes/pxi.cpp @@ -1,3 +1,4 @@ +#include "fs_common.hpp" #include "pxi.hpp" #include "cryptopp/aes.h" #include "cryptopp/modes.h" @@ -114,7 +115,7 @@ FakePXI::FakePXI(FakeThread& thread) // Search for installed NAND titles // TODO: Move titles to ./data/title { - std::filesystem::path base_path = "./data/"; + std::filesystem::path base_path = GetRootDataDirectory(os.settings); auto parse_title_id_part = [](const std::string& filename) -> std::optional { // Expect an 8-digit zero-padded hexadecimal number @@ -270,7 +271,7 @@ FileFormat::ExHeader GetExtendedHeader(Thread& thread, const Platform::FS::Progr uint32_t content_id = 0; const std::string filename = Meta::invoke([&] { std::stringstream filename; - filename << "data/"; + filename << GetRootDataDirectory(thread.GetOS().settings).string(); filename << std::hex << std::setw(8) << std::setfill('0') << (title_info.program_id >> 32); filename << "/"; filename << std::hex << std::setw(8) << std::setfill('0') << (title_info.program_id & 0xFFFFFFFF); diff --git a/source/processes/pxi_fs.cpp b/source/processes/pxi_fs.cpp index d06e8a1..f630611 100644 --- a/source/processes/pxi_fs.cpp +++ b/source/processes/pxi_fs.cpp @@ -283,18 +283,13 @@ class ArchiveSystemSaveData final : public Archive { } public: - ArchiveSystemSaveData(uint32_t media_type) { + ArchiveSystemSaveData(Settings::Settings& settings, uint32_t media_type) { switch (media_type) { case 0: - base_path = "./data/data/"; - // TODO: This ID is actually generated from data in moveable.sed std::array id0; ranges::fill(id0, 0); - for (auto byte : id0) - base_path += fmt::format("{:02x}", byte); - - base_path += "/sysdata/"; + base_path = (GetRootDataDirectory(settings) / "data" / fmt::format("{:02x}", fmt::join(id0, "")) / "sysdata").string(); break; default: @@ -539,9 +534,10 @@ std::unique_ptr OpenNCCHSubFile(Thread& thread, Platform::FS::ProgramInfo return std::make_unique(); } - auto ncch_filename = fmt::format("./data/{:08x}/{:08x}/content/{:08x}.cxi", - program_info.program_id >> 32, program_info.program_id & 0xFFFFFFFF, content_id); - ncch = std::make_unique(ncch_filename, HostFile::Default); + auto ncch_filename = GetRootDataDirectory(thread.GetOS().settings) / + fmt::format("{:08x}/{:08x}/content/{:08x}.cxi", + program_info.program_id >> 32, program_info.program_id & 0xFFFFFFFF, content_id); + ncch = std::make_unique(ncch_filename.string(), HostFile::Default); } else if (gamecard && program_info.media_type == 2) { if (content_id > Meta::to_underlying(Loader::NCSDPartitionId::UpdateData)) { throw Mikage::Exceptions::Invalid("Invalid NCSD partition index {}", content_id); @@ -873,6 +869,8 @@ static std::tuple OpenArchive(FakeThread& thread, Context& con thread.GetLogger()->info(binary_path); + auto& settings = thread.GetOS().settings; + switch (archive_id) { case 0x567890b0: { @@ -889,7 +887,7 @@ static std::tuple OpenArchive(FakeThread& thread, Context& con // System SaveData stored on NAND // TODO: Should we verify that not more than 4 bytes have been given? auto media_type = path.Read(thread, 0); - auto archive = std::make_unique(media_type); + auto archive = std::make_unique(settings, media_type); auto archive_handle = context.next_archive_handle++; context.archives.emplace(std::make_pair(archive_handle, std::move(archive))); return std::make_tuple(RESULT_OK, archive_handle); diff --git a/source/settings.cpp b/source/settings.cpp index 4d926f5..2f33292 100644 --- a/source/settings.cpp +++ b/source/settings.cpp @@ -2,6 +2,16 @@ namespace Settings { +// std::filesystem doesn't understand "~", so we replace the defaults with +// "/HOME~" for safety. The frontend must replace this substring with the true +// home folder path on startup. +// If a buggy frontend doesn't do this, accessing "/HOME~" will reliably fail +// instead of constructing a relative path. +std::string PathConfigDir::default_val = "/HOME~/.config/mikage"; +std::string PathImmutableDataDir::default_val = "/usr/share/mikage"; +std::string PathDataDir::default_val = "/HOME~/.local/share/mikage"; +std::string PathCacheDir::default_val = "/HOME~/.cache/mikage"; + CPUEngine CPUEngineTag::default_val = CPUEngine::NARMive; }