diff --git a/assets/romfs/assoc/fbneo_libretro_libnx.ini b/assets/romfs/assoc/fbneo_libretro_libnx.ini index fc13ee8..2885501 100644 --- a/assets/romfs/assoc/fbneo_libretro_libnx.ini +++ b/assets/romfs/assoc/fbneo_libretro_libnx.ini @@ -2,3 +2,4 @@ path=/retroarch/cores/fbneo_libretro_libnx.nro supported_extensions=zip|7z|cue|ccd database=FBNeo - Arcade Games +use_base_name=true diff --git a/assets/romfs/assoc/mame2000_libretro_libnx.ini b/assets/romfs/assoc/mame2000_libretro_libnx.ini index a58ec96..db17feb 100644 --- a/assets/romfs/assoc/mame2000_libretro_libnx.ini +++ b/assets/romfs/assoc/mame2000_libretro_libnx.ini @@ -2,3 +2,4 @@ path=/retroarch/cores/mame2000_libretro_libnx.nro supported_extensions=zip|7z database=MAME 2000 +use_base_name=true diff --git a/assets/romfs/assoc/mame2003_libretro_libnx.ini b/assets/romfs/assoc/mame2003_libretro_libnx.ini index be5d860..edd2294 100644 --- a/assets/romfs/assoc/mame2003_libretro_libnx.ini +++ b/assets/romfs/assoc/mame2003_libretro_libnx.ini @@ -2,3 +2,4 @@ path=/retroarch/cores/mame2003_libretro_libnx.nro supported_extensions=zip database=MAME 2003 +use_base_name=true diff --git a/assets/romfs/assoc/mame2003_plus_libretro_libnx.ini b/assets/romfs/assoc/mame2003_plus_libretro_libnx.ini index 1f33675..3913f3e 100644 --- a/assets/romfs/assoc/mame2003_plus_libretro_libnx.ini +++ b/assets/romfs/assoc/mame2003_plus_libretro_libnx.ini @@ -2,3 +2,4 @@ path=/retroarch/cores/mame2003_plus_libretro_libnx.nro supported_extensions=zip database=MAME 2003-Plus +use_base_name=true diff --git a/assets/romfs/assoc/xrick_libretro_libnx.ini b/assets/romfs/assoc/xrick_libretro_libnx.ini index 04f9665..def43f4 100644 --- a/assets/romfs/assoc/xrick_libretro_libnx.ini +++ b/assets/romfs/assoc/xrick_libretro_libnx.ini @@ -2,3 +2,4 @@ path=/retroarch/cores/xrick_libretro_libnx.nro supported_extensions=zip database=Rick Dangerous +use_base_name=true diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index 2fb5d02..7d2681e 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -47,6 +47,7 @@ add_executable(sphaira source/ui/menus/themezer.cpp source/ui/menus/ghdl.cpp source/ui/menus/usb_menu.cpp + source/ui/menus/ftp_menu.cpp source/ui/menus/gc_menu.cpp source/ui/error_box.cpp @@ -83,6 +84,8 @@ add_executable(sphaira source/yati/source/file.cpp source/yati/source/stdio.cpp source/yati/source/usb.cpp + source/yati/source/stream.cpp + source/yati/source/stream_file.cpp source/yati/nx/es.cpp source/yati/nx/keys.cpp @@ -143,7 +146,8 @@ set(FETCHCONTENT_QUIET FALSE) FetchContent_Declare(ftpsrv GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git - GIT_TAG 1.2.2 + # GIT_TAG 1.2.2 + GIT_TAG f8a30fd SOURCE_SUBDIR NONE ) diff --git a/sphaira/include/app.hpp b/sphaira/include/app.hpp index 7edafb8..29fcbf5 100644 --- a/sphaira/include/app.hpp +++ b/sphaira/include/app.hpp @@ -64,7 +64,7 @@ public: static void SetTheme(s64 theme_index); static auto GetThemeIndex() -> s64; - static auto GetDefaultImage(int* w = nullptr, int* h = nullptr) -> int; + static auto GetDefaultImage() -> int; // returns argv[0] static auto GetExePath() -> fs::FsPath; @@ -185,7 +185,6 @@ public: option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false}; option::OptionBool m_skip_if_already_installed{INI_SECTION, "skip_if_already_installed", true}; option::OptionBool m_ticket_only{INI_SECTION, "ticket_only", false}; - option::OptionBool m_patch_ticket{INI_SECTION, "patch_ticket", true}; option::OptionBool m_skip_base{INI_SECTION, "skip_base", false}; option::OptionBool m_skip_patch{INI_SECTION, "skip_patch", false}; option::OptionBool m_skip_addon{INI_SECTION, "skip_addon", false}; diff --git a/sphaira/include/ftpsrv_helper.hpp b/sphaira/include/ftpsrv_helper.hpp index 012a02d..6357f9c 100644 --- a/sphaira/include/ftpsrv_helper.hpp +++ b/sphaira/include/ftpsrv_helper.hpp @@ -1,8 +1,22 @@ #pragma once +#include + namespace sphaira::ftpsrv { bool Init(); void Exit(); +using OnInstallStart = std::function; +using OnInstallWrite = std::function; +using OnInstallClose = std::function; + +void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close); +void DisableInstallMode(); + +unsigned GetPort(); +bool IsAnon(); +const char* GetUser(); +const char* GetPass(); + } // namespace sphaira::ftpsrv diff --git a/sphaira/include/ui/menus/filebrowser.hpp b/sphaira/include/ui/menus/filebrowser.hpp index 03503fc..72b202a 100644 --- a/sphaira/include/ui/menus/filebrowser.hpp +++ b/sphaira/include/ui/menus/filebrowser.hpp @@ -89,6 +89,19 @@ struct FileAssocEntry { std::string name{}; // ini name std::vector ext{}; // list of ext std::vector database{}; // list of systems + bool use_base_name{}; // if set, uses base name (rom.zip) otherwise uses internal name (rom.gba) + + auto IsExtension(std::string_view extension, std::string_view internal_extension) const -> bool { + for (const auto& assoc_ext : ext) { + if (extension.length() == assoc_ext.length() && !strncasecmp(assoc_ext.data(), extension.data(), assoc_ext.length())) { + return true; + } + if (internal_extension.length() == assoc_ext.length() && !strncasecmp(assoc_ext.data(), internal_extension.data(), assoc_ext.length())) { + return true; + } + } + return false; + } }; struct LastFile { diff --git a/sphaira/include/ui/menus/ftp_menu.hpp b/sphaira/include/ui/menus/ftp_menu.hpp new file mode 100644 index 0000000..1fac789 --- /dev/null +++ b/sphaira/include/ui/menus/ftp_menu.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "ui/menus/menu_base.hpp" +#include "yati/source/stream.hpp" + +namespace sphaira::ui::menu::ftp { + +enum class State { + // not connected. + None, + // just connected, starts the transfer. + Connected, + // set whilst transfer is in progress. + Progress, + // set when the transfer is finished. + Done, + // failed to connect. + Failed, +}; + +struct StreamFtp final : yati::source::Stream { + StreamFtp(const fs::FsPath& path, std::stop_token token); + + Result ReadChunk(void* buf, s64 size, u64* bytes_read) override; + bool Push(const void* buf, s64 size); + void Disable(); + +// private: + fs::FsPath m_path{}; + std::stop_token m_token{}; + std::vector m_buffer{}; + Mutex m_mutex{}; + bool m_active{}; + // bool m_push_exit{}; +}; + +struct Menu final : MenuBase { + Menu(); + ~Menu(); + + void Update(Controller* controller, TouchInfo* touch) override; + void Draw(NVGcontext* vg, Theme* theme) override; + void OnFocusGained() override; + +// this should be private +// private: + std::shared_ptr m_source; + Thread m_thread{}; + Mutex m_mutex{}; + // the below are shared across threads, lock with the above mutex! + State m_state{State::None}; + + const char* m_user{}; + const char* m_pass{}; + unsigned m_port{}; + bool m_anon{}; +}; + +} // namespace sphaira::ui::menu::ftp diff --git a/sphaira/include/ui/menus/menu_base.hpp b/sphaira/include/ui/menus/menu_base.hpp index 386b269..a79ee35 100644 --- a/sphaira/include/ui/menus/menu_base.hpp +++ b/sphaira/include/ui/menus/menu_base.hpp @@ -29,6 +29,7 @@ private: std::string m_title_sub_heading{}; std::string m_sub_heading{}; +protected: struct tm m_tm{}; TimeStamp m_poll_timestamp{}; u32 m_battery_percetange{}; diff --git a/sphaira/include/yati/container/nsp.hpp b/sphaira/include/yati/container/nsp.hpp index d92d600..84b7d30 100644 --- a/sphaira/include/yati/container/nsp.hpp +++ b/sphaira/include/yati/container/nsp.hpp @@ -8,7 +8,6 @@ namespace sphaira::yati::container { struct Nsp final : Base { using Base::Base; Result GetCollections(Collections& out) override; - static Result Validate(source::Base* source); }; } // namespace sphaira::yati::container diff --git a/sphaira/include/yati/container/xci.hpp b/sphaira/include/yati/container/xci.hpp index cd70147..d1311ec 100644 --- a/sphaira/include/yati/container/xci.hpp +++ b/sphaira/include/yati/container/xci.hpp @@ -10,7 +10,6 @@ namespace sphaira::yati::container { struct Xci final : Base { using Base::Base; Result GetCollections(Collections& out) override; - static Result Validate(source::Base* source); }; } // namespace sphaira::yati::container diff --git a/sphaira/include/yati/nx/es.hpp b/sphaira/include/yati/nx/es.hpp index 77e4f84..74f079d 100644 --- a/sphaira/include/yati/nx/es.hpp +++ b/sphaira/include/yati/nx/es.hpp @@ -78,6 +78,6 @@ Result SetTicketData(std::span ticket, const es::TicketData* in); Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys); Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys); -Result PatchTicket(std::span ticket, const keys::Keys& keys, bool convert_personalised); +Result PatchTicket(std::span ticket, const keys::Keys& keys); } // namespace sphaira::es diff --git a/sphaira/include/yati/source/base.hpp b/sphaira/include/yati/source/base.hpp index be4b7bf..612ca84 100644 --- a/sphaira/include/yati/source/base.hpp +++ b/sphaira/include/yati/source/base.hpp @@ -10,6 +10,10 @@ struct Base { // virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0; virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0; + virtual bool IsStream() const { + return false; + } + Result GetOpenResult() const { return m_open_result; } diff --git a/sphaira/include/yati/source/stream.hpp b/sphaira/include/yati/source/stream.hpp new file mode 100644 index 0000000..00535d9 --- /dev/null +++ b/sphaira/include/yati/source/stream.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "base.hpp" +#include +#include + +namespace sphaira::yati::source { + +// streams are for data that do not allow for random access, +// such as FTP or MTP. +struct Stream : Base { + virtual ~Stream() = default; + virtual Result ReadChunk(void* buf, s64 size, u64* bytes_read) = 0; + + Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override; + + bool IsStream() const override { + return true; + } + +protected: + Result m_open_result{}; + +private: + s64 m_offset{}; +}; + +} // namespace sphaira::yati::source diff --git a/sphaira/include/yati/source/stream_file.hpp b/sphaira/include/yati/source/stream_file.hpp new file mode 100644 index 0000000..a6398e0 --- /dev/null +++ b/sphaira/include/yati/source/stream_file.hpp @@ -0,0 +1,22 @@ +// this is used for testing that streams work, this code isn't used in normal +// release builds as it is slower / less feature complete than normal. +#pragma once + +#include "stream.hpp" +#include "fs.hpp" +#include + +namespace sphaira::yati::source { + +struct StreamFile final : Stream { + StreamFile(FsFileSystem* fs, const fs::FsPath& path); + ~StreamFile(); + + Result ReadChunk(void* buf, s64 size, u64* bytes_read) override; + +private: + FsFile m_file{}; + s64 m_offset{}; +}; + +} // namespace sphaira::yati::source diff --git a/sphaira/include/yati/yati.hpp b/sphaira/include/yati/yati.hpp index 05c94c9..d7fdbc9 100644 --- a/sphaira/include/yati/yati.hpp +++ b/sphaira/include/yati/yati.hpp @@ -79,11 +79,6 @@ struct Config { // installs tickets only. bool ticket_only{}; - // converts personalised tickets to common tickets, allows for offline play. - // this breaks ticket signature so es needs to be patched. - // modified common tickets are patched regardless of this setting. - bool patch_ticket{}; - // flags to enable / disable install of specific types. bool skip_base{}; bool skip_patch{}; diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index 1ced627..6bde6e5 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -544,7 +544,7 @@ auto App::GetThemeIndex() -> s64 { return g_app->m_theme_index; } -auto App::GetDefaultImage(int* w, int* h) -> int { +auto App::GetDefaultImage() -> int { return g_app->m_default_image; } @@ -1392,8 +1392,33 @@ App::App(const char* argv0) { } } - // soon (tm) - // ui::bubble::Init(); + struct EventDay { + u8 day; + u8 month; + }; + + static constexpr EventDay event_days[] = { + { .day = 1, .month = 1 }, // New years + + { .day = 3, .month = 3 }, // March 3 (switch 1) + { .day = 10, .month = 5 }, // June 10 (switch 2) + { .day = 15, .month = 5 }, // June 15 + + { .day = 25, .month = 12 }, // Christmas + { .day = 26, .month = 12 }, + { .day = 27, .month = 12 }, + { .day = 28, .month = 12 }, + }; + + const auto time = std::time(nullptr); + const auto tm = std::localtime(&time); + + for (auto e : event_days) { + if (e.day == tm->tm_mday && e.month == (tm->tm_mon + 1)) { + ui::bubble::Init(); + break; + } + } App::Push(std::make_shared()); log_write("finished app constructor\n"); diff --git a/sphaira/source/ftpsrv_helper.cpp b/sphaira/source/ftpsrv_helper.cpp index 7946a46..52afbf3 100644 --- a/sphaira/source/ftpsrv_helper.cpp +++ b/sphaira/source/ftpsrv_helper.cpp @@ -12,14 +12,30 @@ #include #include +namespace sphaira::ftpsrv { namespace { +struct InstallSharedData { + std::mutex mutex; + + std::deque queued_files; + + void* user; + OnInstallStart on_start; + OnInstallWrite on_write; + OnInstallClose on_close; + + bool in_progress; + bool enabled; +}; + const char* INI_PATH = "/config/ftpsrv/config.ini"; FtpSrvConfig g_ftpsrv_config = {0}; volatile bool g_should_exit = false; bool g_is_running{false}; Thread g_thread; std::mutex g_mutex{}; +InstallSharedData g_shared_data{}; void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) { sphaira::App::NotifyFlashLed(); @@ -29,6 +45,235 @@ void ftp_progress_callback(void) { sphaira::App::NotifyFlashLed(); } +const char* SUPPORTED_EXT[] = { + ".nsp", ".xci", ".nsz", ".xcz", +}; + +struct VfsUserData { + char* path; + int valid; +}; + +// ive given up with good names. +void on_thing() { + log_write("[FTP] doing on_thing\n"); + std::scoped_lock lock{g_shared_data.mutex}; + log_write("[FTP] locked on_thing\n"); + + if (!g_shared_data.in_progress) { + if (!g_shared_data.queued_files.empty()) { + log_write("[FTP] pushing new file data\n"); + if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.user, g_shared_data.queued_files[0].c_str())) { + g_shared_data.queued_files.clear(); + } else { + log_write("[FTP] success on new file push\n"); + g_shared_data.in_progress = true; + } + } + } +} + +int vfs_install_open(void* user, const char* path, enum FtpVfsOpenMode mode) { + { + std::scoped_lock lock{g_shared_data.mutex}; + auto data = static_cast(user); + data->valid = 0; + + if (mode != FtpVfsOpenMode_WRITE) { + errno = EACCES; + return -1; + } + + if (!g_shared_data.enabled) { + errno = EACCES; + return -1; + } + + const char* ext = strrchr(path, '.'); + if (!ext) { + errno = EACCES; + return -1; + } + + bool found = false; + for (size_t i = 0; i < std::size(SUPPORTED_EXT); i++) { + if (!strcasecmp(ext, SUPPORTED_EXT[i])) { + found = true; + break; + } + } + + if (!found) { + errno = EINVAL; + return -1; + } + + // check if we already have this file queued. + auto it = std::find(g_shared_data.queued_files.cbegin(), g_shared_data.queued_files.cend(), path); + if (it != g_shared_data.queued_files.cend()) { + errno = EEXIST; + return -1; + } + + g_shared_data.queued_files.push_back(path); + data->path = strdup(path); + data->valid = true; + } + + on_thing(); + log_write("[FTP] got file: %s\n", path); + return 0; +} + +int vfs_install_read(void* user, void* buf, size_t size) { + errno = EACCES; + return -1; +} + +int vfs_install_write(void* user, const void* buf, size_t size) { + std::scoped_lock lock{g_shared_data.mutex}; + if (!g_shared_data.enabled) { + errno = EACCES; + return -1; + } + + auto data = static_cast(user); + if (!data->valid) { + errno = EACCES; + return -1; + } + + if (!g_shared_data.on_write || !g_shared_data.on_write(g_shared_data.user, buf, size)) { + errno = EIO; + return -1; + } + + return size; +} + +int vfs_install_seek(void* user, const void* buf, size_t size, size_t off) { + errno = ESPIPE; + return -1; +} + +int vfs_install_isfile_open(void* user) { + std::scoped_lock lock{g_shared_data.mutex}; + auto data = static_cast(user); + return data->valid; +} + +int vfs_install_isfile_ready(void* user) { + std::scoped_lock lock{g_shared_data.mutex}; + auto data = static_cast(user); + const auto ready = !g_shared_data.queued_files.empty() && data->path == g_shared_data.queued_files[0]; + return ready; +} + +int vfs_install_close(void* user) { + { + log_write("[FTP] closing file\n"); + std::scoped_lock lock{g_shared_data.mutex}; + auto data = static_cast(user); + if (data->valid) { + log_write("[FTP] closing valid file\n"); + + auto it = std::find(g_shared_data.queued_files.cbegin(), g_shared_data.queued_files.cend(), data->path); + if (it != g_shared_data.queued_files.cend()) { + if (it == g_shared_data.queued_files.cbegin()) { + log_write("[FTP] closing current file\n"); + if (g_shared_data.on_close) { + g_shared_data.on_close(g_shared_data.user); + } + + g_shared_data.in_progress = false; + } else { + log_write("[FTP] closing other file...\n"); + } + + g_shared_data.queued_files.erase(it); + } else { + log_write("[FTP] could not find file in queue...\n"); + } + + if (data->path) { + free(data->path); + } + + data->valid = 0; + } + + memset(data, 0, sizeof(*data)); + } + + on_thing(); + return 0; +} + +int vfs_install_opendir(void* user, const char* path) { + return 0; +} + +const char* vfs_install_readdir(void* user, void* user_entry) { + return NULL; +} + +int vfs_install_dirlstat(void* user, const void* user_entry, const char* path, struct stat* st) { + st->st_nlink = 1; + st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + return 0; +} + +int vfs_install_isdir_open(void* user) { + return 1; +} + +int vfs_install_closedir(void* user) { + return 0; +} + +int vfs_install_stat(const char* path, struct stat* st) { + st->st_nlink = 1; + st->st_mode = S_IFDIR | S_IWUSR | S_IWGRP | S_IWOTH; + return 0; +} + +int vfs_install_mkdir(const char* path) { + return -1; +} + +int vfs_install_unlink(const char* path) { + return -1; +} + +int vfs_install_rmdir(const char* path) { + return -1; +} + +int vfs_install_rename(const char* src, const char* dst) { + return -1; +} + +FtpVfs g_vfs_install = { + .open = vfs_install_open, + .read = vfs_install_read, + .write = vfs_install_write, + .seek = vfs_install_seek, + .close = vfs_install_close, + .isfile_open = vfs_install_isfile_open, + .isfile_ready = vfs_install_isfile_ready, + .opendir = vfs_install_opendir, + .readdir = vfs_install_readdir, + .dirlstat = vfs_install_dirlstat, + .closedir = vfs_install_closedir, + .isdir_open = vfs_install_isdir_open, + .stat = vfs_install_stat, + .lstat = vfs_install_stat, + .mkdir = vfs_install_mkdir, + .unlink = vfs_install_unlink, + .rmdir = vfs_install_rmdir, + .rename = vfs_install_rename, +}; + void loop(void* arg) { while (!g_should_exit) { ftpsrv_init(&g_ftpsrv_config); @@ -44,8 +289,6 @@ void loop(void* arg) { } // namespace -namespace sphaira::ftpsrv { - bool Init() { std::scoped_lock lock{g_mutex}; if (g_is_running) { @@ -84,6 +327,9 @@ bool Init() { mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH); save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH); + mount_devices = true; + g_ftpsrv_config.timeout = 0; + if (!g_ftpsrv_config.port) { return false; } @@ -93,7 +339,13 @@ bool Init() { g_ftpsrv_config.anon = true; } - vfs_nx_init(mount_devices, save_writable, mount_bis); + const VfsNxCustomPath custom = { + .name = "install", + .user = NULL, + .func = &g_vfs_install, + }; + + vfs_nx_init(&custom, mount_devices, save_writable, mount_bis); Result rc; if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*16, 0x2C, 2))) { @@ -123,6 +375,40 @@ void Exit() { fsdev_wrapUnmountAll(); } +void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) { + std::scoped_lock lock{g_shared_data.mutex}; + g_shared_data.user = user; + g_shared_data.on_start = on_start; + g_shared_data.on_write = on_write; + g_shared_data.on_close = on_close; + g_shared_data.enabled = true; +} + +void DisableInstallMode() { + std::scoped_lock lock{g_shared_data.mutex}; + g_shared_data.enabled = false; +} + +unsigned GetPort() { + std::scoped_lock lock{g_mutex}; + return g_ftpsrv_config.port; +} + +bool IsAnon() { + std::scoped_lock lock{g_mutex}; + return g_ftpsrv_config.anon; +} + +const char* GetUser() { + std::scoped_lock lock{g_mutex}; + return g_ftpsrv_config.user; +} + +const char* GetPass() { + std::scoped_lock lock{g_mutex}; + return g_ftpsrv_config.pass; +} + } // namespace sphaira::ftpsrv extern "C" { diff --git a/sphaira/source/ui/menus/filebrowser.cpp b/sphaira/source/ui/menus/filebrowser.cpp index 1a67769..5c9b1d2 100644 --- a/sphaira/source/ui/menus/filebrowser.cpp +++ b/sphaira/source/ui/menus/filebrowser.cpp @@ -186,7 +186,7 @@ auto GetRomDatabaseFromPath(std::string_view path) -> RomDatabaseIndexs { } // -auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, std::string extension, const RomDatabaseIndexs& db_indexs, const NroEntry& nro) { +auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, const RomDatabaseIndexs& db_indexs, const NroEntry& nro) { // if no db entries, use nro icon if (db_indexs.empty()) { log_write("using nro image\n"); @@ -812,8 +812,7 @@ void Menu::InstallForwarder() { return false; } log_write("got nro data\n"); - std::string file_name = GetEntry().GetInternalName(); - std::string extension = GetEntry().GetInternalExtension(); + auto file_name = assoc.use_base_name ? GetEntry().GetName() : GetEntry().GetInternalName(); if (auto pos = file_name.find_last_of('.'); pos != std::string::npos) { log_write("got filename\n"); @@ -829,7 +828,7 @@ void Menu::InstallForwarder() { config.name = nro.nacp.lang[0].name + std::string{" | "} + file_name; // config.name = file_name; config.nacp = nro.nacp; - config.icon = GetRomIcon(m_fs.get(), pbox, file_name, extension, db_indexs, nro); + config.icon = GetRomIcon(m_fs.get(), pbox, file_name, db_indexs, nro); return R_SUCCEEDED(App::Install(pbox, config)); })); @@ -942,15 +941,13 @@ auto Menu::FindFileAssocFor() -> std::vector { // only support roms in correctly named folders, sorry! const auto db_indexs = GetRomDatabaseFromPath(m_path); const auto& entry = GetEntry(); - const auto extension = entry.internal_extension.empty() ? entry.extension : entry.internal_extension; - if (extension.empty()) { + const auto extension = entry.extension; + const auto internal_extension = entry.internal_extension.empty() ? entry.extension : entry.internal_extension; + if (extension.empty() && internal_extension.empty()) { // log_write("failed to get extension for db: %s path: %s\n", database_entry.c_str(), m_path); return {}; } - // log_write("got extension for db: %s path: %s\n", database_entry.c_str(), m_path); - - std::vector out_entries; if (!db_indexs.empty()) { // if database isn't empty, then we are in a valid folder @@ -960,15 +957,14 @@ auto Menu::FindFileAssocFor() -> std::vector { // if (assoc_db == PATHS[db_idx].folder || assoc_db == PATHS[db_idx].database) { for (auto db_idx : db_indexs) { if (PATHS[db_idx].IsDatabase(assoc_db)) { - for (const auto& assoc_ext : assoc.ext) { - if (assoc_ext == extension) { - log_write("found ext: %s assoc_ext: %s assoc.ext: %s\n", assoc.path.s, assoc_ext.c_str(), extension.c_str()); - out_entries.emplace_back(assoc); - } + if (assoc.IsExtension(extension, internal_extension)) { + out_entries.emplace_back(assoc); + goto jump; } } } } + jump: } } else { // otherwise, if not in a valid folder, find an entry that doesn't @@ -979,11 +975,9 @@ auto Menu::FindFileAssocFor() -> std::vector { // to be in the correct folder, ie psx, to know what system that .iso is for. for (const auto& assoc : m_assoc_entries) { if (assoc.database.empty()) { - for (const auto& assoc_ext : assoc.ext) { - if (assoc_ext == extension) { - log_write("found ext: %s\n", assoc.path.s); - out_entries.emplace_back(assoc); - } + if (assoc.IsExtension(extension, internal_extension)) { + log_write("found ext: %s\n", assoc.path.s); + out_entries.emplace_back(assoc); } } } @@ -1040,6 +1034,10 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) { } } } + } else if (!strcmp(Key, "use_base_name")) { + if (!strcmp(Value, "true") || !strcmp(Value, "1")) { + assoc->use_base_name = true; + } } return 1; }, &assoc, full_path); diff --git a/sphaira/source/ui/menus/ftp_menu.cpp b/sphaira/source/ui/menus/ftp_menu.cpp new file mode 100644 index 0000000..e96df89 --- /dev/null +++ b/sphaira/source/ui/menus/ftp_menu.cpp @@ -0,0 +1,296 @@ +#include "ui/menus/ftp_menu.hpp" +#include "yati/yati.hpp" +#include "app.hpp" +#include "defines.hpp" +#include "log.hpp" +#include "ui/nvg_util.hpp" +#include "i18n.hpp" +#include "ftpsrv_helper.hpp" +#include + +namespace sphaira::ui::menu::ftp { +namespace { + +constexpr u64 MAX_BUFFER_SIZE = 1024*1024*32; +constexpr u64 SLEEPNS = 1000; +volatile bool IN_PUSH_THREAD{}; + +bool OnInstallStart(void* user, const char* path) { + auto menu = (Menu*)user; + log_write("[INSTALL] inside OnInstallStart()\n"); + + for (;;) { + mutexLock(&menu->m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex)); + + if (menu->m_state != State::Progress) { + break; + } + + if (menu->GetToken().stop_requested()) { + return false; + } + + svcSleepThread(1e+6); + } + + log_write("[INSTALL] OnInstallStart() got state: %u\n", (u8)menu->m_state); + + if (menu->m_source) { + log_write("[INSTALL] OnInstallStart() we have source\n"); + for (;;) { + mutexLock(&menu->m_source->m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&menu->m_source->m_mutex)); + + if (!IN_PUSH_THREAD) { + break; + } + + if (menu->GetToken().stop_requested()) { + return false; + } + + svcSleepThread(1e+6); + } + + log_write("[INSTALL] OnInstallStart() stopped polling source\n"); + } + + log_write("[INSTALL] OnInstallStart() doing make_shared\n"); + menu->m_source = std::make_shared(path, menu->GetToken()); + + mutexLock(&menu->m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex)); + menu->m_state = State::Connected; + log_write("[INSTALL] OnInstallStart() done make shared\n"); + + return true; +} + +bool OnInstallWrite(void* user, const void* buf, size_t size) { + auto menu = (Menu*)user; + + return menu->m_source->Push(buf, size); +} + +void OnInstallClose(void* user) { + auto menu = (Menu*)user; + menu->m_source->Disable(); +} + +} // namespace + +StreamFtp::StreamFtp(const fs::FsPath& path, std::stop_token token) { + m_path = path; + m_token = token; + m_buffer.reserve(MAX_BUFFER_SIZE); + m_active = true; +} + +Result StreamFtp::ReadChunk(void* buf, s64 size, u64* bytes_read) { + while (!m_token.stop_requested()) { + mutexLock(&m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + + if (m_buffer.empty()) { + if (!m_active) { + break; + } + + svcSleepThread(SLEEPNS); + } else { + size = std::min(size, m_buffer.size()); + std::memcpy(buf, m_buffer.data(), size); + m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size); + *bytes_read = size; + R_SUCCEED(); + } + } + + return 0x1; +} + +bool StreamFtp::Push(const void* buf, s64 size) { + IN_PUSH_THREAD = true; + ON_SCOPE_EXIT(IN_PUSH_THREAD = false); + + while (!m_token.stop_requested()) { + mutexLock(&m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + + if (!m_active) { + break; + } + + if (m_buffer.size() + size >= MAX_BUFFER_SIZE) { + svcSleepThread(SLEEPNS); + } else { + const auto offset = m_buffer.size(); + m_buffer.resize(offset + size); + std::memcpy(m_buffer.data() + offset, buf, size); + return true; + } + } + + return false; +} + +void StreamFtp::Disable() { + mutexLock(&m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + m_active = false; +} + +Menu::Menu() : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n} { + SetAction(Button::B, Action{"Back"_i18n, [this](){ + SetPop(); + }}); + + mutexInit(&m_mutex); + ftpsrv::InitInstallMode(this, OnInstallStart, OnInstallWrite, OnInstallClose); + + m_port = ftpsrv::GetPort(); + m_anon = ftpsrv::IsAnon(); + if (!m_anon) { + m_user = ftpsrv::GetUser(); + m_pass = ftpsrv::GetPass(); + } +} + +Menu::~Menu() { + // signal for thread to exit and wait. + ftpsrv::DisableInstallMode(); + m_stop_source.request_stop(); + + if (m_source) { + m_source->Disable(); + } + + log_write("closing data!!!!\n"); +} + +void Menu::Update(Controller* controller, TouchInfo* touch) { + MenuBase::Update(controller, touch); + + mutexLock(&m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + + switch (m_state) { + case State::None: + break; + + case State::Connected: + log_write("set to progress\n"); + m_state = State::Progress; + log_write("got connection\n"); + App::Push(std::make_shared("Installing App"_i18n, [this](auto pbox) mutable -> bool { + log_write("inside progress box\n"); + const auto rc = yati::InstallFromSource(pbox, m_source, m_source->m_path); + if (R_FAILED(rc)) { + m_source->Disable(); + return false; + } + + log_write("progress box is done\n"); + return true; + }, [this](bool result){ + mutexLock(&m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + + if (result) { + App::Notify("Ftp install success!"_i18n); + m_state = State::Done; + } else { + App::Notify("Ftp install failed!"_i18n); + m_state = State::Failed; + } + })); + break; + + case State::Progress: + case State::Done: + case State::Failed: + break; + } +} + +void Menu::Draw(NVGcontext* vg, Theme* theme) { + MenuBase::Draw(vg, theme); + + mutexLock(&m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + + if (m_ip) { + if (m_type == NifmInternetConnectionType_WiFi) { + SetSubHeading("Connection Type: WiFi | Strength: "_i18n + std::to_string(m_strength)); + } else { + SetSubHeading("Connection Type: Ethernet"_i18n); + } + } else { + SetSubHeading("Connection Type: None"_i18n); + } + + const float start_x = 80; + const float font_size = 22; + const float spacing = 33; + float start_y = 125; + float bounds[4]; + + nvgFontSize(vg, font_size); + + // note: textbounds strips spaces...todo: use nvgTextGlyphPositions() instead. + #define draw(key, ...) \ + gfx::textBounds(vg, start_x, start_y, bounds, key.c_str()); \ + gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT), key.c_str()); \ + gfx::drawTextArgs(vg, bounds[2], start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED), __VA_ARGS__); \ + start_y += spacing; + + if (m_ip) { + draw("Host:"_i18n, " %u.%u.%u.%u", m_ip&0xFF, (m_ip>>8)&0xFF, (m_ip>>16)&0xFF, (m_ip>>24)&0xFF); + draw("Port:"_i18n, " %u", m_port); + if (!m_anon) { + draw("Username:"_i18n, " %s", m_user); + draw("Password:"_i18n, " %s", m_pass); + } + + if (m_type == NifmInternetConnectionType_WiFi) { + NifmNetworkProfileData profile{}; + if (R_SUCCEEDED(nifmGetCurrentNetworkProfile(&profile))) { + const auto& settings = profile.wireless_setting_data; + std::string passphrase; + std::transform(std::cbegin(settings.passphrase), std::cend(settings.passphrase), passphrase.begin(), toascii); + draw("SSID:"_i18n, " %.*s", settings.ssid_len, settings.ssid); + draw("Passphrase:"_i18n, " %s", passphrase.c_str()); + } + } + } + + #undef draw + + switch (m_state) { + case State::None: + gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str()); + break; + + case State::Connected: + break; + + case State::Progress: + gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Transferring data..."_i18n.c_str()); + break; + + case State::Done: + gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to exit..."_i18n.c_str()); + break; + + case State::Failed: + gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to install via FTP, press B to exit..."_i18n.c_str()); + break; + } +} + +void Menu::OnFocusGained() { + MenuBase::OnFocusGained(); +} + +} // namespace sphaira::ui::menu::ftp diff --git a/sphaira/source/ui/menus/main_menu.cpp b/sphaira/source/ui/menus/main_menu.cpp index 70f674c..20cebc1 100644 --- a/sphaira/source/ui/menus/main_menu.cpp +++ b/sphaira/source/ui/menus/main_menu.cpp @@ -3,6 +3,7 @@ #include "ui/menus/themezer.hpp" #include "ui/menus/ghdl.hpp" #include "ui/menus/usb_menu.hpp" +#include "ui/menus/ftp_menu.hpp" #include "ui/menus/gc_menu.hpp" #include "ui/sidebar.hpp" @@ -321,6 +322,12 @@ MainMenu::MainMenu() { } if (App::GetApp()->m_install.Get()) { + if (App::GetFtpEnable()) { + options->Add(std::make_shared("Ftp Install"_i18n, [](){ + App::Push(std::make_shared()); + })); + } + options->Add(std::make_shared("Usb Install"_i18n, [](){ App::Push(std::make_shared()); })); @@ -384,10 +391,6 @@ MainMenu::MainMenu() { App::GetApp()->m_ticket_only.Set(enable); }, "Enabled"_i18n, "Disabled"_i18n)); - options->Add(std::make_shared("Patch ticket"_i18n, App::GetApp()->m_patch_ticket.Get(), [this](bool& enable){ - App::GetApp()->m_patch_ticket.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - options->Add(std::make_shared("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [this](bool& enable){ App::GetApp()->m_skip_base.Set(enable); }, "Enabled"_i18n, "Disabled"_i18n)); diff --git a/sphaira/source/ui/menus/menu_base.cpp b/sphaira/source/ui/menus/menu_base.cpp index 97fbaf6..4d9d8fd 100644 --- a/sphaira/source/ui/menus/menu_base.cpp +++ b/sphaira/source/ui/menus/menu_base.cpp @@ -18,17 +18,18 @@ MenuBase::~MenuBase() { void MenuBase::Update(Controller* controller, TouchInfo* touch) { Widget::Update(controller, touch); - - // update every second. - if (m_poll_timestamp.GetSeconds() >= 1) { - UpdateVars(); - } } void MenuBase::Draw(NVGcontext* vg, Theme* theme) { DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND); Widget::Draw(vg, theme); + // update every second, do this in Draw because Update() isn't called if it + // doesn't have focus. + if (m_poll_timestamp.GetSeconds() >= 1) { + UpdateVars(); + } + const float start_y = 70; const float font_size = 22; const float spacing = 30; diff --git a/sphaira/source/yati/container/nsp.cpp b/sphaira/source/yati/container/nsp.cpp index 2bc3993..4c5f663 100644 --- a/sphaira/source/yati/container/nsp.cpp +++ b/sphaira/source/yati/container/nsp.cpp @@ -24,14 +24,6 @@ struct Pfs0FileTableEntry { } // namespace -Result Nsp::Validate(source::Base* source) { - u32 magic; - u64 bytes_read; - R_TRY(source->Read(std::addressof(magic), 0, sizeof(magic), std::addressof(bytes_read))); - R_UNLESS(magic == PFS0_MAGIC, 0x1); - R_SUCCEED(); -} - Result Nsp::GetCollections(Collections& out) { u64 bytes_read; s64 off = 0; diff --git a/sphaira/source/yati/container/xci.cpp b/sphaira/source/yati/container/xci.cpp index d55ae41..16887ac 100644 --- a/sphaira/source/yati/container/xci.cpp +++ b/sphaira/source/yati/container/xci.cpp @@ -60,14 +60,6 @@ Result Hfs0GetPartition(source::Base* source, s64 off, Hfs0& out) { } // namespace -Result Xci::Validate(source::Base* source) { - u32 magic; - u64 bytes_read; - R_TRY(source->Read(std::addressof(magic), 0x100, sizeof(magic), std::addressof(bytes_read))); - R_UNLESS(magic == XCI_MAGIC, 0x1); - R_SUCCEED(); -} - Result Xci::GetCollections(Collections& out) { Hfs0 root{}; R_TRY(Hfs0GetPartition(m_source.get(), HFS0_HEADER_OFFSET, root)); diff --git a/sphaira/source/yati/nx/es.cpp b/sphaira/source/yati/nx/es.cpp index 6fb5027..4bc4650 100644 --- a/sphaira/source/yati/nx/es.cpp +++ b/sphaira/source/yati/nx/es.cpp @@ -106,13 +106,13 @@ Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys) // todo: i thought i already wrote the code for this?? // todo: patch the ticket. -Result PatchTicket(std::span ticket, const keys::Keys& keys, bool convert_personalised) { +Result PatchTicket(std::span ticket, const keys::Keys& keys) { TicketData data; R_TRY(GetTicketData(ticket, &data)); if (data.title_key_type == es::TicketTitleKeyType_Common) { // todo: verify common signature - } else if (data.title_key_type == es::TicketTitleKeyType_Personalized && convert_personalised) { + } else if (data.title_key_type == es::TicketTitleKeyType_Personalized) { } diff --git a/sphaira/source/yati/source/stream.cpp b/sphaira/source/yati/source/stream.cpp new file mode 100644 index 0000000..8e465a2 --- /dev/null +++ b/sphaira/source/yati/source/stream.cpp @@ -0,0 +1,41 @@ +#include "yati/source/stream.hpp" +#include "defines.hpp" +#include "log.hpp" + +namespace sphaira::yati::source { + +Result Stream::Read(void* _buf, s64 off, s64 size, u64* bytes_read_out) { + // streams don't allow for random access (seeking backwards). + R_UNLESS(off >= m_offset, 0x1); + + auto buf = static_cast(_buf); + *bytes_read_out = 0; + + // check if we already have some data in the buffer. + while (size) { + // while it is invalid to seek backwards, it is valid to seek forwards. + // this can be done to skip padding, skip undeeded files etc. + // to handle this, simply read the data into a buffer and discard it. + if (off > m_offset) { + const auto skip_size = off - m_offset; + std::vector temp_buf(skip_size); + u64 bytes_read; + R_TRY(ReadChunk(temp_buf.data(), temp_buf.size(), &bytes_read)); + + m_offset += bytes_read; + } else { + u64 bytes_read; + R_TRY(ReadChunk(buf, size, &bytes_read)); + + *bytes_read_out += bytes_read; + buf += bytes_read; + off += bytes_read; + m_offset += bytes_read; + size -= bytes_read; + } + } + + R_SUCCEED(); +} + +} // namespace sphaira::yati::source diff --git a/sphaira/source/yati/source/stream_file.cpp b/sphaira/source/yati/source/stream_file.cpp new file mode 100644 index 0000000..d662afa --- /dev/null +++ b/sphaira/source/yati/source/stream_file.cpp @@ -0,0 +1,23 @@ +#include "yati/source/stream_file.hpp" +#include "log.hpp" + +namespace sphaira::yati::source { + +StreamFile::StreamFile(FsFileSystem* fs, const fs::FsPath& path) { + m_open_result = fsFsOpenFile(fs, path, FsOpenMode_Read, std::addressof(m_file)); +} + +StreamFile::~StreamFile() { + if (R_SUCCEEDED(GetOpenResult())) { + fsFileClose(std::addressof(m_file)); + } +} + +Result StreamFile::ReadChunk(void* buf, s64 size, u64* bytes_read) { + R_TRY(GetOpenResult()); + const auto rc = fsFileRead(std::addressof(m_file), m_offset, buf, size, 0, bytes_read); + m_offset += *bytes_read; + return rc; +} + +} // namespace sphaira::yati::source diff --git a/sphaira/source/yati/yati.cpp b/sphaira/source/yati/yati.cpp index 060e93d..1e9a85c 100644 --- a/sphaira/source/yati/yati.cpp +++ b/sphaira/source/yati/yati.cpp @@ -1,5 +1,6 @@ #include "yati/yati.hpp" #include "yati/source/file.hpp" +#include "yati/source/stream_file.hpp" #include "yati/source/stdio.hpp" #include "yati/container/nsp.hpp" #include "yati/container/xci.hpp" @@ -36,13 +37,13 @@ struct CustomVectorAllocator { public: // https://en.cppreference.com/w/cpp/memory/new/operator_new auto allocate(std::size_t n) -> T* { - log_write("allocating ptr size: %zu\n", n); + // log_write("allocating ptr size: %zu\n", n); return new(align) T[n]; } // https://en.cppreference.com/w/cpp/memory/new/operator_delete auto deallocate(T* p, std::size_t n) noexcept -> void { - log_write("deleting ptr size: %zu\n", n); + // log_write("deleting ptr size: %zu\n", n); ::operator delete[] (p, n, align); } @@ -62,17 +63,6 @@ using PageAlignedVector = std::vector>; constexpr u32 KEYGEN_LIMIT = 0x20; -#if 0 -struct FwVersion { - u32 value; - auto relstep() const -> u8 { return (value >> 0) & 0xFFFF; } - auto micro() const -> u8 { return (value >> 16) & 0x000F; } - auto minor() const -> u8 { return (value >> 20) & 0x003F; } - auto major() const -> u8 { return (value >> 26) & 0x003F; } - auto hos() const -> u32 { return MAKEHOSVERSION(major(), minor(), micro()); } -}; -#endif - struct NcaCollection : container::CollectionEntry { // NcmContentType u8 type{}; @@ -293,6 +283,14 @@ struct Yati { Result decompressFuncInternal(ThreadData* t); Result writeFuncInternal(ThreadData* t); + Result ParseTicketsIntoCollection(std::vector& tickets, const container::Collections& collections, bool read_data); + Result GetLatestVersion(const CnmtCollection& cnmt, u32& version_out, bool& skip); + Result ShouldSkip(const CnmtCollection& cnmt, bool& skip); + Result ImportTickets(std::span tickets); + Result RemoveInstalledNcas(const CnmtCollection& cnmt); + Result RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_version_num); + + // private: ui::ProgressBox* pbox{}; std::shared_ptr source{}; @@ -362,8 +360,14 @@ HashStr hexIdToStr(auto id) { // read thread reads all data from the source, it also handles // parsing ncz headers, sections and reading ncz blocks Result Yati::readFuncInternal(ThreadData* t) { + // the main buffer which data is read into. PageAlignedVector buf; + // workaround ncz block reading ahead. if block isn't found, we usually + // would seek back to the offset, however this is not possible in stream + // mode, so we instead store the data to the temp buffer and pre-pend it. + PageAlignedVector temp_buf; buf.reserve(t->max_buffer_size); + temp_buf.reserve(t->max_buffer_size); while (t->read_offset < t->nca->size && R_SUCCEEDED(t->GetResults())) { const auto buffer_offset = t->read_offset; @@ -374,10 +378,18 @@ Result Yati::readFuncInternal(ThreadData* t) { read_size = NCZ_SECTION_OFFSET; } + s64 buf_offset = 0; + if (!temp_buf.empty()) { + buf = temp_buf; + read_size -= temp_buf.size(); + buf_offset = temp_buf.size(); + temp_buf.clear(); + } + u64 bytes_read{}; - buf.resize(read_size); - R_TRY(t->Read(buf.data(), read_size, std::addressof(bytes_read))); - auto buf_size = bytes_read; + buf.resize(buf_offset + read_size); + R_TRY(t->Read(buf.data() + buf_offset, read_size, std::addressof(bytes_read))); + auto buf_size = buf_offset + bytes_read; // read enough bytes for ncz, check magic if (t->read_offset == NCZ_SECTION_OFFSET) { @@ -394,10 +406,12 @@ Result Yati::readFuncInternal(ThreadData* t) { R_TRY(t->Read(t->ncz_sections.data(), t->ncz_sections.size() * sizeof(ncz::Section), std::addressof(bytes_read))); // check for ncz block header. - const auto read_off = t->read_offset; R_TRY(t->Read(std::addressof(t->ncz_block_header), sizeof(t->ncz_block_header), std::addressof(bytes_read))); if (t->ncz_block_header.magic != NCZ_BLOCK_MAGIC) { - t->read_offset = read_off; + // didn't find block, keep the data we just read in the temp buffer. + temp_buf.resize(sizeof(t->ncz_block_header)); + std::memcpy(temp_buf.data(), std::addressof(t->ncz_block_header), temp_buf.size()); + log_write("storing temp data of size: %zu\n", temp_buf.size()); } else { // validate block header. R_UNLESS(t->ncz_block_header.version == 0x2, Result_InvalidNczBlockVersion); @@ -783,7 +797,6 @@ Result Yati::Setup() { config.allow_downgrade = App::GetApp()->m_allow_downgrade.Get(); config.skip_if_already_installed = App::GetApp()->m_skip_if_already_installed.Get(); config.ticket_only = App::GetApp()->m_ticket_only.Get(); - config.patch_ticket = App::GetApp()->m_patch_ticket.Get(); config.skip_base = App::GetApp()->m_skip_base.Get(); config.skip_patch = App::GetApp()->m_skip_patch.Get(); config.skip_addon = App::GetApp()->m_skip_addon.Get(); @@ -891,7 +904,6 @@ Result Yati::InstallNca(std::span tickets, NcaCollection& nca) { std::memcpy(std::addressof(content_id), nca.hash, sizeof(content_id)); log_write("old id: %s new id: %s\n", hexIdToStr(nca.content_id).str, hexIdToStr(content_id).str); - log_write("doing register: %s\n", nca.name.c_str()); if (!config.skip_nca_hash_verify && !nca.modified) { if (std::memcmp(&nca.content_id, nca.hash, sizeof(nca.content_id))) { log_write("nca hash is invalid!!!!\n"); @@ -1027,11 +1039,7 @@ Result Yati::InstallControlNca(std::span tickets, const CnmtColle R_SUCCEED(); } -Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr source, const container::Collections& collections) { - auto yati = std::make_unique(pbox, source); - R_TRY(yati->Setup()); - - std::vector tickets{}; +Result Yati::ParseTicketsIntoCollection(std::vector& tickets, const container::Collections& collections, bool read_data) { for (const auto& collection : collections) { if (collection.name.ends_with(".tik")) { TikCollection entry{}; @@ -1046,17 +1054,226 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr sour entry.ticket.resize(collection.size); entry.cert.resize(cert->size); - u64 bytes_read; - R_TRY(source->Read(entry.ticket.data(), collection.offset, entry.ticket.size(), &bytes_read)); - R_TRY(source->Read(entry.cert.data(), cert->offset, entry.cert.size(), &bytes_read)); + // only supported on non-stream installs. + if (read_data) { + u64 bytes_read; + R_TRY(source->Read(entry.ticket.data(), collection.offset, entry.ticket.size(), &bytes_read)); + R_TRY(source->Read(entry.cert.data(), cert->offset, entry.cert.size(), &bytes_read)); + } + tickets.emplace_back(entry); } } + R_SUCCEED(); +} + +Result Yati::GetLatestVersion(const CnmtCollection& cnmt, u32& version_out, bool& skip) { + const auto app_id = ncm::GetAppId(cnmt.key); + + bool has_records; + R_TRY(nsIsAnyApplicationEntityInstalled(app_id, &has_records)); + + // TODO: fix this when gamecard is inserted as it will only return records + // for the gamecard... + // may have to use ncm directly to get the keys, then parse that. + version_out = cnmt.key.version; + if (has_records) { + s32 meta_count{}; + R_TRY(nsCountApplicationContentMeta(app_id, &meta_count)); + R_UNLESS(meta_count > 0, 0x1); + + std::vector records(meta_count); + s32 count; + R_TRY(ns::ListApplicationRecordContentMeta(std::addressof(ns_app), 0, app_id, records.data(), records.size(), &count)); + R_UNLESS(count == records.size(), 0x1); + + for (auto& record : records) { + log_write("found record: 0x%016lX type: %u version: %u\n", record.key.id, record.key.type, record.key.version); + log_write("cnmt record: 0x%016lX type: %u version: %u\n", cnmt.key.id, cnmt.key.type, cnmt.key.version); + + if (record.key.id == cnmt.key.id && cnmt.key.version == record.key.version && config.skip_if_already_installed) { + log_write("skipping as already installed\n"); + skip = true; + } + + // check if we are downgrading + if (cnmt.key.type == NcmContentMetaType_Patch) { + if (cnmt.key.type == record.key.type && cnmt.key.version < record.key.version && !config.allow_downgrade) { + log_write("skipping due to it being lower\n"); + skip = true; + } + } else { + version_out = std::max(version_out, record.key.version); + } + } + } + + R_SUCCEED(); +} + +Result Yati::ShouldSkip(const CnmtCollection& cnmt, bool& skip) { + // skip invalid types + if (!(cnmt.key.type & 0x80)) { + log_write("\tskipping: invalid: %u\n", cnmt.key.type); + skip = true; + } else if (config.skip_base && cnmt.key.type == NcmContentMetaType_Application) { + log_write("\tskipping: [NcmContentMetaType_Application]\n"); + skip = true; + } else if (config.skip_patch && cnmt.key.type == NcmContentMetaType_Patch) { + log_write("\tskipping: [NcmContentMetaType_Application]\n"); + skip = true; + } else if (config.skip_addon && cnmt.key.type == NcmContentMetaType_AddOnContent) { + log_write("\tskipping: [NcmContentMetaType_AddOnContent]\n"); + skip = true; + } else if (config.skip_data_patch && cnmt.key.type == NcmContentMetaType_DataPatch) { + log_write("\tskipping: [NcmContentMetaType_DataPatch]\n"); + skip = true; + } + + R_SUCCEED(); +} + +Result Yati::ImportTickets(std::span tickets) { + for (auto& ticket : tickets) { + if (ticket.required) { + if (config.skip_ticket) { + log_write("WARNING: skipping ticket install, but it's required!\n"); + } else { + log_write("patching ticket\n"); + R_TRY(es::PatchTicket(ticket.ticket, keys)); + log_write("installing ticket\n"); + R_TRY(es::ImportTicket(std::addressof(es), ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size())); + ticket.required = false; + } + } + } + + R_SUCCEED(); +} + +Result Yati::RemoveInstalledNcas(const CnmtCollection& cnmt) { + const auto app_id = ncm::GetAppId(cnmt.key); + + // remove current entries (if any). + s32 db_list_total; + s32 db_list_count; + u64 id_min = cnmt.key.id; + u64 id_max = cnmt.key.id; + std::vector keys(1); + + // if installing a patch, remove all previously installed patches. + if (cnmt.key.type == NcmContentMetaType_Patch) { + id_min = 0; + id_max = UINT64_MAX; + } + + log_write("listing keys\n"); + for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) { + auto& cs = ncm_cs[i]; + auto& db = ncm_db[i]; + + std::vector keys(1); + R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), static_cast(cnmt.key.type), app_id, id_min, id_max, NcmContentInstallType_Full)); + + if (db_list_total != keys.size()) { + keys.resize(db_list_total); + if (keys.size()) { + R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), static_cast(cnmt.key.type), app_id, id_min, id_max, NcmContentInstallType_Full)); + } + } + + for (auto& key : keys) { + log_write("found key: 0x%016lX type: %u version: %u\n", key.id, key.type, key.version); + NcmContentMetaHeader header; + u64 out_size; + log_write("trying to get from db\n"); + R_TRY(ncmContentMetaDatabaseGet(std::addressof(db), std::addressof(key), std::addressof(out_size), std::addressof(header), sizeof(header))); + R_UNLESS(out_size == sizeof(header), Result_NcmDbCorruptHeader); + log_write("trying to list infos\n"); + + std::vector infos(header.content_count); + s32 content_info_out; + R_TRY(ncmContentMetaDatabaseListContentInfo(std::addressof(db), std::addressof(content_info_out), infos.data(), infos.size(), std::addressof(key), 0)); + R_UNLESS(content_info_out == infos.size(), Result_NcmDbCorruptInfos); + log_write("size matches\n"); + + for (auto& info : infos) { + R_TRY(ncm::Delete(std::addressof(cs), std::addressof(info.content_id))); + } + + log_write("trying to remove it\n"); + R_TRY(ncmContentMetaDatabaseRemove(std::addressof(db), std::addressof(key))); + R_TRY(ncmContentMetaDatabaseCommit(std::addressof(db))); + log_write("all done with this key\n\n"); + } + } + + log_write("done with keys\n"); + R_SUCCEED(); +} + +Result Yati::RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_version_num) { + const auto app_id = ncm::GetAppId(cnmt.key); + + // register all nca's + log_write("registering cnmt nca\n"); + R_TRY(ncm::Register(std::addressof(cs), std::addressof(cnmt.content_id), std::addressof(cnmt.placeholder_id))); + log_write("registered cnmt nca\n"); + + for (auto& nca : cnmt.ncas) { + if (nca.type != NcmContentType_DeltaFragment) { + log_write("registering nca: %s\n", nca.name.c_str()); + R_TRY(ncm::Register(std::addressof(cs), std::addressof(nca.content_id), std::addressof(nca.placeholder_id))); + log_write("registered nca: %s\n", nca.name.c_str()); + } + } + + log_write("register'd all ncas\n"); + + // build ncm meta and push to the database. + BufHelper buf{}; + buf.write(std::addressof(cnmt.header), sizeof(cnmt.header)); + buf.write(cnmt.extended_header.data(), cnmt.extended_header.size()); + buf.write(std::addressof(cnmt.content_info), sizeof(cnmt.content_info)); + + for (auto& info : cnmt.infos) { + buf.write(std::addressof(info.info), sizeof(info.info)); + } + + pbox->NewTransfer("Updating ncm databse"_i18n); + R_TRY(ncmContentMetaDatabaseSet(std::addressof(db), std::addressof(cnmt.key), buf.buf.data(), buf.tell())); + R_TRY(ncmContentMetaDatabaseCommit(std::addressof(db))); + + // push record. + ncm::ContentStorageRecord content_storage_record{}; + content_storage_record.key = cnmt.key; + content_storage_record.storage_id = storage_id; + pbox->NewTransfer("Pushing application record"_i18n); + + R_TRY(ns::PushApplicationRecord(std::addressof(ns_app), app_id, std::addressof(content_storage_record), 1)); + if (hosversionAtLeast(6,0,0)) { + R_TRY(avmInitialize()); + ON_SCOPE_EXIT(avmExit()); + + R_TRY(avmPushLaunchVersion(app_id, latest_version_num)); + } + log_write("pushed\n"); + + R_SUCCEED(); +} + +Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr source, const container::Collections& collections) { + auto yati = std::make_unique(pbox, source); + R_TRY(yati->Setup()); + + std::vector tickets{}; + R_TRY(yati->ParseTicketsIntoCollection(tickets, collections, true)); + std::vector cnmts{}; for (const auto& collection : collections) { log_write("found collection: %s\n", collection.name.c_str()); - if (collection.name.ends_with(".cnmt.nca")) { + if (collection.name.ends_with(".cnmt.nca") || collection.name.ends_with(".cnmt.ncz")) { auto& cnmt = cnmts.emplace_back(NcaCollection{collection}); cnmt.type = NcmContentType_Meta; } @@ -1072,63 +1289,10 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr sour R_TRY(yati->InstallCnmtNca(tickets, cnmt, collections)); + u32 latest_version_num; bool skip = false; - const auto app_id = ncm::GetAppId(cnmt.key); - bool has_records; - R_TRY(nsIsAnyApplicationEntityInstalled(app_id, &has_records)); - - // TODO: fix this when gamecard is inserted as it will only return records - // for the gamecard... - // may have to use ncm directly to get the keys, then parse that. - u32 latest_version_num = cnmt.key.version; - if (has_records) { - s32 meta_count{}; - R_TRY(nsCountApplicationContentMeta(app_id, &meta_count)); - R_UNLESS(meta_count > 0, 0x1); - - std::vector records(meta_count); - s32 count; - R_TRY(ns::ListApplicationRecordContentMeta(std::addressof(yati->ns_app), 0, app_id, records.data(), records.size(), &count)); - R_UNLESS(count == records.size(), 0x1); - - for (auto& record : records) { - log_write("found record: 0x%016lX type: %u version: %u\n", record.key.id, record.key.type, record.key.version); - log_write("cnmt record: 0x%016lX type: %u version: %u\n", cnmt.key.id, cnmt.key.type, cnmt.key.version); - - if (record.key.id == cnmt.key.id && cnmt.key.version == record.key.version && yati->config.skip_if_already_installed) { - log_write("skipping as already installed\n"); - skip = true; - } - - // check if we are downgrading - if (cnmt.key.type == NcmContentMetaType_Patch) { - if (cnmt.key.type == record.key.type && cnmt.key.version < record.key.version && !yati->config.allow_downgrade) { - log_write("skipping due to it being lower\n"); - skip = true; - } - } else { - latest_version_num = std::max(latest_version_num, record.key.version); - } - } - } - - // skip invalid types - if (!(cnmt.key.type & 0x80)) { - log_write("\tskipping: invalid: %u\n", cnmt.key.type); - skip = true; - } else if (yati->config.skip_base && cnmt.key.type == NcmContentMetaType_Application) { - log_write("\tskipping: [NcmContentMetaType_Application]\n"); - skip = true; - } else if (yati->config.skip_patch && cnmt.key.type == NcmContentMetaType_Patch) { - log_write("\tskipping: [NcmContentMetaType_Application]\n"); - skip = true; - } else if (yati->config.skip_addon && cnmt.key.type == NcmContentMetaType_AddOnContent) { - log_write("\tskipping: [NcmContentMetaType_AddOnContent]\n"); - skip = true; - } else if (yati->config.skip_data_patch && cnmt.key.type == NcmContentMetaType_DataPatch) { - log_write("\tskipping: [NcmContentMetaType_DataPatch]\n"); - skip = true; - } + R_TRY(yati->GetLatestVersion(cnmt, latest_version_num, skip)); + R_TRY(yati->ShouldSkip(cnmt, skip)); if (skip) { log_write("skipping install!\n"); @@ -1145,125 +1309,104 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr sour } } - // log_write("exiting early :)\n"); - // return 0; + R_TRY(yati->ImportTickets(tickets)); + R_TRY(yati->RemoveInstalledNcas(cnmt)); + R_TRY(yati->RegisterNcasAndPushRecord(cnmt, latest_version_num)); + } - for (auto& ticket : tickets) { - if (ticket.required) { - if (yati->config.skip_ticket) { - log_write("WARNING: skipping ticket install, but it's required!\n"); - } else { - log_write("patching ticket\n"); - if (yati->config.patch_ticket) { - R_TRY(es::PatchTicket(ticket.ticket, yati->keys, false)); - } - log_write("installing ticket\n"); - R_TRY(es::ImportTicket(std::addressof(yati->es), ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size())); - ticket.required = false; - } + log_write("success!\n"); + R_SUCCEED(); +} + +Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr source, container::Collections collections) { + auto yati = std::make_unique(pbox, source); + R_TRY(yati->Setup()); + + // not supported with stream installs (yet). + yati->config.convert_to_standard_crypto = false; + yati->config.lower_master_key = false; + + std::vector ncas{}; + std::vector cnmts{}; + std::vector tickets{}; + + ON_SCOPE_EXIT( + for (const auto& cnmt : cnmts) { + ncmContentStorageDeletePlaceHolder(std::addressof(yati->cs), std::addressof(cnmt.placeholder_id)); + } + + for (const auto& nca : ncas) { + ncmContentStorageDeletePlaceHolder(std::addressof(yati->cs), std::addressof(nca.placeholder_id)); + } + ); + + // fill ticket entries, the data will be filled later on. + R_TRY(yati->ParseTicketsIntoCollection(tickets, collections, false)); + + // sort based on lowest offset. + const auto sorter = [](const container::CollectionEntry& lhs, const container::CollectionEntry& rhs) -> bool { + return lhs.offset < rhs.offset; + }; + + std::sort(collections.begin(), collections.end(), sorter); + + for (const auto& collection : collections) { + if (collection.name.ends_with(".nca") || collection.name.ends_with(".ncz")) { + auto& nca = ncas.emplace_back(NcaCollection{collection}); + if (collection.name.ends_with(".cnmt.nca") || collection.name.ends_with(".cnmt.ncz")) { + auto& cnmt = cnmts.emplace_back(nca); + cnmt.type = NcmContentType_Meta; + R_TRY(yati->InstallCnmtNca(tickets, cnmt, collections)); + } else { + R_TRY(yati->InstallNca(tickets, nca)); + } + } else if (collection.name.ends_with(".tik") || collection.name.ends_with(".cert")) { + FsRightsId rights_id{}; + keys::parse_hex_key(rights_id.c, collection.name.c_str()); + const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert"; + + auto entry = std::find_if(tickets.begin(), tickets.end(), [rights_id](auto& e){ + return !std::memcmp(&rights_id, &e.rights_id, sizeof(rights_id)); + }); + + // this will never fail...but just in case. + R_UNLESS(entry != tickets.end(), Result_CertNotFound); + + u64 bytes_read; + if (collection.name.ends_with(".tik")) { + R_TRY(source->Read(entry->ticket.data(), collection.offset, entry->ticket.size(), &bytes_read)); + } else { + R_TRY(source->Read(entry->cert.data(), collection.offset, entry->cert.size(), &bytes_read)); } } + } - log_write("listing keys\n"); + for (auto& cnmt : cnmts) { + // copy nca structs into cnmt. + for (auto& cnmt_nca : cnmt.ncas) { + auto it = std::find_if(ncas.cbegin(), ncas.cend(), [cnmt_nca](auto& e){ + return e.name == cnmt_nca.name; + }); - // remove current entries (if any). - s32 db_list_total; - s32 db_list_count; - u64 id_min = cnmt.key.id; - u64 id_max = cnmt.key.id; - std::vector keys(1); - - // if installing a patch, remove all previously installed patches. - if (cnmt.key.type == NcmContentMetaType_Patch) { - id_min = 0; - id_max = UINT64_MAX; + R_UNLESS(it != ncas.cend(), Result_NczSectionNotFound); + const auto type = cnmt_nca.type; + cnmt_nca = *it; + cnmt_nca.type = type; } - for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) { - auto& cs = yati->ncm_cs[i]; - auto& db = yati->ncm_db[i]; + u32 latest_version_num; + bool skip = false; + R_TRY(yati->GetLatestVersion(cnmt, latest_version_num, skip)); + R_TRY(yati->ShouldSkip(cnmt, skip)); - std::vector keys(1); - R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), static_cast(cnmt.key.type), app_id, id_min, id_max, NcmContentInstallType_Full)); - - if (db_list_total != keys.size()) { - keys.resize(db_list_total); - if (keys.size()) { - R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), static_cast(cnmt.key.type), app_id, id_min, id_max, NcmContentInstallType_Full)); - } - } - - for (auto& key : keys) { - log_write("found key: 0x%016lX type: %u version: %u\n", key.id, key.type, key.version); - NcmContentMetaHeader header; - u64 out_size; - log_write("trying to get from db\n"); - R_TRY(ncmContentMetaDatabaseGet(std::addressof(db), std::addressof(key), std::addressof(out_size), std::addressof(header), sizeof(header))); - R_UNLESS(out_size == sizeof(header), Result_NcmDbCorruptHeader); - log_write("trying to list infos\n"); - - std::vector infos(header.content_count); - s32 content_info_out; - R_TRY(ncmContentMetaDatabaseListContentInfo(std::addressof(db), std::addressof(content_info_out), infos.data(), infos.size(), std::addressof(key), 0)); - R_UNLESS(content_info_out == infos.size(), Result_NcmDbCorruptInfos); - log_write("size matches\n"); - - for (auto& info : infos) { - R_TRY(ncm::Delete(std::addressof(cs), std::addressof(info.content_id))); - } - - log_write("trying to remove it\n"); - R_TRY(ncmContentMetaDatabaseRemove(std::addressof(db), std::addressof(key))); - R_TRY(ncmContentMetaDatabaseCommit(std::addressof(db))); - log_write("all done with this key\n\n"); - } + if (skip) { + log_write("skipping install!\n"); + continue; } - log_write("done with keys\n"); - - // register all nca's - log_write("registering cnmt nca\n"); - R_TRY(ncm::Register(std::addressof(yati->cs), std::addressof(cnmt.content_id), std::addressof(cnmt.placeholder_id))); - log_write("registered cnmt nca\n"); - - for (auto& nca : cnmt.ncas) { - log_write("registering nca: %s\n", nca.name.c_str()); - R_TRY(ncm::Register(std::addressof(yati->cs), std::addressof(nca.content_id), std::addressof(nca.placeholder_id))); - log_write("registered nca: %s\n", nca.name.c_str()); - } - - log_write("register'd all ncas\n"); - - { - BufHelper buf{}; - buf.write(std::addressof(cnmt.header), sizeof(cnmt.header)); - buf.write(cnmt.extended_header.data(), cnmt.extended_header.size()); - buf.write(std::addressof(cnmt.content_info), sizeof(cnmt.content_info)); - - for (auto& info : cnmt.infos) { - buf.write(std::addressof(info.info), sizeof(info.info)); - } - - pbox->NewTransfer("Updating ncm databse"_i18n); - R_TRY(ncmContentMetaDatabaseSet(std::addressof(yati->db), std::addressof(cnmt.key), buf.buf.data(), buf.tell())); - R_TRY(ncmContentMetaDatabaseCommit(std::addressof(yati->db))); - } - - { - ncm::ContentStorageRecord content_storage_record{}; - content_storage_record.key = cnmt.key; - content_storage_record.storage_id = yati->storage_id; - pbox->NewTransfer("Pushing application record"_i18n); - - R_TRY(ns::PushApplicationRecord(std::addressof(yati->ns_app), app_id, std::addressof(content_storage_record), 1)); - if (hosversionAtLeast(6,0,0)) { - R_TRY(avmInitialize()); - ON_SCOPE_EXIT(avmExit()); - - R_TRY(avmPushLaunchVersion(app_id, latest_version_num)); - } - log_write("pushed\n"); - } + R_TRY(yati->ImportTickets(tickets)); + R_TRY(yati->RemoveInstalledNcas(cnmt)); + R_TRY(yati->RegisterNcasAndPushRecord(cnmt, latest_version_num)); } log_write("success!\n"); @@ -1274,6 +1417,7 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr sour Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path) { return InstallFromSource(pbox, std::make_shared(fs, path), path); + // return InstallFromSource(pbox, std::make_shared(fs, path), path); } Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path) { @@ -1281,16 +1425,16 @@ Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path) { } Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr source, const fs::FsPath& path) { - if (R_SUCCEEDED(container::Nsp::Validate(source.get()))) { - log_write("found nsp\n"); + const auto ext = std::strrchr(path.s, '.'); + R_UNLESS(ext, Result_ContainerNotFound); + + if (!strcasecmp(ext, ".nsp") || !strcasecmp(ext, ".nsz")) { return InstallFromContainer(pbox, std::make_unique(source)); - } else if (R_SUCCEEDED(container::Xci::Validate(source.get()))) { - log_write("found xci\n"); + } else if (!strcasecmp(ext, ".xci") || !strcasecmp(ext, ".xcz")) { return InstallFromContainer(pbox, std::make_unique(source)); - } else { - log_write("found unknown container\n"); - R_THROW(Result_ContainerNotFound); } + + R_THROW(Result_ContainerNotFound); } Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr container) { @@ -1300,7 +1444,11 @@ Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr source, const container::Collections& collections) { - return InstallInternal(pbox, source, collections); + if (source->IsStream()) { + return InstallInternalStream(pbox, source, collections); + } else { + return InstallInternal(pbox, source, collections); + } } } // namespace sphaira::yati