From 3c504cc85d1e795f52cddf4d0219568d36484df8 Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Sun, 21 Sep 2025 03:51:13 +0100 Subject: [PATCH] devoptab: add mounts (wrapper around all mounts, exposed via MTP/FTP). lots of fixes (see below). - updated libhaze to 81154c1. - increase ftpsrv stack size as it would crash when modifying custom mounts. - fix warning for unused log data in haze. - fix eof read for nsp/xci source by instead returning 0 for bytes read, rather than error. - add support for lstat the root of a mount. - handle zero size reads when reading games via devoptab. --- sphaira/CMakeLists.txt | 3 +- sphaira/include/defines.hpp | 43 ++++ sphaira/include/utils/devoptab.hpp | 1 + sphaira/source/app.cpp | 5 + sphaira/source/ftpsrv_helper.cpp | 22 +- sphaira/source/haze_helper.cpp | 8 +- sphaira/source/ui/menus/game_menu.cpp | 6 + sphaira/source/ui/menus/gc_menu.cpp | 6 + sphaira/source/utils/devoptab_common.cpp | 34 +-- sphaira/source/utils/devoptab_game.cpp | 38 +-- sphaira/source/utils/devoptab_mounts.cpp | 311 +++++++++++++++++++++++ 11 files changed, 427 insertions(+), 50 deletions(-) create mode 100644 sphaira/source/utils/devoptab_mounts.cpp diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index cf9dedd..dd78280 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -133,6 +133,7 @@ add_executable(sphaira source/utils/devoptab_vfs.cpp source/utils/devoptab_fatfs.cpp source/utils/devoptab_game.cpp + source/utils/devoptab_mounts.cpp source/utils/devoptab.cpp source/usb/base.cpp @@ -399,7 +400,7 @@ endif() if (ENABLE_LIBHAZE) FetchContent_Declare(libhaze GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git - GIT_TAG 6e24502 + GIT_TAG 81154c1 ) FetchContent_MakeAvailable(libhaze) diff --git a/sphaira/include/defines.hpp b/sphaira/include/defines.hpp index a8ebe5f..b8627f7 100644 --- a/sphaira/include/defines.hpp +++ b/sphaira/include/defines.hpp @@ -882,9 +882,52 @@ private: Mutex* const m_mutex; }; +struct ScopedRMutex { + ScopedRMutex(RMutex* _mutex) : mutex{_mutex} { + rmutexLock(mutex); + } + + ~ScopedRMutex() { + rmutexUnlock(mutex); + } + + ScopedRMutex(const ScopedRMutex&) = delete; + void operator=(const ScopedRMutex&) = delete; + +private: + RMutex* const mutex; +}; + +struct ScopedRwLock { + ScopedRwLock(RwLock* _lock, bool _write) : lock{_lock}, write{_write} { + if (write) { + rwlockWriteLock(lock); + } else { + rwlockReadLock(lock); + } + } + + ~ScopedRwLock() { + if (write) { + rwlockWriteUnlock(lock); + } else { + rwlockReadUnlock(lock); + } + } + + ScopedRwLock(const ScopedRwLock&) = delete; + void operator=(const ScopedRwLock&) = delete; + +private: + RwLock* const lock; + bool const write; +}; + // #define ON_SCOPE_EXIT(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { _f; }}; #define ON_SCOPE_EXIT(_f) ScopeGuard ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { _f; }}; #define SCOPED_MUTEX(_m) ScopedMutex ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){_m} +#define SCOPED_RMUTEX(_m) ScopedRMutex ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){_m} +#define SCOPED_RWLOCK(_m, _write) ScopedRwLock ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){_m, _write} // #define ON_SCOPE_FAIL(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_FAILED(rc)) { _f; } }}; // #define ON_SCOPE_SUCCESS(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_SUCCEEDED(rc)) { _f; } }}; diff --git a/sphaira/include/utils/devoptab.hpp b/sphaira/include/utils/devoptab.hpp index 30110e0..6bc85f5 100644 --- a/sphaira/include/utils/devoptab.hpp +++ b/sphaira/include/utils/devoptab.hpp @@ -28,6 +28,7 @@ Result MountNfsAll(); Result MountSmb2All(); Result MountFatfsAll(); Result MountGameAll(); +Result MountInternalMounts(); Result GetNetworkDevices(location::StdioEntries& out); void UmountAllNeworkDevices(); diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index a9af9fb..13a886c 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -1731,6 +1731,11 @@ App::App(const char* argv0) { devoptab::MountFatfsAll(); } + { + SCOPED_TIMESTAMP("mounts init"); + devoptab::MountInternalMounts(); + } + { SCOPED_TIMESTAMP("timestamp init"); // ini_putl(GetExePath(), "timestamp", m_start_timestamp, App::PLAYLOG_PATH); diff --git a/sphaira/source/ftpsrv_helper.cpp b/sphaira/source/ftpsrv_helper.cpp index 58a8d5b..a3e606f 100644 --- a/sphaira/source/ftpsrv_helper.cpp +++ b/sphaira/source/ftpsrv_helper.cpp @@ -39,11 +39,11 @@ Mutex g_mutex{}; void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) { log_write("[FTPSRV] %s\n", msg); - sphaira::App::NotifyFlashLed(); + App::NotifyFlashLed(); } void ftp_progress_callback(void) { - sphaira::App::NotifyFlashLed(); + App::NotifyFlashLed(); } InstallSharedData g_shared_data{}; @@ -389,6 +389,17 @@ const char* vfs_stdio_readdir(void* user, void* user_entry) { } int vfs_stdio_dirlstat(void* user, const void* user_entry, const char* _path, struct stat* st) { + // could probably be optimised to th below, but we won't know its r/w perms. + #if 0 + auto entry = static_cast(user_entry); + if (entry->buf->d_type == DT_DIR) { + st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + st->st_nlink = 1; + return 0; + } + #else + #endif + const auto path = vfs_stdio_fix_path(_path); return lstat(path, st); } @@ -512,6 +523,11 @@ void loop(void* arg) { .user = NULL, .func = &g_vfs_stdio, }, + { + .name = "mounts", + .user = NULL, + .func = &g_vfs_stdio, + }, { .name = "install", .user = NULL, @@ -559,7 +575,7 @@ bool Init() { // or load everything in the init thread. Result rc; - if (R_FAILED(rc = utils::CreateThread(&g_thread, loop, nullptr, 1024*16))) { + if (R_FAILED(rc = utils::CreateThread(&g_thread, loop, nullptr))) { log_write("[FTP] failed to create nxlink thread: 0x%X\n", rc); return false; } diff --git a/sphaira/source/haze_helper.cpp b/sphaira/source/haze_helper.cpp index 72dc82c..3f53979 100644 --- a/sphaira/source/haze_helper.cpp +++ b/sphaira/source/haze_helper.cpp @@ -65,11 +65,6 @@ struct FsProxyBase : haze::FileSystemProxyImpl { fs::FsPath buf; const auto len = std::strlen(GetName()); - // if (!base || !base[0]) { - // std::strcpy(buf, path); - // return buf; - // } - if (len && !strncasecmp(path, GetName(), len)) { std::snprintf(buf, sizeof(buf), "%s/%s", base, path + len); } else { @@ -636,9 +631,9 @@ struct FsInstallProxy final : FsProxyVfs { haze::FsEntries g_fs_entries{}; void haze_callback(const haze::CallbackData *data) { + #if 0 auto& e = *data; - #if 0 switch (e.type) { case haze::CallbackType_OpenSession: log_write("[LIBHAZE] Opening Session\n"); break; case haze::CallbackType_CloseSession: log_write("[LIBHAZE] Closing Session\n"); break; @@ -677,6 +672,7 @@ bool Init() { g_fs_entries.emplace_back(std::make_shared(std::make_unique(), "", "microSD card")); g_fs_entries.emplace_back(std::make_shared(std::make_unique(FsImageDirectoryId_Sd), "Album", "Album (Image SD)")); g_fs_entries.emplace_back(std::make_shared(std::make_unique(true, "games:/"), "Games", "Games")); + g_fs_entries.emplace_back(std::make_shared(std::make_unique(true, "mounts:/"), "Mounts", "Mounts")); g_fs_entries.emplace_back(std::make_shared("DevNull", "DevNull (Speed Test)")); g_fs_entries.emplace_back(std::make_shared("install", "Install (NSP, XCI, NSZ, XCZ)")); diff --git a/sphaira/source/ui/menus/game_menu.cpp b/sphaira/source/ui/menus/game_menu.cpp index 3657aa2..0a013da 100644 --- a/sphaira/source/ui/menus/game_menu.cpp +++ b/sphaira/source/ui/menus/game_menu.cpp @@ -234,6 +234,12 @@ Result CreateSave(u64 app_id, AccountUid uid) { } // namespace Result NspEntry::Read(void* buf, s64 off, s64 size, u64* bytes_read) { + if (off == nsp_size) { + log_write("[NspEntry::Read] read at eof...\n"); + *bytes_read = 0; + R_SUCCEED(); + } + if (off < nsp_data.size()) { *bytes_read = size = ClipSize(off, size, nsp_data.size()); std::memcpy(buf, nsp_data.data() + off, size); diff --git a/sphaira/source/ui/menus/gc_menu.cpp b/sphaira/source/ui/menus/gc_menu.cpp index 2cf4e1c..c10554a 100644 --- a/sphaira/source/ui/menus/gc_menu.cpp +++ b/sphaira/source/ui/menus/gc_menu.cpp @@ -235,6 +235,12 @@ struct XciSource final : dump::BaseSource { int icon{}; Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override { + if (off == xci_size) { + log_write("[XciSource::Read] read at eof...\n"); + *bytes_read = 0; + R_SUCCEED(); + } + if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI)) || path.ends_with(GetDumpTypeStr(DumpFileType_XCZ))) { size = ClipSize(off, size, xci_size); *bytes_read = size; diff --git a/sphaira/source/utils/devoptab_common.cpp b/sphaira/source/utils/devoptab_common.cpp index a00b3f4..e2fe0c1 100644 --- a/sphaira/source/utils/devoptab_common.cpp +++ b/sphaira/source/utils/devoptab_common.cpp @@ -47,30 +47,6 @@ const char* curl_url_strerror_wrap(CURLUcode code) { } } -struct ScopedRwLock { - ScopedRwLock(RwLock* _lock, bool _write) : lock{_lock}, write{_write} { - if (write) { - rwlockWriteLock(lock); - } else { - rwlockReadLock(lock); - } - } - - ~ScopedRwLock() { - if (write) { - rwlockWriteUnlock(lock); - } else { - rwlockReadUnlock(lock); - } - } - -private: - RwLock* const lock; - bool const write; -}; - -#define SCOPED_RWLOCK(_m, _write) ScopedRwLock ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){_m, _write} - struct Device { std::unique_ptr mount_device; size_t file_size; @@ -408,6 +384,14 @@ int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { SCOPED_RWLOCK(&g_rwlock, false); SCOPED_MUTEX(&device->mutex); + // special case: root of the device. + const auto dilem = std::strchr(_path, ':'); + if (dilem && (dilem > _path) && (dilem[1] == '\0' || (dilem[1] == '/' && dilem[2] == '\0'))) { + st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + st->st_nlink = 1; + return r->_errno = 0; + } + char path[PATH_MAX]{}; if (!device->mount_device->fix_path(_path, path)) { return set_errno(r, ENOENT); @@ -494,7 +478,7 @@ int devoptab_utimes(struct _reent *r, const char *_path, const struct timeval ti SCOPED_MUTEX(&device->mutex); if (!times) { - log_write("[NFS] devoptab_utimes() times is null\n"); + log_write("[DEVOPTAB] devoptab_utimes() times is null\n"); return set_errno(r, EINVAL); } diff --git a/sphaira/source/utils/devoptab_game.cpp b/sphaira/source/utils/devoptab_game.cpp index 3ba7af7..f1bfdd3 100644 --- a/sphaira/source/utils/devoptab_game.cpp +++ b/sphaira/source/utils/devoptab_game.cpp @@ -299,9 +299,13 @@ ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) { const auto& nsp = file->nsp; len = std::min(len, nsp->nsp_size - file->off); + if (!len) { + return 0; + } u64 bytes_read; if (R_FAILED(nsp->Read(ptr, file->off, len, &bytes_read))) { + log_write("[GAME] failed to read from nsp %s off: %zu len: %zu size: %zu\n", nsp->path.s, file->off, len, nsp->nsp_size); return -EIO; } @@ -398,28 +402,32 @@ int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) { filestat->st_nlink = 1; filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; std::strcpy(filename, entry.name.c_str()); + dir->index++; } else { auto& entry = dir->entry; - if (dir->index >= entry->contents.size()) { - log_write("[GAME] dirnext: no more entries\n"); - return -ENOENT; - } - - const auto& content = entry->contents[dir->index]; - if (!content.nsp) { - if (!FindNspFromEntry(*entry, content.status.application_id)) { - log_write("[GAME] failed to find nsp for content id: %016lx\n", content.status.application_id); + do { + if (dir->index >= entry->contents.size()) { + log_write("[GAME] dirnext: no more entries\n"); return -ENOENT; } - } - filestat->st_nlink = 1; - filestat->st_size = content.nsp->nsp_size; - filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; - std::snprintf(filename, NAME_MAX, "%s", content.nsp->path.s); + const auto& content = entry->contents[dir->index]; + if (!content.nsp) { + if (!FindNspFromEntry(*entry, content.status.application_id)) { + log_write("[GAME] failed to find nsp for content id: %016lx\n", content.status.application_id); + continue; + } + } + + filestat->st_nlink = 1; + filestat->st_size = content.nsp->nsp_size; + filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + std::snprintf(filename, NAME_MAX, "%s", content.nsp->path.s); + dir->index++; + break; + } while (dir->index++); } - dir->index++; return 0; } diff --git a/sphaira/source/utils/devoptab_mounts.cpp b/sphaira/source/utils/devoptab_mounts.cpp new file mode 100644 index 0000000..9fe2793 --- /dev/null +++ b/sphaira/source/utils/devoptab_mounts.cpp @@ -0,0 +1,311 @@ + +#include "utils/devoptab.hpp" +#include "utils/devoptab_common.hpp" +#include "defines.hpp" +#include "log.hpp" +#include "location.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sphaira::devoptab { +namespace { + +struct File { + int fd; +}; + +struct Dir { + DIR* dir; + location::StdioEntries* entries; + u32 index; +}; + +struct Device final : common::MountDevice { + Device(const common::MountConfig& _config) + : MountDevice{_config} { + + } + +private: + bool Mount() override { return true; } + int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override; + int devoptab_close(void *fd) override; + ssize_t devoptab_read(void *fd, char *ptr, size_t len) override; + ssize_t devoptab_seek(void *fd, off_t pos, int dir) override; + int devoptab_fstat(void *fd, struct stat *st) override; + int devoptab_unlink(const char *path) override; + int devoptab_rename(const char *oldName, const char *newName) override; + int devoptab_mkdir(const char *path, int mode) override; + int devoptab_rmdir(const char *path) override; + int devoptab_diropen(void* fd, const char *path) override; + int devoptab_dirreset(void* fd) override; + int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override; + int devoptab_dirclose(void* fd) override; + int devoptab_lstat(const char *path, struct stat *st) override; + int devoptab_ftruncate(void *fd, off_t len) override; + int devoptab_statvfs(const char *path, struct statvfs *buf) override; + int devoptab_fsync(void *fd) override; + int devoptab_utimes(const char *path, const struct timeval times[2]) override; +}; + +// converts "/[SMB] pi:/folder/file.txt" to "pi:" +auto FixPath(const char* path) -> std::pair { + while (*path == '/') { + path++; + } + + std::string_view mount_name = path; + const auto dilem = mount_name.find_first_of(':'); + if (dilem == std::string_view::npos) { + return {path, {}}; + } + mount_name = mount_name.substr(0, dilem + 1); + + fs::FsPath fixed_path = path; + if (fixed_path.ends_with(":")) { + fixed_path += '/'; + } + + log_write("[MOUNTS] FixPath: %s -> %s, mount: %.*s\n", path, fixed_path.s, (int)mount_name.size(), mount_name.data()); + return {fixed_path, mount_name}; +} + +int Device::devoptab_open(void *fileStruct, const char *_path, int flags, int mode) { + auto file = static_cast(fileStruct); + + const auto [path, mount_name] = FixPath(_path); + if (mount_name.empty()) { + log_write("[MOUNTS] devoptab_open: invalid path: %s\n", _path); + return -ENOENT; + } + + file->fd = open(path, flags, mode); + if (file->fd < 0) { + log_write("[MOUNTS] devoptab_open: failed to open %s: %s\n", path.s, std::strerror(errno)); + return -errno; + } + + return 0; +} + +int Device::devoptab_close(void *fd) { + auto file = static_cast(fd); + std::memset(file, 0, sizeof(*file)); + + return 0; +} + +ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) { + auto file = static_cast(fd); + return read(file->fd, ptr, len); +} + +ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) { + auto file = static_cast(fd); + return lseek(file->fd, pos, dir); +} + +int Device::devoptab_fstat(void *fd, struct stat *st) { + auto file = static_cast(fd); + return fstat(file->fd, st); +} + +int Device::devoptab_unlink(const char *_path) { + const auto [path, mount_name] = FixPath(_path); + if (mount_name.empty()) { + log_write("[MOUNTS] devoptab_unlink: invalid path: %s\n", _path); + return -ENOENT; + } + + return unlink(path); +} + +int Device::devoptab_rename(const char *_oldName, const char *_newName) { + const auto [oldName, old_mount_name] = FixPath(_oldName); + const auto [newName, new_mount_name] = FixPath(_newName); + if (old_mount_name.empty() || new_mount_name.empty() || old_mount_name != new_mount_name) { + log_write("[MOUNTS] devoptab_rename: invalid path: %s or %s\n", _oldName, _newName); + return -ENOENT; + } + + return rename(oldName, newName); +} + +int Device::devoptab_mkdir(const char *_path, int mode) { + const auto [path, mount_name] = FixPath(_path); + if (mount_name.empty()) { + log_write("[MOUNTS] devoptab_mkdir: invalid path: %s\n", _path); + return -ENOENT; + } + + return mkdir(path, mode); +} + +int Device::devoptab_rmdir(const char *_path) { + const auto [path, mount_name] = FixPath(_path); + if (mount_name.empty()) { + log_write("[MOUNTS] devoptab_rmdir: invalid path: %s\n", _path); + return -ENOENT; + } + + return rmdir(path); +} + +int Device::devoptab_diropen(void* fd, const char *_path) { + auto dir = static_cast(fd); + const auto [path, mount_name] = FixPath(_path); + + if (mount_name.empty()) { + dir->entries = new location::StdioEntries(); + const auto entries = location::GetStdio(false); + + for (auto& entry : entries) { + if (entry.fs_hidden) { + continue; + } + + dir->entries->emplace_back(std::move(entry)); + } + + return 0; + } else { + dir->dir = opendir(path); + if (!dir->dir) { + log_write("[MOUNTS] devoptab_diropen: failed to open dir %s: %s\n", path.s, std::strerror(errno)); + return -errno; + } + + return 0; + } + + return -ENOENT; +} + +int Device::devoptab_dirreset(void* fd) { + auto dir = static_cast(fd); + + if (dir->dir) { + rewinddir(dir->dir); + } else { + dir->index = 0; + } + + return 0; +} + +int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) { + log_write("[MOUNTS] devoptab_dirnext\n"); + auto dir = static_cast(fd); + + if (dir->dir) { + const auto entry = readdir(dir->dir); + if (!entry) { + log_write("[MOUNTS] devoptab_dirnext: no more entries\n"); + return -ENOENT; + } + + // todo: verify this. + filestat->st_nlink = 1; + filestat->st_mode = entry->d_type == DT_DIR ? S_IFDIR : S_IFREG; + std::snprintf(filename, NAME_MAX, "%s", entry->d_name); + } else { + if (dir->index >= dir->entries->size()) { + return -ENOENT; + } + + const auto& entry = (*dir->entries)[dir->index]; + filestat->st_nlink = 1; + filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + if (entry.mount.ends_with(":/")) { + std::snprintf(filename, NAME_MAX, "%s", entry.mount.substr(0, entry.mount.size() - 1).c_str()); + } else { + std::snprintf(filename, NAME_MAX, "%s", entry.mount.c_str()); + } + } + + dir->index++; + return 0; +} + +int Device::devoptab_dirclose(void* fd) { + auto dir = static_cast(fd); + + if (dir->dir) { + closedir(dir->dir); + } else if (dir->entries) { + delete dir->entries; + } + + return 0; +} + +int Device::devoptab_lstat(const char *_path, struct stat *st) { + const auto [path, mount_name] = FixPath(_path); + if (mount_name.empty()) { + st->st_nlink = 1; + st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + } else { + return lstat(path, st); + } + + return -ENOENT; +} + +int Device::devoptab_ftruncate(void *fd, off_t len) { + auto file = static_cast(fd); + return ftruncate(file->fd, len); +} + +int Device::devoptab_statvfs(const char *_path, struct statvfs *buf) { + const auto [path, mount_name] = FixPath(_path); + if (mount_name.empty()) { + log_write("[MOUNTS] devoptab_statvfs: invalid path: %s\n", _path); + return -ENOENT; + } + + return statvfs(path, buf); +} + +int Device::devoptab_fsync(void *fd) { + auto file = static_cast(fd); + return fsync(file->fd); +} + +int Device::devoptab_utimes(const char *_path, const struct timeval times[2]) { + const auto [path, mount_name] = FixPath(_path); + if (mount_name.empty()) { + log_write("[MOUNTS] devoptab_utimes: invalid path: %s\n", _path); + return -ENOENT; + } + + return utimes(path, times); +} + +} // namespace + +Result MountInternalMounts() { + common::MountConfig config{}; + config.fs_hidden = true; + config.dump_hidden = true; + + if (!common::MountNetworkDevice2( + std::make_unique(config), + config, + sizeof(File), sizeof(Dir), + "mounts", "mounts:/" + )) { + log_write("[MOUNTS] Failed to mount\n"); + R_THROW(0x1); + } + + R_SUCCEED(); +} + +} // namespace sphaira::devoptab