diff --git a/CMakeLists.txt b/CMakeLists.txt index 2446358..5171e7b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,19 +21,21 @@ set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # enable LTO (only in release builds) -if (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") - include(CheckIPOSupported) - check_ipo_supported(RESULT ipo_supported OUTPUT ipo_error) - if (ipo_supported) - message(STATUS "IPO / LTO enabled for ALL targets") - cmake_policy(SET CMP0069 NEW) - set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) - set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) +if (LTO) + if (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") + include(CheckIPOSupported) + check_ipo_supported(RESULT ipo_supported OUTPUT ipo_error) + if (ipo_supported) + message(STATUS "IPO / LTO enabled for ALL targets") + cmake_policy(SET CMP0069 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) + else() + message(STATUS "IPO / LTO not supported: <${ipo_error}>") + endif() else() - message(STATUS "IPO / LTO not supported: <${ipo_error}>") + message(STATUS "IPO / LTO not enabled in debug build") endif() -else() - message(STATUS "IPO / LTO not enabled in debug build") endif() function(dkp_fatal_if_not_found var package) diff --git a/CMakePresets.json b/CMakePresets.json index 9fed1a8..757f3f6 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -18,7 +18,8 @@ "inherits":["core"], "cacheVariables": { "CMAKE_BUILD_TYPE": "MinSizeRel", - "ENABLE_NETWORK_INSTALL": false + "ENABLE_NETWORK_INSTALL": false, + "LTO": true } }, { @@ -27,14 +28,19 @@ "inherits":["core"], "cacheVariables": { "CMAKE_BUILD_TYPE": "MinSizeRel", - "ENABLE_NETWORK_INSTALL": true + "ENABLE_NETWORK_INSTALL": true, + "LTO": true } }, { - "name": "Debug", - "displayName": "Debug", + "name": "Dev", + "displayName": "Dev", "inherits":["core"], - "cacheVariables": { "CMAKE_BUILD_TYPE":"Debug" } + "cacheVariables": { + "CMAKE_BUILD_TYPE": "MinSizeRel", + "ENABLE_NETWORK_INSTALL": true, + "LTO": false + } } ], "buildPresets": [ @@ -49,8 +55,8 @@ "jobs": 16 }, { - "name": "Debug", - "configurePreset": "Debug", + "name": "Dev", + "configurePreset": "Dev", "jobs": 16 } ] diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index 4f8a3ae..dbc76ee 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -91,6 +91,8 @@ add_executable(sphaira source/fatfs.cpp source/utils/devoptab_save.cpp + source/utils/devoptab_nsp.cpp + source/utils/devoptab_xci.cpp # todo: # source/utils/devoptab_zip.cpp @@ -143,6 +145,8 @@ target_compile_options(sphaira PRIVATE # disabled as it warns for strcat 2 paths together, but it will never # overflow due to fs enforcing a max path len anyway. -Wno-format-truncation + # many false positives when LTO is not enabled. + -Wno-suggest-final-types # the below are taken from my gba emulator, they've served me well ;) -Wformat-overflow=2 @@ -159,9 +163,7 @@ target_compile_options(sphaira PRIVATE -Wcast-qual -Wcast-align -Wimplicit-fallthrough=5 - -Wsuggest-final-types -Wuninitialized - -fdiagnostics-all-candidates ) include(FetchContent) diff --git a/sphaira/include/ui/menus/filebrowser.hpp b/sphaira/include/ui/menus/filebrowser.hpp index 005edac..4bb4be7 100644 --- a/sphaira/include/ui/menus/filebrowser.hpp +++ b/sphaira/include/ui/menus/filebrowser.hpp @@ -305,6 +305,10 @@ struct FsView final : Widget { void DisplayOptions(); void DisplayAdvancedOptions(); + void MountNspFs(); + void MountXciFs(); + void MountZipFs(); + // private: Base* m_menu{}; ViewSide m_side{}; @@ -476,4 +480,22 @@ auto IsSamePath(std::string_view a, std::string_view b) -> bool; auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool; auto IsExtension(std::string_view ext, std::span list) -> bool; + +struct FsStdioWrapper final : fs::FsStdio { + using OnExit = std::function; + FsStdioWrapper(const fs::FsPath& root, const OnExit& on_exit) : fs::FsStdio{true, root}, m_on_exit{on_exit} { + + } + + ~FsStdioWrapper() { + if (m_on_exit) { + m_on_exit(); + } + } + + const OnExit m_on_exit; +}; + +void MountFsHelper(const std::shared_ptr& fs, const fs::FsPath& name); + } // namespace sphaira::ui::menu::filebrowser diff --git a/sphaira/include/utils/devoptab.hpp b/sphaira/include/utils/devoptab.hpp index 886dd6d..a0f466a 100644 --- a/sphaira/include/utils/devoptab.hpp +++ b/sphaira/include/utils/devoptab.hpp @@ -10,15 +10,15 @@ Result MountFromSavePath(u64 id, fs::FsPath& out_path); void UnmountSave(u64 id); // todo: -void MountZip(fs::Fs* fs, const fs::FsPath& mount, fs::FsPath& out_path); +Result MountZip(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path); void UmountZip(const fs::FsPath& mount); -// todo: -void MountNsp(fs::Fs* fs, const fs::FsPath& mount, fs::FsPath& out_path); +Result MountNsp(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path); void UmountNsp(const fs::FsPath& mount); -// todo: -void MountXci(fs::Fs* fs, const fs::FsPath& mount, fs::FsPath& out_path); +Result MountXci(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path); void UmountXci(const fs::FsPath& mount); +bool fix_path(const char* str, char* out); + } // namespace sphaira::devoptab diff --git a/sphaira/include/yati/container/xci.hpp b/sphaira/include/yati/container/xci.hpp index d1311ec..ab8c98e 100644 --- a/sphaira/include/yati/container/xci.hpp +++ b/sphaira/include/yati/container/xci.hpp @@ -8,8 +8,19 @@ namespace sphaira::yati::container { struct Xci final : Base { + + struct Partition { + // name of the partition. + std::string name; + // all the collections for this partition, may be empty. + Collections collections; + }; + + using Partitions = std::vector; + using Base::Base; Result GetCollections(Collections& out) override; + Result GetPartitions(Partitions& out); }; } // namespace sphaira::yati::container diff --git a/sphaira/source/ui/menus/filebrowser.cpp b/sphaira/source/ui/menus/filebrowser.cpp index e8358f6..631275c 100644 --- a/sphaira/source/ui/menus/filebrowser.cpp +++ b/sphaira/source/ui/menus/filebrowser.cpp @@ -7,6 +7,8 @@ #include "ui/error_box.hpp" #include "ui/menus/file_viewer.hpp" +#include "utils/devoptab.hpp" + #include "log.hpp" #include "app.hpp" #include "ui/nvg_util.hpp" @@ -90,6 +92,12 @@ constexpr std::string_view IMAGE_EXTENSIONS[] = { constexpr std::string_view INSTALL_EXTENSIONS[] = { "nsp", "xci", "nsz", "xcz", }; +constexpr std::string_view NSP_EXTENSIONS[] = { + "nsp", "nsz", +}; +constexpr std::string_view XCI_EXTENSIONS[] = { + "xci", "xcz", +}; // these are files that are already compressed or encrypted and should // be stored raw in a zip file. constexpr std::string_view COMPRESSED_EXTENSIONS[] = { @@ -678,6 +686,10 @@ void FsView::OnClick() { nro_launch(GetNewPathCurrent()); } }); + } else if (IsExtension(entry.GetExtension(), NSP_EXTENSIONS)) { + MountNspFs(); + } else if (IsExtension(entry.GetExtension(), XCI_EXTENSIONS)) { + MountXciFs(); } else if (IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) { InstallFiles(); } else if (IsSd()) { @@ -1950,6 +1962,38 @@ void FsView::DisplayAdvancedOptions() { }); } +void FsView::MountNspFs() { + fs::FsPath mount; + const auto rc = devoptab::MountNsp(GetFs(), GetNewPathCurrent(), mount); + App::PushErrorBox(rc, "Failed to mount NSP."_i18n); + + if (R_SUCCEEDED(rc)) { + auto fs = std::make_shared(mount, [mount](){ + devoptab::UmountNsp(mount); + }); + + MountFsHelper(fs, GetEntry().GetName()); + } +} + +void FsView::MountXciFs() { + fs::FsPath mount; + const auto rc = devoptab::MountXci(GetFs(), GetNewPathCurrent(), mount); + App::PushErrorBox(rc, "Failed to mount XCI."_i18n); + + if (R_SUCCEEDED(rc)) { + auto fs = std::make_shared(mount, [mount](){ + devoptab::UmountXci(mount); + }); + + MountFsHelper(fs, GetEntry().GetName()); + } +} + +void FsView::MountZipFs() { + //todo: +} + Base::Base(u32 flags, u32 options) : Base{CreateFs(FS_ENTRY_DEFAULT), FS_ENTRY_DEFAULT, {}, false, flags, options} { } @@ -2299,4 +2343,15 @@ auto Base::CreateFs(const FsEntry& fs_entry) -> std::shared_ptr { std::unreachable(); } +void MountFsHelper(const std::shared_ptr& fs, const fs::FsPath& name) { + const filebrowser::FsEntry fs_entry{ + .name = name, + .root = fs->Root(), + .type = filebrowser::FsType::Custom, + .flags = filebrowser::FsEntryFlag_ReadOnly, + }; + + App::Push(fs, fs_entry, fs->Root()); +} + } // namespace sphaira::ui::menu::filebrowser diff --git a/sphaira/source/ui/menus/game_nca_menu.cpp b/sphaira/source/ui/menus/game_nca_menu.cpp index a3da765..33f330c 100644 --- a/sphaira/source/ui/menus/game_nca_menu.cpp +++ b/sphaira/source/ui/menus/game_nca_menu.cpp @@ -411,14 +411,7 @@ Result Menu::MountNcaFs() { auto fs = std::make_shared(program_id, type, path); R_TRY(fs->GetFsOpenResult()); - const filebrowser::FsEntry fs_entry{ - .name = "NCA", - .root = "/", - .type = filebrowser::FsType::Custom, - .flags = filebrowser::FsEntryFlag_ReadOnly, - }; - - App::Push(fs, fs_entry, "/"); + filebrowser::MountFsHelper(fs, hexIdToStr(e.content_id).str); R_SUCCEED(); } diff --git a/sphaira/source/ui/menus/homebrew.cpp b/sphaira/source/ui/menus/homebrew.cpp index 916c50c..7145cb7 100644 --- a/sphaira/source/ui/menus/homebrew.cpp +++ b/sphaira/source/ui/menus/homebrew.cpp @@ -523,8 +523,8 @@ struct NroRomFS final : fs::FsStdio { }; Result Menu::MountRomfsFs() { - const char* name = "nro_romfs"; - const char* root = "nro_romfs:/"; + static const char* name = "nro_romfs"; + static const char* root = "nro_romfs:/"; const auto& e = GetEntry(); // todo: add errors for when nro doesn't have romfs. @@ -540,16 +540,11 @@ Result Menu::MountRomfsFs() { R_THROW(rc); } - auto fs = std::make_shared(name, root); + auto fs = std::make_shared(root, [](){ + romfsUnmount(name); + }); - const filebrowser::FsEntry fs_entry{ - .name = e.GetName(), - .root = root, - .type = filebrowser::FsType::Custom, - .flags = filebrowser::FsEntryFlag_ReadOnly, - }; - - App::Push(fs, fs_entry, root); + filebrowser::MountFsHelper(fs, e.GetName()); R_SUCCEED(); } diff --git a/sphaira/source/ui/menus/save_menu.cpp b/sphaira/source/ui/menus/save_menu.cpp index 4659a5d..881121e 100644 --- a/sphaira/source/ui/menus/save_menu.cpp +++ b/sphaira/source/ui/menus/save_menu.cpp @@ -42,18 +42,6 @@ constexpr const char* NX_SAVE_META_NAME = ".nx_save_meta.bin"; constinit UEvent g_change_uevent; -struct SystemSaveFs final : fs::FsStdio { - SystemSaveFs(u64 id, const fs::FsPath& root) : FsStdio{true, root}, m_id{id} { - - } - - ~SystemSaveFs() { - devoptab::UnmountSave(m_id); - } - - const u64 m_id; -}; - // https://github.com/J-D-K/JKSV/issues/264#issuecomment-2618962807 struct NXSaveMeta { u32 magic{}; // NX_SAVE_META_MAGIC @@ -1118,21 +1106,16 @@ Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& loc Result Menu::MountSaveFs() { const auto& e = m_entries[m_index]; - fs::FsPath root; if (e.system_save_data_id) { + fs::FsPath root; R_TRY(devoptab::MountFromSavePath(e.system_save_data_id, root)); - auto fs = std::make_shared(e.system_save_data_id, root); + auto fs = std::make_shared(root, [&e](){ + devoptab::UnmountSave(e.system_save_data_id); + }); - const filebrowser::FsEntry fs_entry{ - .name = e.GetName(), - .root = root, - .type = filebrowser::FsType::Custom, - .flags = filebrowser::FsEntryFlag_ReadOnly, - }; - - App::Push(fs, fs_entry, root); + filebrowser::MountFsHelper(fs, e.GetName()); } else { const auto save_data_space_id = (FsSaveDataSpaceId)e.save_data_space_id; @@ -1146,15 +1129,7 @@ Result Menu::MountSaveFs() { auto fs = std::make_shared((FsSaveDataType)e.save_data_type, save_data_space_id, &attr, true); R_TRY(fs->GetFsOpenResult()); - - const filebrowser::FsEntry fs_entry{ - .name = e.GetName(), - .root = "/", - .type = filebrowser::FsType::Custom, - .flags = filebrowser::FsEntryFlag_ReadOnly, - }; - - App::Push(fs, fs_entry, "/"); + filebrowser::MountFsHelper(fs, e.GetName()); } R_SUCCEED(); diff --git a/sphaira/source/utils/devoptab_nsp.cpp b/sphaira/source/utils/devoptab_nsp.cpp new file mode 100644 index 0000000..4c84bd3 --- /dev/null +++ b/sphaira/source/utils/devoptab_nsp.cpp @@ -0,0 +1,285 @@ + +#include "utils/devoptab.hpp" +#include "defines.hpp" +#include "log.hpp" + +#include "yati/container/nsp.hpp" +#include "yati/container/xci.hpp" +#include "yati/source/file.hpp" + +#include +#include +#include +#include +#include + +namespace sphaira::devoptab { +namespace { + +struct Device { + std::unique_ptr source; + yati::container::Collections collections; +}; + +struct File { + Device* device; + const yati::container::CollectionEntry* collection; + size_t off; +}; + +struct Dir { + Device* device; + u32 index; +}; + +int set_errno(struct _reent *r, int err) { + r->_errno = err; + return -1; +} + +int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) { + auto device = (Device*)r->deviceData; + auto file = static_cast(fileStruct); + std::memset(file, 0, sizeof(*file)); + + char path[FS_MAX_PATH]; + if (!fix_path(_path, path)) { + return set_errno(r, ENOENT); + } + + for (const auto& collection : device->collections) { + if (path == "/" + collection.name) { + file->device = device; + file->collection = &collection; + return r->_errno = 0; + } + } + + return set_errno(r, ENOENT); +} + +int devoptab_close(struct _reent *r, void *fd) { + auto file = static_cast(fd); + std::memset(file, 0, sizeof(*file)); + + return r->_errno = 0; +} + +ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) { + auto file = static_cast(fd); + + const auto& collection = file->collection; + len = std::min(len, collection->size - file->off); + + u64 bytes_read; + if (R_FAILED(file->device->source->Read(ptr, collection->offset + file->off, len, &bytes_read))) { + return set_errno(r, ENOENT); + } + + file->off += bytes_read; + return bytes_read; +} + +off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) { + auto file = static_cast(fd); + const auto& collection = file->collection; + + if (dir == SEEK_CUR) { + pos += file->off; + } else if (dir == SEEK_END) { + pos = collection->size; + } + + r->_errno = 0; + return file->off = std::clamp(pos, 0, collection->size); +} + +int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) { + auto file = static_cast(fd); + const auto& collection = file->collection; + + std::memset(st, 0, sizeof(*st)); + st->st_nlink = 1; + st->st_size = collection->size; + st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + return r->_errno = 0; +} + +DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) { + auto device = (Device*)r->deviceData; + auto dir = static_cast(dirState->dirStruct); + std::memset(dir, 0, sizeof(*dir)); + + char path[FS_MAX_PATH]; + if (!fix_path(_path, path)) { + set_errno(r, ENOENT); + return NULL; + } + + if (!std::strcmp(path, "/")) { + dir->device = device; + } else { + set_errno(r, ENOENT); + return NULL; + } + + r->_errno = 0; + return dirState; +} + +int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { + auto dir = static_cast(dirState->dirStruct); + + dir->index = 0; + + return r->_errno = 0; +} + +int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) { + auto dir = static_cast(dirState->dirStruct); + std::memset(filestat, 0, sizeof(*filestat)); + + if (dir->index >= dir->device->collections.size()) { + return set_errno(r, ENOENT); + } + + const auto& collection = dir->device->collections[dir->index]; + filestat->st_nlink = 1; + filestat->st_size = collection.size; + filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + std::strcpy(filename, collection.name.c_str()); + + dir->index++; + return r->_errno = 0; +} + +int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { + auto dir = static_cast(dirState->dirStruct); + std::memset(dir, 0, sizeof(*dir)); + + return r->_errno = 0; +} + +int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { + auto device = (Device*)r->deviceData; + + log_write("[\t\tDEV] lstat\n"); + + char path[FS_MAX_PATH]; + if (!fix_path(_path, path)) { + return set_errno(r, ENOENT); + } + + std::memset(st, 0, sizeof(*st)); + + if (!std::strcmp(path, "/")) { + st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + } else { + const auto it = std::ranges::find_if(device->collections, [path](auto& e){ + return path == "/" + e.name; + }); + + if (it == device->collections.end()) { + return set_errno(r, ENOENT); + } + + st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + st->st_size = it->size; + } + + st->st_nlink = 1; + + return r->_errno = 0; +} + +constexpr devoptab_t DEVOPTAB = { + .structSize = sizeof(File), + .open_r = devoptab_open, + .close_r = devoptab_close, + .read_r = devoptab_read, + .seek_r = devoptab_seek, + .fstat_r = devoptab_fstat, + .stat_r = devoptab_lstat, + .dirStateSize = sizeof(Dir), + .diropen_r = devoptab_diropen, + .dirreset_r = devoptab_dirreset, + .dirnext_r = devoptab_dirnext, + .dirclose_r = devoptab_dirclose, + .lstat_r = devoptab_lstat, +}; + +struct Entry { + Device device; + devoptab_t devoptab; + fs::FsPath path; + fs::FsPath mount; + char name[32]; + s32 ref_count; +}; + +Mutex g_mutex; +std::vector g_entries; +u32 g_mount_idx; + +} // namespace + +Result MountNsp(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) { + SCOPED_MUTEX(&g_mutex); + + // check if we already have the save mounted. + for (auto& e : g_entries) { + if (e.path == path) { + e.ref_count++; + out_path = e.mount; + R_SUCCEED(); + } + } + + // create new entry. + auto& entry = g_entries.emplace_back(); + + auto source = std::make_unique(fs, path); + // R_TRY(yati::container::Xci(source.get()).ReadAll()); + yati::container::Nsp nsp{source.get()}; + R_TRY(nsp.GetCollections(entry.device.collections)); + + entry.path = path; + entry.devoptab = DEVOPTAB; + entry.devoptab.name = entry.name; + entry.devoptab.deviceData = &entry.device; + entry.device.source = std::move(source); + std::snprintf(entry.name, sizeof(entry.name), "nsp_%u", g_mount_idx); + std::snprintf(entry.mount, sizeof(entry.mount), "nsp_%u:/", g_mount_idx); + + R_UNLESS(AddDevice(&entry.devoptab) >= 0, 0x1); + log_write("[NSP] DEVICE SUCCESS %s %s\n", path.s, entry.name); + + out_path = entry.mount; + entry.ref_count++; + g_mount_idx++; + + R_SUCCEED(); +} + +void UmountNsp(const fs::FsPath& mount) { + SCOPED_MUTEX(&g_mutex); + + auto itr = std::ranges::find_if(g_entries, [&mount](auto& e){ + return mount == e.mount; + }); + + if (itr == g_entries.end()) { + return; + } + + if (itr->ref_count) { + itr->ref_count--; + } + + if (!itr->ref_count) { + RemoveDevice(mount); + g_entries.erase(itr); + } +} + +} // namespace sphaira::devoptab diff --git a/sphaira/source/utils/devoptab_save.cpp b/sphaira/source/utils/devoptab_save.cpp index d4dd03c..e8fab99 100644 --- a/sphaira/source/utils/devoptab_save.cpp +++ b/sphaira/source/utils/devoptab_save.cpp @@ -39,47 +39,7 @@ struct Dir { u32 next_file; }; -bool fix_path(const char* str, char* out) { - // log_write("[SAVE] got path: %s\n", str); - - str = std::strrchr(str, ':'); - if (!str) { - return false; - } - - // skip over ':' - str++; - size_t len = 0; - - for (size_t i = 0; str[i]; i++) { - // skip multiple slashes. - if (i && str[i] == '/' && str[i - 1] == '/') { - continue; - } - - // add leading slash. - if (!i && str[i] != '/') { - out[len++] = '/'; - } - - // save single char. - out[len++] = str[i]; - } - - // root path uses "" - if (len == 1 && out[0] == '/') { - // out[0] = '\0'; - } - - // null the end. - out[len] = '\0'; - - // log_write("[SAVE] end path: %s\n", out); - - return true; -} - -static int set_errno(struct _reent *r, int err) { +int set_errno(struct _reent *r, int err) { r->_errno = err; return -1; } @@ -242,8 +202,6 @@ int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { auto device = (Device*)r->deviceData; - log_write("[\t\tDEV] lstat\n"); - char path[FS_MAX_PATH]; if (!fix_path(_path, path)) { return set_errno(r, ENOENT); @@ -300,6 +258,47 @@ void MakeMountPath(u64 id, fs::FsPath& out_path) { } // namespace +bool fix_path(const char* str, char* out) { + // log_write("[SAVE] got path: %s\n", str); + + str = std::strrchr(str, ':'); + if (!str) { + return false; + } + + // skip over ':' + str++; + size_t len = 0; + + // todo: hanle utf8 paths. + for (size_t i = 0; str[i]; i++) { + // skip multiple slashes. + if (i && str[i] == '/' && str[i - 1] == '/') { + continue; + } + + // add leading slash. + if (!i && str[i] != '/') { + out[len++] = '/'; + } + + // save single char. + out[len++] = str[i]; + } + + // skip trailing slash. + if (len > 1 && out[len - 1] == '/') { + out[len - 1] = '\0'; + } + + // null the end. + out[len] = '\0'; + + // log_write("[SAVE] end path: %s\n", out); + + return true; +} + Result MountFromSavePath(u64 id, fs::FsPath& out_path) { SCOPED_MUTEX(&g_mutex); diff --git a/sphaira/source/utils/devoptab_xci.cpp b/sphaira/source/utils/devoptab_xci.cpp new file mode 100644 index 0000000..9e6978e --- /dev/null +++ b/sphaira/source/utils/devoptab_xci.cpp @@ -0,0 +1,304 @@ + +#include "utils/devoptab.hpp" +#include "defines.hpp" +#include "log.hpp" + +#include "yati/container/xci.hpp" +#include "yati/source/file.hpp" + +#include +#include +#include +#include +#include + +namespace sphaira::devoptab { +namespace { + +struct Device { + std::unique_ptr source; + yati::container::Xci::Partitions partitions; +}; + +struct File { + Device* device; + const yati::container::CollectionEntry* collection; + size_t off; +}; + +struct Dir { + Device* device; + const yati::container::Collections* collections; + u32 index; +}; + +int set_errno(struct _reent *r, int err) { + r->_errno = err; + return -1; +} + +int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) { + auto device = (Device*)r->deviceData; + auto file = static_cast(fileStruct); + std::memset(file, 0, sizeof(*file)); + + char path[FS_MAX_PATH]; + if (!fix_path(_path, path)) { + return set_errno(r, ENOENT); + } + + for (const auto& partition : device->partitions) { + for (const auto& collection : partition.collections) { + if (path == "/" + partition.name + "/" + collection.name) { + file->device = device; + file->collection = &collection; + return r->_errno = 0; + } + } + } + + return set_errno(r, ENOENT); +} + +int devoptab_close(struct _reent *r, void *fd) { + auto file = static_cast(fd); + std::memset(file, 0, sizeof(*file)); + + return r->_errno = 0; +} + +ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) { + auto file = static_cast(fd); + + const auto& collection = file->collection; + len = std::min(len, collection->size - file->off); + + u64 bytes_read; + if (R_FAILED(file->device->source->Read(ptr, collection->offset + file->off, len, &bytes_read))) { + return set_errno(r, ENOENT); + } + + file->off += bytes_read; + return bytes_read; +} + +off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) { + auto file = static_cast(fd); + const auto& collection = file->collection; + + if (dir == SEEK_CUR) { + pos += file->off; + } else if (dir == SEEK_END) { + pos = collection->size; + } + + r->_errno = 0; + return file->off = std::clamp(pos, 0, collection->size); +} + +int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) { + auto file = static_cast(fd); + const auto& collection = file->collection; + + std::memset(st, 0, sizeof(*st)); + st->st_nlink = 1; + st->st_size = collection->size; + st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + return r->_errno = 0; +} + +DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) { + auto device = (Device*)r->deviceData; + auto dir = static_cast(dirState->dirStruct); + std::memset(dir, 0, sizeof(*dir)); + + char path[FS_MAX_PATH]; + if (!fix_path(_path, path)) { + set_errno(r, ENOENT); + return NULL; + } + + if (!std::strcmp(path, "/")) { + dir->device = device; + r->_errno = 0; + return dirState; + } else { + for (const auto& partition : device->partitions) { + if (path == "/" + partition.name) { + dir->collections = &partition.collections; + r->_errno = 0; + return dirState; + } + } + } + + set_errno(r, ENOENT); + return NULL; +} + +int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { + auto dir = static_cast(dirState->dirStruct); + + dir->index = 0; + + return r->_errno = 0; +} + +int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) { + auto dir = static_cast(dirState->dirStruct); + std::memset(filestat, 0, sizeof(*filestat)); + + if (!dir->collections) { + if (dir->index >= dir->device->partitions.size()) { + return set_errno(r, ENOENT); + } + + filestat->st_nlink = 1; + filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + std::strcpy(filename, dir->device->partitions[dir->index].name.c_str()); + } else { + if (dir->index >= dir->collections->size()) { + return set_errno(r, ENOENT); + } + + const auto& collection = (*dir->collections)[dir->index]; + filestat->st_nlink = 1; + filestat->st_size = collection.size; + filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + std::strcpy(filename, collection.name.c_str()); + } + + dir->index++; + return r->_errno = 0; +} + +int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { + auto dir = static_cast(dirState->dirStruct); + std::memset(dir, 0, sizeof(*dir)); + + return r->_errno = 0; +} + +int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { + auto device = (Device*)r->deviceData; + + char path[FS_MAX_PATH]; + if (!fix_path(_path, path)) { + return set_errno(r, ENOENT); + } + + std::memset(st, 0, sizeof(*st)); + st->st_nlink = 1; + + if (!std::strcmp(path, "/")) { + st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + } else { + for (const auto& partition : device->partitions) { + if (path == "/" + partition.name) { + st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + return r->_errno = 0; + } + + for (const auto& collection : partition.collections) { + if (path == "/" + partition.name + "/" + collection.name) { + st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + st->st_size = collection.size; + return r->_errno = 0; + } + } + } + } + + return set_errno(r, ENOENT); +} + +constexpr devoptab_t DEVOPTAB = { + .structSize = sizeof(File), + .open_r = devoptab_open, + .close_r = devoptab_close, + .read_r = devoptab_read, + .seek_r = devoptab_seek, + .fstat_r = devoptab_fstat, + .stat_r = devoptab_lstat, + .dirStateSize = sizeof(Dir), + .diropen_r = devoptab_diropen, + .dirreset_r = devoptab_dirreset, + .dirnext_r = devoptab_dirnext, + .dirclose_r = devoptab_dirclose, + .lstat_r = devoptab_lstat, +}; + +struct Entry { + Device device; + devoptab_t devoptab; + fs::FsPath path; + fs::FsPath mount; + char name[32]; + s32 ref_count; +}; + +Mutex g_mutex; +std::vector g_entries; +u32 g_mount_idx; + +} // namespace + +Result MountXci(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) { + SCOPED_MUTEX(&g_mutex); + + // check if we already have the save mounted. + for (auto& e : g_entries) { + if (e.path == path) { + e.ref_count++; + out_path = e.mount; + R_SUCCEED(); + } + } + + // create new entry. + auto& entry = g_entries.emplace_back(); + + auto source = std::make_unique(fs, path); + yati::container::Xci xci{source.get()}; + R_TRY(xci.GetPartitions(entry.device.partitions)); + + entry.path = path; + entry.devoptab = DEVOPTAB; + entry.devoptab.name = entry.name; + entry.devoptab.deviceData = &entry.device; + entry.device.source = std::move(source); + std::snprintf(entry.name, sizeof(entry.name), "xci_%u", g_mount_idx); + std::snprintf(entry.mount, sizeof(entry.mount), "xci_%u:/", g_mount_idx); + + R_UNLESS(AddDevice(&entry.devoptab) >= 0, 0x1); + log_write("[XCI] DEVICE SUCCESS %s %s\n", path.s, entry.name); + + out_path = entry.mount; + entry.ref_count++; + g_mount_idx++; + + R_SUCCEED(); +} + +void UmountXci(const fs::FsPath& mount) { + SCOPED_MUTEX(&g_mutex); + + auto itr = std::ranges::find_if(g_entries, [&mount](auto& e){ + return mount == e.mount; + }); + + if (itr == g_entries.end()) { + return; + } + + if (itr->ref_count) { + itr->ref_count--; + } + + if (!itr->ref_count) { + RemoveDevice(mount); + g_entries.erase(itr); + } +} + +} // namespace sphaira::devoptab diff --git a/sphaira/source/yati/container/xci.cpp b/sphaira/source/yati/container/xci.cpp index 56f75c1..1dbe797 100644 --- a/sphaira/source/yati/container/xci.cpp +++ b/sphaira/source/yati/container/xci.cpp @@ -58,8 +58,40 @@ Result Hfs0GetPartition(source::Base* source, s64 off, Hfs0& out) { R_SUCCEED(); } +Result ReadPartitionIntoCollection(source::Base* source, const Hfs0& root, u32 index, Collections& out) { + log_write("[XCI] fetching %s partition\n", root.string_table[index].c_str()); + + Hfs0 hfs0{}; + R_TRY(Hfs0GetPartition(source, root.data_offset + root.file_table[index].data_offset, hfs0)); + log_write("[XCI] got %s partition\n", root.string_table[index].c_str()); + + for (u32 i = 0; i < hfs0.header.total_files; i++) { + CollectionEntry entry; + entry.name = hfs0.string_table[i]; + entry.offset = hfs0.data_offset + hfs0.file_table[i].data_offset; + entry.size = hfs0.file_table[i].data_size; + out.emplace_back(entry); + } + + log_write("[XCI] read %s partition count: %zu\n", root.string_table[index].c_str(), out.size()); + R_SUCCEED(); +} + } // namespace +Result Xci::GetPartitions(Partitions& out) { + Hfs0 root{}; + R_TRY(Hfs0GetPartition(m_source, HFS0_HEADER_OFFSET, root)); + + for (u32 i = 0; i < root.header.total_files; i++) { + Partition partition{root.string_table[i]}; + R_TRY(ReadPartitionIntoCollection(m_source, root, i, partition.collections)); + out.emplace_back(partition); + } + + R_SUCCEED(); +} + Result Xci::GetCollections(Collections& out) { Hfs0 root{}; R_TRY(Hfs0GetPartition(m_source, HFS0_HEADER_OFFSET, root)); @@ -68,20 +100,7 @@ Result Xci::GetCollections(Collections& out) { for (u32 i = 0; i < root.header.total_files; i++) { if (root.string_table[i] == "secure") { log_write("[XCI] found secure partition\n"); - - Hfs0 secure{}; - R_TRY(Hfs0GetPartition(m_source, root.data_offset + root.file_table[i].data_offset, secure)); - log_write("[XCI] got secure partition\n"); - - for (u32 i = 0; i < secure.header.total_files; i++) { - CollectionEntry entry; - entry.name = secure.string_table[i]; - entry.offset = secure.data_offset + secure.file_table[i].data_offset; - entry.size = secure.file_table[i].data_size; - out.emplace_back(entry); - } - - R_SUCCEED(); + return ReadPartitionIntoCollection(m_source, root, i, out); } else { log_write("[XCI] skipping partition %u | %s\n", i, root.string_table[i].c_str()); } diff --git a/sphaira/source/yati/yati.cpp b/sphaira/source/yati/yati.cpp index 2e4df31..1f31c49 100644 --- a/sphaira/source/yati/yati.cpp +++ b/sphaira/source/yati/yati.cpp @@ -646,7 +646,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) { if ((config.convert_to_standard_crypto && ticket) || config.lower_master_key) { t->nca->modified = true; - u8 keak_generation; + u8 keak_generation = 0; if (ticket) { const auto key_gen = header.key_gen; @@ -673,10 +673,6 @@ Result Yati::decompressFuncInternal(ThreadData* t) { R_TRY(nca::DecryptKeak(keys, header)); } - if (config.lower_master_key) { - keak_generation = 0; - } - R_TRY(nca::EncryptKeak(keys, header, keak_generation)); std::memset(&header.rights_id, 0, sizeof(header.rights_id)); }