From b99d1e5dea4a5b2e9ee20c34e62b7350544fc070 Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Sun, 7 Sep 2025 12:40:45 +0100 Subject: [PATCH] devoptab: add webdav, refactor network devices, multi thread r/w to improve perf and support webdav uploads. --- sphaira/CMakeLists.txt | 12 + sphaira/include/fs.hpp | 5 +- sphaira/include/utils/devoptab.hpp | 15 +- sphaira/include/utils/devoptab_common.hpp | 128 +++ sphaira/source/app.cpp | 67 +- sphaira/source/fs.cpp | 3 +- sphaira/source/i18n.cpp | 17 +- sphaira/source/location.cpp | 28 +- sphaira/source/utils/devoptab_common.cpp | 1205 ++++++++++++++++++++- sphaira/source/utils/devoptab_ftp.cpp | 783 ++++--------- sphaira/source/utils/devoptab_http.cpp | 523 ++------- sphaira/source/utils/devoptab_nfs.cpp | 700 ++++-------- sphaira/source/utils/devoptab_smb2.cpp | 667 ++++-------- sphaira/source/utils/devoptab_webdav.cpp | 581 ++++++++++ 14 files changed, 2696 insertions(+), 2038 deletions(-) create mode 100644 sphaira/source/utils/devoptab_webdav.cpp diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index 53d4443..6582349 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -109,6 +109,7 @@ add_executable(sphaira source/utils/devoptab_nfs.cpp source/utils/devoptab_smb2.cpp source/utils/devoptab_ftp.cpp + source/utils/devoptab_webdav.cpp source/usb/base.cpp source/usb/usbds.cpp @@ -269,6 +270,15 @@ FetchContent_Declare(libsmb2 GIT_TAG 867beea ) +FetchContent_Declare(pugixml + GIT_REPOSITORY https://github.com/zeux/pugixml.git + GIT_TAG v1.15 +) + +set(PUGIXML_NO_EXCEPTIONS ON) +# set(PUGIXML_NO_XPATH ON) +set(PUGIXML_WCHAR_MODE OFF) + set(USE_NEW_ZSTD ON) # has issues with some homebrew and game icons (oxenfree, overwatch2). set(USE_NVJPG OFF) @@ -331,6 +341,7 @@ FetchContent_MakeAvailable( libusbdvd libnfs libsmb2 + pugixml ) set(FTPSRV_LIB_BUILD TRUE) @@ -464,6 +475,7 @@ target_link_libraries(sphaira PRIVATE libusbdvd nfs smb2 + pugixml ${minizip_lib} ZLIB::ZLIB diff --git a/sphaira/include/fs.hpp b/sphaira/include/fs.hpp index 5b7f75c..d474b39 100644 --- a/sphaira/include/fs.hpp +++ b/sphaira/include/fs.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -307,7 +308,7 @@ Result IsDirEmpty(fs::Fs* m_fs, const fs::FsPath& path, bool* out); // helpers. Result read_entire_file(Fs* fs, const FsPath& path, std::vector& out); -Result write_entire_file(Fs* fs, const FsPath& path, const std::vector& in, bool ignore_read_only = true); +Result write_entire_file(Fs* fs, const FsPath& path, std::span in, bool ignore_read_only = true); Result copy_entire_file(Fs* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only = true); struct Fs { @@ -354,7 +355,7 @@ struct Fs { Result read_entire_file(const FsPath& path, std::vector& out) { return fs::read_entire_file(this, path, out); } - Result write_entire_file(const FsPath& path, const std::vector& in) { + Result write_entire_file(const FsPath& path, std::span in) { return fs::write_entire_file(this, path, in, m_ignore_read_only); } Result copy_entire_file(const FsPath& dst, const FsPath& src) { diff --git a/sphaira/include/utils/devoptab.hpp b/sphaira/include/utils/devoptab.hpp index ce93641..7ad1228 100644 --- a/sphaira/include/utils/devoptab.hpp +++ b/sphaira/include/utils/devoptab.hpp @@ -33,20 +33,13 @@ void UmountBfsar(const fs::FsPath& mount); Result MountNro(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path); void UmountNro(const fs::FsPath& mount); +Result MountWebdavAll(); Result MountHttpAll(); -Result GetHttpMounts(location::StdioEntries& out); -void UnmountHttpAll(); - Result MountFtpAll(); -Result GetFtpMounts(location::StdioEntries& out); -void UnmountFtpAll(); - Result MountNfsAll(); -Result GetNfsMounts(location::StdioEntries& out); -void UnmountNfsAll(); - Result MountSmb2All(); -Result GetSmb2Mounts(location::StdioEntries& out); -void UnmountSmb2All(); + +Result GetNetworkDevices(location::StdioEntries& out); +void UmountAllNeworkDevices(); } // namespace sphaira::devoptab diff --git a/sphaira/include/utils/devoptab_common.hpp b/sphaira/include/utils/devoptab_common.hpp index f2aebb8..9d503c5 100644 --- a/sphaira/include/utils/devoptab_common.hpp +++ b/sphaira/include/utils/devoptab_common.hpp @@ -2,8 +2,13 @@ #include "yati/source/file.hpp" #include "utils/lru.hpp" +#include "location.hpp" #include +#include #include +#include +#include +#include namespace sphaira::devoptab::common { @@ -85,4 +90,127 @@ bool fix_path(const char* str, char* out, bool strip_leading_slash = false); void update_devoptab_for_read_only(devoptab_t* devoptab, bool read_only); +struct PushPullThreadData { + PushPullThreadData(CURL* _curl); + virtual ~PushPullThreadData(); + Result CreateAndStart(); + + void Cancel(); + bool IsRunning(); + + size_t PullData(char* data, size_t total_size); + size_t PushData(const char* data, size_t total_size); + + static size_t push_thread_callback(const char *ptr, size_t size, size_t nmemb, void *userdata); + static size_t pull_thread_callback(char *ptr, size_t size, size_t nmemb, void *userdata); + +private: + static void thread_func(void* arg); + +public: + CURL* const curl{}; + std::vector buffer{}; + Mutex mutex{}; + CondVar can_push{}; + CondVar can_pull{}; + + long code{}; + bool error{}; + bool finished{}; + bool started{}; + +private: + Thread thread{}; +}; + +struct MountConfig { + std::string name{}; + std::string url{}; + std::string user{}; + std::string pass{}; + std::optional port{}; + int timeout{3000}; // 3 seconds. + bool read_only{}; + bool no_stat_file{true}; + bool no_stat_dir{true}; + + std::unordered_map extra{}; +}; + +struct PullThreadData final : PushPullThreadData { + using PushPullThreadData::PushPullThreadData; + ~PullThreadData(); +}; + + +struct PushThreadData final : PushPullThreadData { + using PushPullThreadData::PushPullThreadData; + ~PushThreadData(); +}; + +struct MountDevice { + MountDevice(const MountConfig& _config) : config{_config} {} + virtual ~MountDevice() = default; + + virtual bool fix_path(const char* str, char* out, bool strip_leading_slash = false) { + return common::fix_path(str, out, strip_leading_slash); + } + + virtual bool Mount() = 0; + virtual int devoptab_open(void *fileStruct, const char *path, int flags, int mode) { return -EIO; } + virtual int devoptab_close(void *fd) { return -EIO; } + virtual ssize_t devoptab_read(void *fd, char *ptr, size_t len) { return -EIO; } + virtual ssize_t devoptab_write(void *fd, const char *ptr, size_t len) { return -EIO; } + virtual off_t devoptab_seek(void *fd, off_t pos, int dir) { return 0; } + virtual int devoptab_fstat(void *fd, struct stat *st) { return -EIO; } + virtual int devoptab_unlink(const char *path) { return -EIO; } + virtual int devoptab_rename(const char *oldName, const char *newName) { return -EIO; } + virtual int devoptab_mkdir(const char *path, int mode) { return -EIO; } + virtual int devoptab_rmdir(const char *path) { return -EIO; } + virtual int devoptab_diropen(void* fd, const char *path) { return -EIO; } + virtual int devoptab_dirreset(void* fd) { return -EIO; } + virtual int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) { return -EIO; } + virtual int devoptab_dirclose(void* fd) { return -EIO; } + virtual int devoptab_lstat(const char *path, struct stat *st) { return -EIO; } + virtual int devoptab_ftruncate(void *fd, off_t len) { return -EIO; } + virtual int devoptab_statvfs(const char *_path, struct statvfs *buf) { return -EIO; } + virtual int devoptab_fsync(void *fd) { return -EIO; } + virtual int devoptab_utimes(const char *_path, const struct timeval times[2]) { return -EIO; } + + const MountConfig config; +}; + +struct MountCurlDevice : MountDevice { + using MountDevice::MountDevice; + // MountCurlDevice(const MountConfig& _config); + virtual ~MountCurlDevice(); + + PushThreadData* CreatePushData(CURL* curl, const std::string& url, size_t offset); + PullThreadData* CreatePullData(CURL* curl, const std::string& url, bool append = false); + + virtual bool Mount(); + virtual void curl_set_common_options(CURL* curl, const std::string& url); + static size_t write_memory_callback(char *ptr, size_t size, size_t nmemb, void *userdata); + static size_t write_data_callback(char *ptr, size_t size, size_t nmemb, void *userdata); + static size_t read_data_callback(char *ptr, size_t size, size_t nmemb, void *userdata); + static std::string html_decode(const std::string_view& str); + static std::string url_decode(const std::string& str); + std::string build_url(const std::string& path, bool is_dir); + +protected: + CURL* curl{}; + CURL* transfer_curl{}; + +private: + // path extracted from the url. + std::string m_url_path{}; + CURLU* curlu{}; + CURLSH* m_curl_share{}; + RwLock m_rwlocks[CURL_LOCK_DATA_LAST]{}; + bool m_mounted{}; +}; + +using CreateDeviceCallback = std::function(const MountConfig& config)>; +Result MountNetworkDevice(const CreateDeviceCallback& create_device, size_t file_size, size_t dir_size, const char* config_path, const char* name); + } // namespace sphaira::devoptab::common diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index 41536eb..9aa45d8 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -543,7 +543,14 @@ void App::Loop() { } auto App::Push(std::unique_ptr&& widget) -> void { - log_write("[Mui] pushing widget\n"); + log_write("[APP] pushing widget\n"); + + // when freeing widges, this may cancel a transfer which causes it to push + // an error box, so check if we are quitting first before adding. + if (g_app->m_quit) { + log_write("[APP] is quitting, not pushing widget\n"); + return; + } // check if the widget wants to pop before adding. // this can happen if something failed in the constructor and the widget wants to exit. @@ -1597,6 +1604,11 @@ App::App(const char* argv0) { devoptab::MountHttpAll(); } + { + SCOPED_TIMESTAMP("webdav init"); + devoptab::MountWebdavAll(); + } + { SCOPED_TIMESTAMP("ftp init"); devoptab::MountFtpAll(); @@ -2178,6 +2190,16 @@ App::~App() { curl::ExitSignal(); } + // this has to be called before any cleanup to ensure the lifetime of + // nvg is still active as some widgets may need to free images. + // clear in reverse order as the widgets are a stack. + { + SCOPED_TIMESTAMP("widget exit"); + while (!m_widgets.empty()) { + m_widgets.pop_back(); + } + } + utils::Async async_exit([this](){ { SCOPED_TIMESTAMP("usbdvd_exit"); @@ -2206,23 +2228,8 @@ App::~App() { // this has to come before curl exit as it uses curl global. { - SCOPED_TIMESTAMP("http exit"); - devoptab::UnmountHttpAll(); - } - - { - SCOPED_TIMESTAMP("ftp exit"); - devoptab::UnmountFtpAll(); - } - - { - SCOPED_TIMESTAMP("nfs exit"); - devoptab::UnmountNfsAll(); - } - - { - SCOPED_TIMESTAMP("smb exit"); - devoptab::UnmountSmb2All(); + SCOPED_TIMESTAMP("devoptab exit"); + devoptab::UmountAllNeworkDevices(); } // do these last as they were signalled to exit. @@ -2248,25 +2255,6 @@ App::~App() { } }); - // destroy this first as it seems to prevent a crash when exiting the appstore - // when an image that was being drawn is displayed - // replicate: saves -> homebrew -> misc -> appstore -> sphaira -> changelog -> exit - // it will crash when deleting image 43. - { - SCOPED_TIMESTAMP("destroy frame buffer resources"); - this->destroyFramebufferResources(); - } - - // this has to be called before any cleanup to ensure the lifetime of - // nvg is still active as some widgets may need to free images. - // clear in reverse order as the widgets are a stack (todo: just use a stack?) - { - SCOPED_TIMESTAMP("widget exit"); - while (!m_widgets.empty()) { - m_widgets.pop_back(); - } - } - // do not async close theme as it frees textures. { SCOPED_TIMESTAMP("theme exit"); @@ -2274,6 +2262,11 @@ App::~App() { CloseTheme(); } + { + SCOPED_TIMESTAMP("destroy frame buffer resources"); + this->destroyFramebufferResources(); + } + { SCOPED_TIMESTAMP("nvg exit"); nvgDeleteImage(vg, m_default_image); diff --git a/sphaira/source/fs.cpp b/sphaira/source/fs.cpp index a8b5d2b..394db8f 100644 --- a/sphaira/source/fs.cpp +++ b/sphaira/source/fs.cpp @@ -134,7 +134,7 @@ Result read_entire_file(Fs* fs, const FsPath& path, std::vector& out) { R_SUCCEED(); } -Result write_entire_file(Fs* fs, const FsPath& path, const std::vector& in, bool ignore_read_only) { +Result write_entire_file(Fs* fs, const FsPath& path, std::span in, bool ignore_read_only) { R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly); if (auto rc = fs->CreateFile(path, in.size(), 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) { @@ -586,6 +586,7 @@ void File::Close() { } } else { if (m_stdio) { + log_write("[FS] closing stdio file\n"); std::fclose(m_stdio); m_stdio = {}; } diff --git a/sphaira/source/i18n.cpp b/sphaira/source/i18n.cpp index fe5e8db..b5cb5e9 100644 --- a/sphaira/source/i18n.cpp +++ b/sphaira/source/i18n.cpp @@ -8,12 +8,15 @@ namespace sphaira::i18n { namespace { -std::vector g_i18n_data; -yyjson_doc* json; -yyjson_val* root; -std::unordered_map g_tr_cache; +std::vector g_i18n_data{}; +yyjson_doc* json{}; +yyjson_val* root{}; +std::unordered_map g_tr_cache{}; +Mutex g_mutex{}; std::string get_internal(std::string_view str) { + SCOPED_MUTEX(&g_mutex); + const std::string kkey = {str.data(), str.length()}; if (auto it = g_tr_cache.find(kkey); it != g_tr_cache.end()) { @@ -50,6 +53,8 @@ std::string get_internal(std::string_view str) { } // namespace bool init(long index) { + SCOPED_MUTEX(&g_mutex); + g_tr_cache.clear(); R_TRY_RESULT(romfsInit(), false); ON_SCOPE_EXIT( romfsExit() ); @@ -87,7 +92,7 @@ bool init(long index) { case SetLanguage_DE: lang_name = "de"; break; case SetLanguage_IT: lang_name = "it"; break; case SetLanguage_ES: lang_name = "es"; break; - case SetLanguage_ZHCN: lang_name = "zh"; break; + case SetLanguage_ZHCN: lang_name = "zh"; break; case SetLanguage_KO: lang_name = "ko"; break; case SetLanguage_NL: lang_name = "nl"; break; case SetLanguage_PT: lang_name = "pt"; break; @@ -128,6 +133,8 @@ bool init(long index) { } void exit() { + SCOPED_MUTEX(&g_mutex); + if (json) { yyjson_doc_free(json); json = nullptr; diff --git a/sphaira/source/location.cpp b/sphaira/source/location.cpp index 76ce52e..8590a2d 100644 --- a/sphaira/source/location.cpp +++ b/sphaira/source/location.cpp @@ -93,32 +93,8 @@ auto GetStdio(bool write) -> StdioEntries { { StdioEntries entries; - if (R_SUCCEEDED(devoptab::GetNfsMounts(entries))) { - log_write("[NFS] got nfs mounts: %zu\n", entries.size()); - add_from_entries(entries, out, write); - } - } - - { - StdioEntries entries; - if (R_SUCCEEDED(devoptab::GetSmb2Mounts(entries))) { - log_write("[SMB2] got smb2 mounts: %zu\n", entries.size()); - add_from_entries(entries, out, write); - } - } - - { - StdioEntries entries; - if (R_SUCCEEDED(devoptab::GetHttpMounts(entries))) { - log_write("[HTTP] got http mounts: %zu\n", entries.size()); - add_from_entries(entries, out, write); - } - } - - { - StdioEntries entries; - if (R_SUCCEEDED(devoptab::GetFtpMounts(entries))) { - log_write("[FTP] got ftp mounts: %zu\n", entries.size()); + if (R_SUCCEEDED(devoptab::GetNetworkDevices(entries))) { + log_write("[LOCATION] got devoptab mounts: %zu\n", entries.size()); add_from_entries(entries, out, write); } } diff --git a/sphaira/source/utils/devoptab_common.cpp b/sphaira/source/utils/devoptab_common.cpp index 2eb2062..e2077dc 100644 --- a/sphaira/source/utils/devoptab_common.cpp +++ b/sphaira/source/utils/devoptab_common.cpp @@ -1,11 +1,554 @@ #include "utils/devoptab_common.hpp" +#include "utils/thread.hpp" + #include "defines.hpp" #include "log.hpp" +#include "download.hpp" #include #include +#include +#include +#include namespace sphaira::devoptab::common { +namespace { + +RwLock g_rwlock{}; + +// curl_url_strerror doesn't exist in the switch version of libcurl as its so old. +// todo: update libcurl and send patches to dkp. +const char* curl_url_strerror_wrap(CURLUcode code) { + switch (code) { + case CURLUE_OK: return "No error"; + case CURLUE_BAD_HANDLE: return "Invalid handle"; + case CURLUE_BAD_PARTPOINTER: return "Invalid pointer to a part of the URL"; + case CURLUE_MALFORMED_INPUT: return "Malformed input"; + case CURLUE_BAD_PORT_NUMBER: return "Invalid port number"; + case CURLUE_UNSUPPORTED_SCHEME: return "Unsupported scheme"; + case CURLUE_URLDECODE: return "Failed to decode URL component"; + case CURLUE_OUT_OF_MEMORY: return "Out of memory"; + case CURLUE_USER_NOT_ALLOWED: return "User not allowed in URL"; + case CURLUE_UNKNOWN_PART: return "Unknown URL part"; + case CURLUE_NO_SCHEME: return "No scheme found in URL"; + case CURLUE_NO_USER: return "No user found in URL"; + case CURLUE_NO_PASSWORD: return "No password found in URL"; + case CURLUE_NO_OPTIONS: return "No options found in URL"; + case CURLUE_NO_HOST: return "No host found in URL"; + case CURLUE_NO_PORT: return "No port number found in URL"; + case CURLUE_NO_QUERY: return "No query found in URL"; + case CURLUE_NO_FRAGMENT: return "No fragment found in URL"; + default: return "Unknown error 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; + size_t dir_size; + + MountConfig config{}; + Mutex mutex{}; +}; + +struct File { + Device* device; + void* fd; +}; + +struct Dir { + Device* device; + void* fd; +}; + +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 = static_cast(r->deviceData); + auto file = static_cast(fileStruct); + std::memset(file, 0, sizeof(*file)); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&device->mutex); + + if (device->config.read_only && (flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND))) { + return set_errno(r, EROFS); + } + + char path[PATH_MAX]{}; + if (!device->mount_device->fix_path(_path, path)) { + return set_errno(r, ENOENT); + } + + if (!device->mount_device->Mount()) { + return set_errno(r, EIO); + } + + file->fd = calloc(1, device->file_size); + if (!file->fd) { + return set_errno(r, ENOMEM); + } + + const auto ret = device->mount_device->devoptab_open(file->fd, path, flags, mode); + if (ret) { + free(file->fd); + file->fd = nullptr; + return set_errno(r, -ret); + } + + file->device = device; + return r->_errno = 0; +} + +int devoptab_close(struct _reent *r, void *fd) { + auto file = static_cast(fd); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&file->device->mutex); + + if (file->fd) { + file->device->mount_device->devoptab_close(file->fd); + free(file->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); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&file->device->mutex); + + const auto ret = file->device->mount_device->devoptab_read(file->fd, ptr, len); + if (ret < 0) { + return set_errno(r, -ret); + } + + return ret; +} + +ssize_t devoptab_write(struct _reent *r, void *fd, const char *ptr, size_t len) { + auto file = static_cast(fd); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&file->device->mutex); + + const auto ret = file->device->mount_device->devoptab_write(file->fd, ptr, len); + if (ret < 0) { + return set_errno(r, -ret); + } + + return ret; +} + +off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) { + auto file = static_cast(fd); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&file->device->mutex); + + const auto off = file->device->mount_device->devoptab_seek(file->fd, pos, dir); + r->_errno = 0; + return off; +} + +int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) { + auto file = static_cast(fd); + std::memset(st, 0, sizeof(*st)); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&file->device->mutex); + + const auto ret = file->device->mount_device->devoptab_fstat(file->fd, st); + if (ret) { + return set_errno(r, -ret); + } + + return r->_errno = 0; +} + +int devoptab_unlink(struct _reent *r, const char *_path) { + auto device = static_cast(r->deviceData); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&device->mutex); + + if (device->config.read_only) { + return set_errno(r, EROFS); + } + + char path[PATH_MAX]{}; + if (!device->mount_device->fix_path(_path, path)) { + return set_errno(r, ENOENT); + } + + if (!device->mount_device->Mount()) { + return set_errno(r, EIO); + } + + const auto ret = device->mount_device->devoptab_unlink(path); + if (ret) { + return set_errno(r, -ret); + } + + return r->_errno = 0; +} + +int devoptab_rename(struct _reent *r, const char *_oldName, const char *_newName) { + auto device = static_cast(r->deviceData); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&device->mutex); + + if (device->config.read_only) { + return set_errno(r, EROFS); + } + + char oldName[PATH_MAX]{}; + if (!device->mount_device->fix_path(_oldName, oldName)) { + return set_errno(r, ENOENT); + } + + char newName[PATH_MAX]{}; + if (!device->mount_device->fix_path(_newName, newName)) { + return set_errno(r, ENOENT); + } + + if (!device->mount_device->Mount()) { + return set_errno(r, EIO); + } + + const auto ret = device->mount_device->devoptab_rename(oldName, newName); + if (ret) { + return set_errno(r, -ret); + } + + return r->_errno = 0; +} + +int devoptab_mkdir(struct _reent *r, const char *_path, int mode) { + auto device = static_cast(r->deviceData); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&device->mutex); + + if (device->config.read_only) { + return set_errno(r, EROFS); + } + + char path[PATH_MAX]{}; + if (!device->mount_device->fix_path(_path, path)) { + return set_errno(r, ENOENT); + } + + if (!device->mount_device->Mount()) { + return set_errno(r, EIO); + } + + const auto ret = device->mount_device->devoptab_mkdir(path, mode); + if (ret) { + return set_errno(r, -ret); + } + + return r->_errno = 0; +} + +int devoptab_rmdir(struct _reent *r, const char *_path) { + auto device = static_cast(r->deviceData); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&device->mutex); + + if (device->config.read_only) { + return set_errno(r, EROFS); + } + + char path[PATH_MAX]{}; + if (!device->mount_device->fix_path(_path, path)) { + return set_errno(r, ENOENT); + } + + if (!device->mount_device->Mount()) { + return set_errno(r, EIO); + } + + const auto ret = device->mount_device->devoptab_rmdir(path); + if (ret) { + return set_errno(r, -ret); + } + + return r->_errno = 0; +} + +DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) { + auto device = static_cast(r->deviceData); + auto dir = static_cast(dirState->dirStruct); + std::memset(dir, 0, sizeof(*dir)); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&device->mutex); + + log_write("[DEVOPTAB] diropen %s\n", _path); + + if (!device->mount_device) { + log_write("[DEVOPTAB] diropen no mount device\n"); + set_errno(r, ENOENT); + return nullptr; + } + + char path[PATH_MAX]{}; + if (!device->mount_device->fix_path(_path, path)) { + set_errno(r, ENOENT); + return nullptr; + } + + log_write("[DEVOPTAB] diropen fixed path %s\n", path); + + if (!device->mount_device->Mount()) { + set_errno(r, EIO); + return nullptr; + } + + log_write("[DEVOPTAB] diropen mounted\n"); + + dir->fd = calloc(1, device->dir_size); + if (!dir->fd) { + set_errno(r, ENOMEM); + return nullptr; + } + + log_write("[DEVOPTAB] diropen allocated dir\n"); + + const auto ret = device->mount_device->devoptab_diropen(dir->fd, path); + if (ret) { + free(dir->fd); + dir->fd = nullptr; + set_errno(r, -ret); + return nullptr; + } + + log_write("[DEVOPTAB] diropen opened dir\n"); + + dir->device = device; + return dirState; +} + +int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { + auto dir = static_cast(dirState->dirStruct); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&dir->device->mutex); + + const auto ret = dir->device->mount_device->devoptab_dirreset(dir->fd); + if (ret) { + return set_errno(r, -ret); + } + + 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)); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&dir->device->mutex); + + const auto ret = dir->device->mount_device->devoptab_dirnext(dir->fd, filename, filestat); + if (ret) { + return set_errno(r, -ret); + } + + return r->_errno = 0; +} + +int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { + auto dir = static_cast(dirState->dirStruct); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&dir->device->mutex); + + if (dir->fd) { + dir->device->mount_device->devoptab_dirclose(dir->fd); + free(dir->fd); + } + + std::memset(dir, 0, sizeof(*dir)); + return r->_errno = 0; +} + +int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { + auto device = static_cast(r->deviceData); + std::memset(st, 0, sizeof(*st)); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&device->mutex); + + char path[PATH_MAX]{}; + if (!device->mount_device->fix_path(_path, path)) { + return set_errno(r, ENOENT); + } + + if (!device->mount_device->Mount()) { + return set_errno(r, EIO); + } + + const auto ret = device->mount_device->devoptab_lstat(path, st); + if (ret) { + return set_errno(r, -ret); + } + + return r->_errno = 0; +} + +int devoptab_ftruncate(struct _reent *r, void *fd, off_t len) { + auto file = static_cast(fd); + SCOPED_MUTEX(&file->device->mutex); + + if (!file || !file->fd) { + return set_errno(r, EBADF); + } + + if (file->device->config.read_only) { + return set_errno(r, EROFS); + } + + const auto ret = file->device->mount_device->devoptab_ftruncate(file->fd, len); + if (ret) { + return set_errno(r, -ret); + } + + return r->_errno = 0; +} + +int devoptab_statvfs(struct _reent *r, const char *_path, struct statvfs *buf) { + auto device = static_cast(r->deviceData); + std::memset(buf, 0, sizeof(*buf)); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&device->mutex); + + char path[PATH_MAX]{}; + if (!device->mount_device->fix_path(_path, path)) { + return set_errno(r, ENOENT); + } + + if (!device->mount_device->Mount()) { + return set_errno(r, EIO); + } + + const auto ret = device->mount_device->devoptab_statvfs(path, buf); + if (ret) { + return set_errno(r, -ret); + } + + return r->_errno = 0; +} + +int devoptab_fsync(struct _reent *r, void *fd) { + auto file = static_cast(fd); + SCOPED_MUTEX(&file->device->mutex); + + if (!file || !file->fd) { + return set_errno(r, EBADF); + } + + if (file->device->config.read_only) { + return set_errno(r, EROFS); + } + + const auto ret = file->device->mount_device->devoptab_fsync(file->fd); + if (ret) { + return set_errno(r, -ret); + } + + return r->_errno = 0; +} + +int devoptab_utimes(struct _reent *r, const char *_path, const struct timeval times[2]) { + auto device = static_cast(r->deviceData); + SCOPED_RWLOCK(&g_rwlock, false); + SCOPED_MUTEX(&device->mutex); + + if (!times) { + log_write("[NFS] devoptab_utimes() times is null\n"); + return set_errno(r, EINVAL); + } + + if (device->config.read_only) { + return set_errno(r, EROFS); + } + + char path[PATH_MAX]{}; + if (!device->mount_device->fix_path(_path, path)) { + return set_errno(r, ENOENT); + } + + if (!device->mount_device->Mount()) { + return set_errno(r, EIO); + } + + const auto ret = device->mount_device->devoptab_utimes(path, times); + if (ret) { + return set_errno(r, -ret); + } + + return r->_errno = 0; +} + +constexpr devoptab_t DEVOPTAB = { + .structSize = sizeof(File), + .open_r = devoptab_open, + .close_r = devoptab_close, + .write_r = devoptab_write, + .read_r = devoptab_read, + .seek_r = devoptab_seek, + .fstat_r = devoptab_fstat, + .stat_r = devoptab_lstat, + .unlink_r = devoptab_unlink, + .rename_r = devoptab_rename, + .mkdir_r = devoptab_mkdir, + .dirStateSize = sizeof(Dir), + .diropen_r = devoptab_diropen, + .dirreset_r = devoptab_dirreset, + .dirnext_r = devoptab_dirnext, + .dirclose_r = devoptab_dirclose, + .statvfs_r = devoptab_statvfs, + .ftruncate_r = devoptab_ftruncate, + .fsync_r = devoptab_fsync, + .rmdir_r = devoptab_rmdir, + .lstat_r = devoptab_lstat, + .utimes_r = devoptab_utimes, +}; + +struct Entry { + Device device{}; + devoptab_t devoptab{}; + fs::FsPath mount{}; + char name[32]{}; + s32 ref_count{}; + + ~Entry() { + RemoveDevice(mount); + } +}; + +std::array, 16> g_entries; + +} // namespace // todo: change above function to handle bytes read instead. Result BufferedData::Read(void *_buffer, s64 file_off, s64 read_size, u64* bytes_read) { @@ -157,8 +700,6 @@ Result LruBufferedData::Read(void *_buffer, s64 file_off, s64 read_size, u64* by } bool fix_path(const char* str, char* out, bool strip_leading_slash) { - // log_write("[SAVE] got path: %s\n", str); - str = std::strrchr(str, ':'); if (!str) { return false; @@ -199,25 +740,663 @@ bool fix_path(const char* str, char* out, bool strip_leading_slash) { // null the end. out[len] = '\0'; - // log_write("[SAVE] end path: %s\n", out); - return true; } void update_devoptab_for_read_only(devoptab_t* devoptab, bool read_only) { // remove write functions if read_only is set. if (read_only) { - devoptab->write_r = nullptr; - devoptab->link_r = nullptr; - devoptab->unlink_r = nullptr; - devoptab->rename_r = nullptr; - devoptab->mkdir_r = nullptr; + devoptab->write_r = nullptr; + devoptab->link_r = nullptr; + devoptab->unlink_r = nullptr; + devoptab->rename_r = nullptr; + devoptab->mkdir_r = nullptr; devoptab->ftruncate_r = nullptr; - devoptab->fsync_r = nullptr; - devoptab->rmdir_r = nullptr; - devoptab->utimes_r = nullptr; - devoptab->symlink_r = nullptr; + devoptab->fsync_r = nullptr; + devoptab->rmdir_r = nullptr; + devoptab->utimes_r = nullptr; + devoptab->symlink_r = nullptr; } } +Result MountNetworkDevice(const CreateDeviceCallback& create_device, size_t file_size, size_t dir_size, const char* config_path, const char* name) { + { + static Mutex rw_lock_init_mutex{}; + SCOPED_MUTEX(&rw_lock_init_mutex); + + static bool rwlock_init{}; + if (!rwlock_init) { + rwlockInit(&g_rwlock); + rwlock_init = true; + } + } + + SCOPED_RWLOCK(&g_rwlock, true); + + using MountConfigs = std::vector; + + static const auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int { + auto e = static_cast(UserData); + if (!Section || !Key || !Value) { + return 1; + } + + // add new entry if use section changed. + if (e->empty() || std::strcmp(Section, e->back().name.c_str())) { + e->emplace_back(Section); + } + + if (!std::strcmp(Key, "url")) { + e->back().url = Value; + } else if (!std::strcmp(Key, "user")) { + e->back().user = Value; + } else if (!std::strcmp(Key, "pass")) { + e->back().pass = Value; + } else if (!std::strcmp(Key, "port")) { + const auto port = ini_parse_getl(Value, -1); + if (port < 0 || port > 65535) { + log_write("[DEVOPTAB] INI: invalid port %s\n", Value); + } else { + e->back().port = port; + } + } else if (!std::strcmp(Key, "timeout")) { + e->back().timeout = ini_parse_getl(Value, e->back().timeout); + } else if (!std::strcmp(Key, "read_only")) { + e->back().read_only = ini_parse_getbool(Value, e->back().read_only); + } else if (!std::strcmp(Key, "no_stat_file")) { + e->back().no_stat_file = ini_parse_getbool(Value, e->back().no_stat_file); + } else if (!std::strcmp(Key, "no_stat_dir")) { + e->back().no_stat_dir = ini_parse_getbool(Value, e->back().no_stat_dir); + } else { + log_write("[DEVOPTAB] INI: extra key %s=%s\n", Key, Value); + e->back().extra.emplace(Key, Value); + } + + return 1; + }; + + MountConfigs configs; + ini_browse(cb, &configs, config_path); + log_write("[DEVOPTAB] Found %zu mount configs\n", configs.size()); + + for (const auto& config : configs) { + // check if we already have the http mounted. + bool already_mounted = false; + for (const auto& entry : g_entries) { + if (entry && entry->mount == config.name) { + already_mounted = true; + break; + } + } + + if (already_mounted) { + log_write("[DEVOPTAB] Already mounted %s, skipping\n", config.name.c_str()); + continue; + } + + // otherwise, find next free entry. + auto itr = std::ranges::find_if(g_entries, [](auto& e){ + return !e; + }); + + if (itr == g_entries.end()) { + log_write("[DEVOPTAB] No free entries to mount %s\n", config.name.c_str()); + break; + } + + auto entry = std::make_unique(); + entry->device.mount_device = create_device(config); + entry->device.file_size = file_size; + entry->device.dir_size = dir_size; + entry->device.config = config; + + if (!entry->device.mount_device) { + log_write("[DEVOPTAB] Failed to create device for %s\n", config.url.c_str()); + continue; + } + + entry->devoptab = DEVOPTAB; + entry->devoptab.name = entry->name; + entry->devoptab.deviceData = &entry->device; + std::snprintf(entry->name, sizeof(entry->name), "[%s] %s", name, config.name.c_str()); + std::snprintf(entry->mount, sizeof(entry->mount), "[%s] %s:/", name, config.name.c_str()); + common::update_devoptab_for_read_only(&entry->devoptab, config.read_only); + + R_UNLESS(AddDevice(&entry->devoptab) >= 0, 0x1); + log_write("[DEVOPTAB] DEVICE SUCCESS %s %s\n", entry->device.config.url.c_str(), entry->name); + + entry->ref_count++; + *itr = std::move(entry); + log_write("[DEVOPTAB] Mounted %s at /%s\n", config.url.c_str(), config.name.c_str()); + } + + R_SUCCEED(); +} + +PushPullThreadData::PushPullThreadData(CURL* _curl) : curl{_curl} { + mutexInit(&mutex); + condvarInit(&can_push); + condvarInit(&can_pull); +} + +PushPullThreadData::~PushPullThreadData() { + Cancel(); + threadWaitForExit(&thread); + threadClose(&thread); +} + +Result PushPullThreadData::CreateAndStart() { + if (started) { + R_SUCCEED(); + } + + R_TRY(utils::CreateThread(&thread, thread_func, this)); + R_TRY(threadStart(&thread)); + + started = true; + R_SUCCEED(); +} + +void PushPullThreadData::Cancel() { + SCOPED_MUTEX(&mutex); + finished = true; + condvarWakeOne(&can_pull); + condvarWakeOne(&can_push); +} + +bool PushPullThreadData::IsRunning() { + SCOPED_MUTEX(&mutex); + return !finished && !error; +} + +size_t PushPullThreadData::PullData(char* data, size_t total_size) { + SCOPED_MUTEX(&mutex); + ON_SCOPE_EXIT(condvarWakeOne(&can_push)); + + size_t bytes_read = 0; + while (bytes_read < total_size && !error) { + if (buffer.empty()) { + if (finished) { + break; + } + + condvarWakeOne(&can_push); + condvarWait(&can_pull, &mutex); + continue; + } + + const auto rsize = std::min(total_size - bytes_read, buffer.size()); + std::memcpy(data + bytes_read, buffer.data(), rsize); + buffer.erase(buffer.begin(), buffer.begin() + rsize); + bytes_read += rsize; + } + + return bytes_read; +} + +size_t PushPullThreadData::PushData(const char* data, size_t total_size) { + SCOPED_MUTEX(&mutex); + ON_SCOPE_EXIT(condvarWakeOne(&can_pull)); + + size_t bytes_written = 0; + while (bytes_written < total_size && !error && !finished) { + const size_t space_left = (1024 * 64) - buffer.size(); // 64K max buffer + if (space_left == 0) { + condvarWakeOne(&can_pull); + condvarWait(&can_push, &mutex); + continue; + } + + const auto wsize = std::min(total_size - bytes_written, space_left); + buffer.insert(buffer.end(), data + bytes_written, data + bytes_written + wsize); + bytes_written += wsize; + } + + return bytes_written; +} + +size_t PushPullThreadData::push_thread_callback(const char *ptr, size_t size, size_t nmemb, void *userdata) { + auto* data = static_cast(userdata); + return data->PushData(ptr, size * nmemb); +} + +size_t PushPullThreadData::pull_thread_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { + auto* data = static_cast(userdata); + return data->PullData(ptr, size * nmemb); +} + +void PushPullThreadData::thread_func(void* arg) { + log_write("[PUSH:PULL] Read thread started\n"); + auto data = static_cast(arg); + const auto res = curl_easy_perform(data->curl); + + // when finished, lock mutex and signal for anything waiting. + SCOPED_MUTEX(&data->mutex); + condvarWakeOne(&data->can_push); + condvarWakeOne(&data->can_pull); + + log_write("[PUSH:PULL] curl_easy_perform() finished for read thread: %s\n", curl_easy_strerror(res)); + + data->finished = true; + data->error = res != CURLE_OK; + curl_easy_getinfo(data->curl, CURLINFO_RESPONSE_CODE, &data->code); + + log_write("[PUSH:PULL] Read thread finished, code: %ld, error: %d\n", data->code, data->error); +} + +PullThreadData::~PullThreadData() { + if (started) { + SCOPED_MUTEX(&mutex); + + while (!finished && !error && !buffer.empty()) { + condvarWakeOne(&can_pull); + condvarWaitTimeout(&can_push, &mutex, 5e+9); + } + } +} + +PushThreadData::~PushThreadData() { + +} + +MountCurlDevice::~MountCurlDevice() { + log_write("[CURL] Cleaning up mount device\n"); + if (curlu) { + curl_url_cleanup(curlu); + } + + if (curl) { + curl_easy_cleanup(curl); + } + + if (transfer_curl) { + curl_easy_cleanup(transfer_curl); + } + + if (m_curl_share) { + curl_share_cleanup(m_curl_share); + } + log_write("[CURL] Cleaned up mount device\n"); +} + +bool MountCurlDevice::Mount() { + if (m_mounted) { + return true; + } + + if (!curl) { + curl = curl_easy_init(); + if (!curl) { + log_write("[CURL] curl_easy_init() failed\n"); + return false; + } + } + + if (!transfer_curl) { + transfer_curl = curl_easy_init(); + if (!transfer_curl) { + log_write("[CURL] transfer curl_easy_init() failed\n"); + return false; + } + } + + // setup url, only the path is updated at runtime. + if (!curlu) { + curlu = curl_url(); + if (!curlu) { + log_write("[CURL] curl_url() failed\n"); + return false; + } + + auto url = config.url; + if (url.starts_with("webdav://") || url.starts_with("webdavs://")) { + log_write("[CURL] updating host: %s\n", url.c_str()); + url.replace(0, std::strlen("webdav"), "http"); + log_write("[CURL] updated host: %s\n", url.c_str()); + } + + const auto flags = CURLU_DEFAULT_SCHEME|CURLU_URLENCODE; + CURLUcode rc = curl_url_set(curlu, CURLUPART_URL, url.c_str(), flags); + if (rc != CURLUE_OK) { + log_write("[CURL] curl_url_set() failed: %s\n", curl_url_strerror_wrap(rc)); + return false; + } + + if (config.port.has_value()) { + rc = curl_url_set(curlu, CURLUPART_PORT, std::to_string(config.port.value()).c_str(), flags); + if (rc != CURLUE_OK) { + log_write("[CURL] curl_url_set() port failed: %s\n", curl_url_strerror_wrap(rc)); + } + } + + if (!config.user.empty()) { + rc = curl_url_set(curlu, CURLUPART_USER, config.user.c_str(), flags); + if (rc != CURLUE_OK) { + log_write("[CURL] curl_url_set() user failed: %s\n", curl_url_strerror_wrap(rc)); + } + } + + if (!config.pass.empty()) { + rc = curl_url_set(curlu, CURLUPART_PASSWORD, config.pass.c_str(), flags); + if (rc != CURLUE_OK) { + log_write("[CURL] curl_url_set() pass failed: %s\n", curl_url_strerror_wrap(rc)); + } + } + + // try and parse the path from the url, if any. + // eg, https://example.com/some/path/here + char* path{}; + rc = curl_url_get(curlu, CURLUPART_PATH, &path, 0); + if (rc == CURLUE_OK && path) { + log_write("[CURL] base path: %s\n", path); + m_url_path = path; + curl_free(path); + } + } + + // create share handle, used to share info between curl and transfer_curl. + if (!m_curl_share) { + m_curl_share = curl_share_init(); + if (!m_curl_share) { + log_write("[CURL] curl_share_init() failed\n"); + return false; + } + + // todo: use a mutex instead. + for (auto& e : m_rwlocks) { + rwlockInit(&e); + } + + static const auto lock_func = [](CURL* handle, curl_lock_data data, curl_lock_access access, void* userptr) { + auto rwlocks = static_cast(userptr); + rwlockWriteLock(&rwlocks[data]); + + #if 0 + if (access == CURL_LOCK_ACCESS_SHARED) { + rwlockReadLock(&rwlocks[data]); + } else { + rwlockWriteLock(&rwlocks[data]); + } + #endif + }; + + static const auto unlock_func = [](CURL* handle, curl_lock_data data, void* userptr) { + auto rwlocks = static_cast(userptr); + rwlockWriteUnlock(&rwlocks[data]); + }; + + if (m_curl_share) { + curl_share_setopt(m_curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE); + curl_share_setopt(m_curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); + curl_share_setopt(m_curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); + curl_share_setopt(m_curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT); + curl_share_setopt(m_curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL); + curl_share_setopt(m_curl_share, CURLSHOPT_USERDATA, m_rwlocks); + curl_share_setopt(m_curl_share, CURLSHOPT_LOCKFUNC, lock_func); + curl_share_setopt(m_curl_share, CURLSHOPT_UNLOCKFUNC, unlock_func); + } + } + + return m_mounted = true; +} + +PushThreadData* MountCurlDevice::CreatePushData(CURL* curl, const std::string& url, size_t offset) { + auto data = new PushThreadData{curl}; + if (!data) { + log_write("[PUSH:PULL] Failed to allocate PushThreadData\n"); + return nullptr; + } + + curl_set_common_options(curl, url); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, PushThreadData::push_thread_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)data); + + if (offset > 0) { + char range[64]; + std::snprintf(range, sizeof(range), "%zu-", offset); + log_write("[PUSH:PULL] Requesting range: %s\n", range); + curl_easy_setopt(curl, CURLOPT_RANGE, range); + } + + if (R_FAILED(data->CreateAndStart())) { + log_write("[PUSH:PULL] Failed to create and start push thread\n"); + delete data; + return nullptr; + } + + return data; +} + +PullThreadData* MountCurlDevice::CreatePullData(CURL* curl, const std::string& url, bool append) { + auto data = new PullThreadData{curl}; + if (!data) { + log_write("[PUSH:PULL] Failed to allocate PullThreadData\n"); + return nullptr; + } + + curl_set_common_options(curl, url); + curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); + curl_easy_setopt(curl, CURLOPT_READFUNCTION, PullThreadData::pull_thread_callback); + curl_easy_setopt(curl, CURLOPT_READDATA, (void *)data); + + if (append) { + log_write("[PUSH:PULL] Setting append mode for upload\n"); + curl_easy_setopt(curl, CURLOPT_APPEND, 1L); + } + + if (R_FAILED(data->CreateAndStart())) { + log_write("[PUSH:PULL] Failed to create and start pull thread\n"); + delete data; + return nullptr; + } + + return data; +} + +void MountCurlDevice::curl_set_common_options(CURL* curl, const std::string& url) { + // NOTE: port, user and pass are set in the curl_url. + curl_easy_reset(curl); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + // cancel if speed is less than 1 bytes/sec for timeout seconds. + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1L); + // todo: change config to accept seconds rather than ms. + curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, config.timeout / 1000L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, config.timeout); + curl_easy_setopt(curl, CURLOPT_AUTOREFERER, 1L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 15L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 1024L * 64L); + curl_easy_setopt(curl, CURLOPT_UPLOAD_BUFFERSIZE, 1024L * 64L); + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); + + if (m_curl_share) { + curl_easy_setopt(curl, CURLOPT_SHARE, m_curl_share); + } +} + +size_t MountCurlDevice::write_memory_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { + auto data = static_cast*>(userdata); + + // increase by chunk size. + const auto realsize = size * nmemb; + if (data->capacity() < data->size() + realsize) { + const auto rsize = std::max(realsize, data->size() + 1024 * 1024); + data->reserve(rsize); + } + + // store the data. + const auto offset = data->size(); + data->resize(offset + realsize); + std::memcpy(data->data() + offset, ptr, realsize); + + return realsize; +} + +size_t MountCurlDevice::write_data_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { + auto data = static_cast*>(userdata); + const auto rsize = std::min(size * nmemb, data->size()); + + std::memcpy(data->data(), ptr, rsize); + *data = data->subspan(rsize); + return rsize; +} + +size_t MountCurlDevice::read_data_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { + auto data = static_cast*>(userdata); + const auto rsize = std::min(size * nmemb, data->size()); + + std::memcpy(ptr, data->data(), rsize); + *data = data->subspan(rsize); + return rsize; +} + +// libcurl doesn't handle html encodings, so we have to do it manually. +std::string MountCurlDevice::html_decode(const std::string_view& str) { + struct Entry { + std::string_view key; + char value; + }; + + static constexpr Entry map[]{ + { "&", '&' }, + { "<", '<' }, + { ">", '>' }, + { """, '"' }, + { "'", '\'' }, + { " ", ' ' }, + { "&", '&' }, + { "<", '<' }, + { ">", '>' }, + { """, '"' }, + { "'", '\'' }, + { " ", ' ' }, + { "#", '#' }, + { "%", '%' }, + { "+", '+' }, + { "=", '=' }, + { "@", '@' }, + { "[", '[' }, + { "]", ']' }, + { "{", '{' }, + { "}", '}' }, + { "~", '~' }, + }; + + std::string output{}; + output.reserve(str.size()); + + for (size_t i = 0; i < str.size(); i++) { + if (str[i] == '&') { + bool found = false; + for (const auto& e : map) { + if (!str.compare(i, e.key.length(), e.key)) { + output += e.value; + i += e.key.length() - 1; // skip ahead. + found = true; + break; + } + } + + if (!found) { + output += '&'; + } + } else { + output += str[i]; + } + } + + return output; +} + +std::string MountCurlDevice::url_decode(const std::string& str) { + auto unescaped = curl_unescape(str.c_str(), str.length()); + if (!unescaped) { + return str; + } + ON_SCOPE_EXIT(curl_free(unescaped)); + + return html_decode(unescaped); +} + +std::string MountCurlDevice::build_url(const std::string& _path, bool is_dir) { + log_write("[CURL] building url for path: %s\n", _path.c_str()); + auto path = _path; + if (is_dir && !path.ends_with('/')) { + path += '/'; // append trailing slash for folder. + } + + if (!m_url_path.empty()) { + if (path.starts_with('/') || m_url_path.ends_with('/')) { + path = m_url_path + path; + } else { + path = m_url_path + '/' + path; + } + } + + if (!path.empty()) { + const auto rc = curl_url_set(curlu, CURLUPART_PATH, path.c_str(), CURLU_URLENCODE); + if (rc != CURLUE_OK) { + log_write("[CURL] failed to set path: %s\n", curl_url_strerror_wrap(rc)); + return {}; + } + } + + char* encoded_url; + const auto rc = curl_url_get(curlu, CURLUPART_URL, &encoded_url, 0); + if (rc != CURLUE_OK) { + log_write("[CURL] failed to get encoded url: %s\n", curl_url_strerror_wrap(rc)); + return {}; + } + ON_SCOPE_EXIT(curl_free(encoded_url)); + + log_write("[CURL] encoded url: %s\n", encoded_url); + return encoded_url; +} + } // sphaira::devoptab::common + +namespace sphaira::devoptab { + +using namespace sphaira::devoptab::common; + +Result GetNetworkDevices(location::StdioEntries& out) { + SCOPED_RWLOCK(&g_rwlock, false); + out.clear(); + + for (const auto& entry : g_entries) { + if (entry) { + u32 flags = 0; + if (entry->device.config.read_only) { + flags |= location::FsEntryFlag::FsEntryFlag_ReadOnly; + } + if (entry->device.config.no_stat_file) { + flags |= location::FsEntryFlag::FsEntryFlag_NoStatFile; + } + if (entry->device.config.no_stat_dir) { + flags |= location::FsEntryFlag::FsEntryFlag_NoStatDir; + } + + out.emplace_back(entry->mount, entry->name, flags); + } + } + + R_SUCCEED(); +} + +void UmountAllNeworkDevices() { + SCOPED_RWLOCK(&g_rwlock, true); + + for (auto& entry : g_entries) { + if (!entry) { + continue; + } + + log_write("[DEVOPTAB] Unmounting %s\n", entry->device.config.url.c_str()); + entry.reset(); + } +} + +} // sphaira::devoptab diff --git a/sphaira/source/utils/devoptab_ftp.cpp b/sphaira/source/utils/devoptab_ftp.cpp index 22f2fe5..7c07376 100644 --- a/sphaira/source/utils/devoptab_ftp.cpp +++ b/sphaira/source/utils/devoptab_ftp.cpp @@ -1,13 +1,10 @@ #include "utils/devoptab_common.hpp" #include "utils/profile.hpp" -#include "location.hpp" #include "log.hpp" #include "defines.hpp" -#include #include #include -#include #include #include @@ -21,39 +18,6 @@ namespace sphaira::devoptab { namespace { -constexpr long DEFAULT_FTP_PORT = 21; -constexpr long DEFAULT_FTP_TIMEOUT = 3000; // 3 seconds. - -#define CURL_EASY_SETOPT_LOG(handle, opt, v) \ - if (auto r = curl_easy_setopt(handle, opt, v); r != CURLE_OK) { \ - log_write("curl_easy_setopt(%s, %s) msg: %s\n", #opt, #v, curl_easy_strerror(r)); \ - } \ - -#define CURL_EASY_GETINFO_LOG(handle, opt, v) \ - if (auto r = curl_easy_getinfo(handle, opt, v); r != CURLE_OK) { \ - log_write("curl_easy_getinfo(%s, %s) msg: %s\n", #opt, #v, curl_easy_strerror(r)); \ - } \ - -struct FtpMountConfig { - std::string name{}; - std::string url{}; - std::string user{}; - std::string pass{}; - std::optional port{}; - long timeout{DEFAULT_FTP_TIMEOUT}; - bool read_only{}; - bool no_stat_file{false}; - bool no_stat_dir{true}; -}; -using FtpMountConfigs = std::vector; - -struct Device { - CURL* curl{}; - FtpMountConfig config{}; - Mutex mutex{}; - bool mounted{}; -}; - struct DirEntry { std::string name{}; bool is_dir{}; @@ -65,102 +29,68 @@ struct FileEntry { struct stat st{}; }; +struct Device final : common::MountCurlDevice { + using MountCurlDevice::MountCurlDevice; + +private: + bool Mount() override; + 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_write(void *fd, const char *ptr, size_t len) override; + off_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_fsync(void *fd) override; + void curl_set_common_options(CURL* curl, const std::string& url) override; + + static bool ftp_parse_mlst_line(std::string_view line, struct stat* st, std::string* file_out, bool type_only); + static void ftp_parse_mlsd(std::string_view chunk, DirEntries& out); + static bool ftp_parse_mlist(std::string_view chunk, struct stat* st); + + std::pair ftp_quote(std::span commands, bool is_dir, std::vector* response_data = nullptr); + bool ftp_dirlist(const std::string& path, DirEntries& out); + bool ftp_stat(const std::string& path, struct stat* st, bool is_dir); + bool ftp_remove_file_folder(const std::string& path, bool is_dir); + bool ftp_unlink(const std::string& path); + bool ftp_rename(const std::string& old_path, const std::string& new_path, bool is_dir); + bool ftp_mkdir(const std::string& path); + bool ftp_rmdir(const std::string& path); + +private: + bool mounted{}; +}; + struct File { - Device* client; FileEntry* entry; + common::PushPullThreadData* push_pull_thread_data; size_t off; + size_t last_off; bool write_mode; + bool append_mode; }; struct Dir { - Device* client; DirEntries* entries; size_t index; }; -size_t write_memory_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { - auto data = static_cast*>(userdata); - - // increase by chunk size. - const auto realsize = size * nmemb; - if (data->capacity() < data->size() + realsize) { - const auto rsize = std::max(realsize, data->size() + 1024 * 1024); - data->reserve(rsize); - } - - // store the data. - const auto offset = data->size(); - data->resize(offset + realsize); - std::memcpy(data->data() + offset, ptr, realsize); - - return realsize; +void Device::curl_set_common_options(CURL* curl, const std::string& url) { + MountCurlDevice::curl_set_common_options(curl, url); + curl_easy_setopt(curl, CURLOPT_FTP_CREATE_MISSING_DIRS, CURLFTP_CREATE_DIR_NONE); + curl_easy_setopt(curl, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_NOCWD); } -size_t write_data_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { - auto data = static_cast*>(userdata); - const auto rsize = std::min(size * nmemb, data->size()); - - std::memcpy(data->data(), ptr, rsize); - *data = data->subspan(rsize); - return rsize; -} - -size_t read_data_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { - auto data = static_cast*>(userdata); - const auto rsize = std::min(size * nmemb, data->size()); - - std::memcpy(ptr, data->data(), rsize); - *data = data->subspan(rsize); - return rsize; -} - -std::string url_encode(const std::string& str) { - auto escaped = curl_escape(str.c_str(), str.length()); - if (!escaped) { - return str; - } - - std::string result(escaped); - curl_free(escaped); - return result; -} - -std::string build_url(const std::string& base, const std::string& path, bool is_dir) { - std::string url = base; - if (!url.ends_with('/')) { - url += '/'; - } - - url += url_encode(path); - if (is_dir && !url.ends_with('/')) { - url += '/'; // append trailing slash for folder. - } - - return url; -} - -void ftp_set_common_options(Device& client, const std::string& url) { - curl_easy_reset(client.curl); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_URL, url.c_str()); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_TIMEOUT_MS, client.config.timeout); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_CONNECTTIMEOUT_MS, client.config.timeout); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_NOPROGRESS, 0L); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_FTP_CREATE_MISSING_DIRS, CURLFTP_CREATE_DIR_NONE); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_NOCWD); - - if (client.config.port) { - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_PORT, client.config.port.value()); - } - - if (!client.config.user.empty()) { - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_USERNAME, client.config.user.c_str()); - } - if (!client.config.pass.empty()) { - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_PASSWORD, client.config.pass.c_str()); - } -} - -bool ftp_parse_mlst_line(std::string_view line, struct stat* st, std::string* file_out, bool type_only) { +bool Device::ftp_parse_mlst_line(std::string_view line, struct stat* st, std::string* file_out, bool type_only) { // trim leading white space. while (line.size() > 0 && std::isspace(line[0])) { line = line.substr(1); @@ -250,7 +180,7 @@ S> 250- Listing file1 S> Type=file;Modify=19990929003355.237; file1 S> 250 End */ -bool ftp_parse_mlist(std::string_view chunk, struct stat* st) { +bool Device::ftp_parse_mlist(std::string_view chunk, struct stat* st) { // sometimes the header data includes the full login exchange // so we need to find the actual start of the MLST response. const auto start_pos = chunk.find("250-"); @@ -281,7 +211,7 @@ D> Type=file;Size=25730;Modify=19940728095854;Perm=; capmux.tar.z D> Type=file;Size=1024990;Modify=19980130010322;Perm=r; cap60.pl198.tar.gz S> 226 MLSD completed */ -void ftp_parse_mlsd(std::string_view chunk, DirEntries& out) { +void Device::ftp_parse_mlsd(std::string_view chunk, DirEntries& out) { if (chunk.ends_with("\r\n")) { chunk = chunk.substr(0, chunk.size() - 2); } else if (chunk.ends_with('\n')) { @@ -306,8 +236,8 @@ void ftp_parse_mlsd(std::string_view chunk, DirEntries& out) { } } -std::pair ftp_quote(Device& client, std::span commands, bool is_dir, std::vector* response_data = nullptr) { - const auto url = build_url(client.config.url, "/", is_dir); +std::pair Device::ftp_quote(std::span commands, bool is_dir, std::vector* response_data) { + const auto url = build_url("/", is_dir); curl_slist* cmdlist{}; ON_SCOPE_EXIT(curl_slist_free_all(cmdlist)); @@ -316,37 +246,37 @@ std::pair ftp_quote(Device& client, std::span com cmdlist = curl_slist_append(cmdlist, cmd.c_str()); } - ftp_set_common_options(client, url); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_QUOTE, cmdlist); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_NOBODY, 1L); + curl_set_common_options(this->curl, url); + curl_easy_setopt(this->curl, CURLOPT_QUOTE, cmdlist); + curl_easy_setopt(this->curl, CURLOPT_NOBODY, 1L); if (response_data) { response_data->clear(); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_HEADERFUNCTION, write_memory_callback); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_HEADERDATA, (void *)response_data); + curl_easy_setopt(this->curl, CURLOPT_HEADERFUNCTION, write_memory_callback); + curl_easy_setopt(this->curl, CURLOPT_HEADERDATA, (void *)response_data); } - const auto res = curl_easy_perform(client.curl); + const auto res = curl_easy_perform(this->curl); if (res != CURLE_OK) { log_write("[FTP] curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); return {false, 0}; } long response_code = 0; - CURL_EASY_GETINFO_LOG(client.curl, CURLINFO_RESPONSE_CODE, &response_code); + curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code); return {true, response_code}; } -bool ftp_dirlist(Device& client, const std::string& path, DirEntries& out) { - const auto url = build_url(client.config.url, path, true); +bool Device::ftp_dirlist(const std::string& path, DirEntries& out) { + const auto url = build_url(path, true); std::vector chunk; - ftp_set_common_options(client, url); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_WRITEFUNCTION, write_memory_callback); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_WRITEDATA, (void *)&chunk); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_CUSTOMREQUEST, "MLSD"); + curl_set_common_options(this->curl, url); + curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, write_memory_callback); + curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, (void *)&chunk); + curl_easy_setopt(this->curl, CURLOPT_CUSTOMREQUEST, "MLSD"); - const auto res = curl_easy_perform(client.curl); + const auto res = curl_easy_perform(this->curl); if (res != CURLE_OK) { log_write("[FTP] curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); return false; @@ -356,11 +286,11 @@ bool ftp_dirlist(Device& client, const std::string& path, DirEntries& out) { return true; } -bool ftp_stat(Device& client, const std::string& path, struct stat* st, bool is_dir) { +bool Device::ftp_stat(const std::string& path, struct stat* st, bool is_dir) { std::memset(st, 0, sizeof(*st)); std::vector chunk; - const auto [success, response_code] = ftp_quote(client, {"MLST " + path}, is_dir, &chunk); + const auto [success, response_code] = ftp_quote({"MLST " + path}, is_dir, &chunk); if (!success) { return false; } @@ -378,67 +308,9 @@ bool ftp_stat(Device& client, const std::string& path, struct stat* st, bool is_ return true; } -bool ftp_read_file_chunk(Device& client, const std::string& path, size_t start, std::span buffer) { - const auto url = build_url(client.config.url, path, false); - - char range[64]{}; - std::snprintf(range, sizeof(range), "%zu-%zu", start, start + buffer.size() - 1); - log_write("[FTP] Requesting range: %s\n", range); - - ftp_set_common_options(client, url); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_RANGE, range); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_WRITEFUNCTION, write_data_callback); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_WRITEDATA, (void *)&buffer); - - const auto res = curl_easy_perform(client.curl); - if (res != CURLE_OK) { - log_write("[FTP] curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); - return false; - } - - return true; -} - -bool ftp_write_file_chunk(Device& client, const std::string& path, size_t start, std::span buffer) { - // manually set offset as curl seems to not do anything if CURLOPT_RESUME_FROM_LARGE is used for ftp. - // NOTE: RFC 3659 specifies that setting the offset to anything other than the end for STOR - // is undefined behavior, so random access writes are disabled for now. - #if 0 - if (start || !buffer.empty()) { - const auto [success, response_code] = ftp_quote(client, {"REST " + std::to_string(start)}, false); - if (!success || response_code != 350) { - log_write("[FTP] REST command failed with response code: %ld\n", response_code); - return false; - } - } - #endif - - const auto url = build_url(client.config.url, path, false); - - log_write("[FTP] Writing %zu bytes at offset %zu to %s\n", buffer.size(), start, path.c_str()); - ftp_set_common_options(client, url); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_UPLOAD, 1L); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)buffer.size()); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_READFUNCTION, read_data_callback); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_READDATA, (void *)&buffer); - - // set resume from if needed. - if (start || !buffer.empty()) { - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_APPEND, 1L); - } - - const auto res = curl_easy_perform(client.curl); - if (res != CURLE_OK) { - log_write("[FTP] curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); - return false; - } - - return true; -} - -bool ftp_remove_file_folder(Device& client, const std::string& path, bool is_dir) { +bool Device::ftp_remove_file_folder(const std::string& path, bool is_dir) { const auto cmd = (is_dir ? "RMD " : "DELE ") + path; - const auto [success, response_code] = ftp_quote(client, {cmd}, is_dir); + const auto [success, response_code] = ftp_quote({cmd}, is_dir); if (!success || response_code >= 400) { log_write("[FTP] MLST command failed with response code: %ld\n", response_code); return false; @@ -447,18 +319,18 @@ bool ftp_remove_file_folder(Device& client, const std::string& path, bool is_dir return true; } -bool ftp_unlink(Device& client, const std::string& path) { - return ftp_remove_file_folder(client, path, false); +bool Device::ftp_unlink(const std::string& path) { + return ftp_remove_file_folder(path, false); } -bool ftp_rename(Device& client, const std::string& old_path, const std::string& new_path, bool is_dir) { - const auto url = build_url(client.config.url, "/", is_dir); +bool Device::ftp_rename(const std::string& old_path, const std::string& new_path, bool is_dir) { + const auto url = build_url("/", is_dir); std::vector commands; commands.emplace_back("RNFR " + old_path); commands.emplace_back("RNTO " + new_path); - const auto [success, response_code] = ftp_quote(client, commands, is_dir); + const auto [success, response_code] = ftp_quote(commands, is_dir); if (!success || response_code >= 400) { log_write("[FTP] MLST command failed with response code: %ld\n", response_code); return false; @@ -467,9 +339,9 @@ bool ftp_rename(Device& client, const std::string& old_path, const std::string& return true; } -bool ftp_mkdir(Device& client, const std::string& path) { +bool Device::ftp_mkdir(const std::string& path) { std::vector chunk; - const auto [success, response_code] = ftp_quote(client, {"MKD " + path}, true); + const auto [success, response_code] = ftp_quote({"MKD " + path}, true); if (!success) { return false; } @@ -483,26 +355,22 @@ bool ftp_mkdir(Device& client, const std::string& path) { return true; } -bool ftp_rmdir(Device& client, const std::string& path) { - return ftp_remove_file_folder(client, path, true); +bool Device::ftp_rmdir(const std::string& path) { + return ftp_remove_file_folder(path, true); } -bool mount_ftp(Device& client) { - if (client.mounted) { +bool Device::Mount() { + if (mounted) { return true; } - if (!client.curl) { - client.curl = curl_easy_init(); - if (!client.curl) { - log_write("[FTP] curl_easy_init() failed\n"); - return false; - } + if (!MountCurlDevice::Mount()) { + return false; } // issue FEAT command to see if we support MLST/MLSD. std::vector chunk; - const auto [success, response_code] = ftp_quote(client, {"FEAT"}, true, &chunk); + const auto [success, response_code] = ftp_quote({"FEAT"}, true, &chunk); if (!success || response_code != 211) { log_write("[FTP] FEAT command failed with response code: %ld\n", response_code); return false; @@ -521,120 +389,115 @@ bool mount_ftp(Device& client) { if (view.find("UTF8") != std::string_view::npos) { // it doesn't matter if this fails tbh. // also, i am not sure if this persists between logins or not... - ftp_quote(client, {"OPTS UTF8 ON"}, true); + ftp_quote({"OPTS UTF8 ON"}, true); } - client.mounted = true; - return true; + return this->mounted = true; } -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; +int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) { auto file = static_cast(fileStruct); - std::memset(file, 0, sizeof(*file)); - SCOPED_MUTEX(&device->mutex); + struct stat st{}; - if (device->config.read_only && (flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND))) { - return set_errno(r, EROFS); - } + if ((flags & O_ACCMODE) == O_RDONLY || (flags & O_APPEND)) { + // ensure the file exists and get its size. + if (!ftp_stat(path, &st, false)) { + log_write("[FTP] File not found: %s\n", path); + return -ENOENT; + } - char path[PATH_MAX]{}; - if (!common::fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_ftp(*device)) { - return set_errno(r, EIO); - } - - // create an empty file. - if (flags & (O_CREAT | O_TRUNC)) { - std::span empty{}; - if (!ftp_write_file_chunk(*device, path, 0, empty)) { - log_write("[FTP] Failed to create file: %s\n", path); - return set_errno(r, EIO); + if (st.st_mode & S_IFDIR) { + log_write("[FTP] Path is a directory, not a file: %s\n", path); + return -EISDIR; } } - // ensure the file exists and get its size. - struct stat st{}; - if (!ftp_stat(*device, path, &st, false)) { - log_write("[FTP] File not found: %s\n", path); - return set_errno(r, ENOENT); - } - - if (st.st_mode & S_IFDIR) { - log_write("[FTP] Path is a directory, not a file: %s\n", path); - return set_errno(r, EISDIR); - } - - if (flags & O_APPEND) { - file->off = st.st_size; - } else { - file->off = 0; - } - - log_write("[FTP] Opened file: %s (size=%zu)\n", path, (size_t)st.st_size); - file->client = device; file->entry = new FileEntry{path, st}; file->write_mode = (flags & (O_WRONLY | O_RDWR)); - return r->_errno = 0; + file->append_mode = (flags & O_APPEND); + + if (file->append_mode) { + file->off = st.st_size; + file->last_off = file->off; + } + + return 0; } -int devoptab_close(struct _reent *r, void *fd) { +int Device::devoptab_close(void *fd) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->client->mutex); + delete file->push_pull_thread_data; delete file->entry; - std::memset(file, 0, sizeof(*file)); - return r->_errno = 0; + return 0; } -ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) { +ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->client->mutex); len = std::min(len, file->entry->st.st_size - file->off); if (file->write_mode) { log_write("[FTP] Attempt to read from a write-only file\n"); - return set_errno(r, EBADF); + return -EBADF; } if (!len) { return 0; } - if (!ftp_read_file_chunk(*file->client, file->entry->path, file->off, {ptr, len})) { - log_write("[FTP] Failed to read file chunk: %s\n", file->entry->path.c_str()); - return set_errno(r, EIO); + if (file->off != file->last_off) { + log_write("[FTP] File offset changed from %zu to %zu, resetting download thread\n", file->last_off, file->off); + file->last_off = file->off; + delete file->push_pull_thread_data; + file->push_pull_thread_data = nullptr; } - file->off += len; - return len; + if (!file->push_pull_thread_data) { + log_write("[FTP] Creating download thread data for file: %s\n", file->entry->path.c_str()); + file->push_pull_thread_data = CreatePushData(this->transfer_curl, build_url(file->entry->path, false), file->off); + if (!file->push_pull_thread_data) { + log_write("[FTP] Failed to create download thread data for file: %s\n", file->entry->path.c_str()); + return -EIO; + } + } + + const auto ret = file->push_pull_thread_data->PullData(ptr, len); + + file->off += ret; + file->last_off = file->off; + return ret; } -ssize_t devoptab_write(struct _reent *r, void *fd, const char *ptr, size_t len) { +ssize_t Device::devoptab_write(void *fd, const char *ptr, size_t len) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->client->mutex); - if (!ftp_write_file_chunk(*file->client, file->entry->path, file->off, {ptr, len})) { - log_write("[FTP] Failed to write file chunk: %s\n", file->entry->path.c_str()); - return set_errno(r, EIO); + if (!file->write_mode) { + log_write("[FTP] Attempt to write to a read-only file\n"); + return -EBADF; } - file->off += len; + if (!len) { + return 0; + } + + if (!file->push_pull_thread_data) { + log_write("[FTP] Creating upload thread data for file: %s\n", file->entry->path.c_str()); + file->push_pull_thread_data = CreatePullData(this->transfer_curl, build_url(file->entry->path, false), file->append_mode); + if (!file->push_pull_thread_data) { + log_write("[FTP] Failed to create upload thread data for file: %s\n", file->entry->path.c_str()); + return -EIO; + } + } + + const auto ret = file->push_pull_thread_data->PushData(ptr, len); + + file->off += ret; file->entry->st.st_size = std::max(file->entry->st.st_size, file->off); - return len; + return ret; } -off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) { +off_t Device::devoptab_seek(void *fd, off_t pos, int dir) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->client->mutex); if (dir == SEEK_CUR) { pos += file->off; @@ -644,152 +507,77 @@ off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) { // for now, random access writes are disabled. if (file->write_mode && pos != file->off) { - set_errno(r, ESPIPE); + log_write("[FTP] Random access writes are not supported\n"); return file->off; } - r->_errno = 0; return file->off = std::clamp(pos, 0, file->entry->st.st_size); } -int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) { +int Device::devoptab_fstat(void *fd, struct stat *st) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->client->mutex); std::memcpy(st, &file->entry->st, sizeof(*st)); - return r->_errno = 0; + return 0; } -int devoptab_unlink(struct _reent *r, const char *_path) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!common::fix_path(_path, path)) { - return set_errno(r, ENOENT); +int Device::devoptab_unlink(const char *path) { + if (!ftp_unlink(path)) { + return -EIO; } - if (!mount_ftp(*device)) { - return set_errno(r, EIO); - } - - if (!ftp_unlink(*device, path)) { - return set_errno(r, EIO); - } - - return r->_errno = 0; + return 0; } -int devoptab_rename(struct _reent *r, const char *_oldName, const char *_newName) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char oldName[PATH_MAX]{}; - if (!common::fix_path(_oldName, oldName)) { - return set_errno(r, ENOENT); +int Device::devoptab_rename(const char *oldName, const char *newName) { + if (!ftp_rename(oldName, newName, false) && !ftp_rename(oldName, newName, true)) { + return -EIO; } - char newName[PATH_MAX]{}; - if (!common::fix_path(_newName, newName)) { - return set_errno(r, ENOENT); - } - - if (!mount_ftp(*device)) { - return set_errno(r, EIO); - } - - if (!ftp_rename(*device, oldName, newName, false) && !ftp_rename(*device, oldName, newName, true)) { - return set_errno(r, EIO); - } - - return r->_errno = 0; + return 0; } -int devoptab_mkdir(struct _reent *r, const char *_path, int mode) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!common::fix_path(_path, path)) { - return set_errno(r, ENOENT); +int Device::devoptab_mkdir(const char *path, int mode) { + if (!ftp_mkdir(path)) { + return -EIO; } - if (!mount_ftp(*device)) { - return set_errno(r, EIO); - } - - if (!ftp_mkdir(*device, path)) { - return set_errno(r, EIO); - } - - return r->_errno = 0; + return 0; } -int devoptab_rmdir(struct _reent *r, const char *_path) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!common::fix_path(_path, path)) { - return set_errno(r, ENOENT); +int Device::devoptab_rmdir(const char *path) { + if (!ftp_rmdir(path)) { + return -EIO; } - if (!mount_ftp(*device)) { - return set_errno(r, EIO); - } - - if (!ftp_rmdir(*device, path)) { - return set_errno(r, EIO); - } - - return r->_errno = 0; + return 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)); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]; - if (!common::fix_path(_path, path)) { - set_errno(r, ENOENT); - return NULL; - } - - if (!mount_ftp(*device)) { - set_errno(r, EIO); - return NULL; - } +int Device::devoptab_diropen(void* fd, const char *path) { + auto dir = static_cast(fd); auto entries = new DirEntries(); - if (!ftp_dirlist(*device, path, *entries)) { + if (!ftp_dirlist(path, *entries)) { delete entries; - set_errno(r, ENOENT); - return NULL; + return -ENOENT; } - dir->client = device; dir->entries = entries; - r->_errno = 0; - return dirState; + return 0; } -int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { - auto dir = static_cast(dirState->dirStruct); - SCOPED_MUTEX(&dir->client->mutex); +int Device::devoptab_dirreset(void* fd) { + auto dir = static_cast(fd); dir->index = 0; - return r->_errno = 0; + return 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)); - SCOPED_MUTEX(&dir->client->mutex); +int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) { + auto dir = static_cast(fd); if (dir->index >= dir->entries->size()) { - return set_errno(r, ENOENT); + return -ENOENT; } auto& entry = (*dir->entries)[dir->index]; @@ -803,214 +591,57 @@ int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struc std::strcpy(filename, entry.name.c_str()); dir->index++; - return r->_errno = 0; + return 0; } -int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { - auto dir = static_cast(dirState->dirStruct); - SCOPED_MUTEX(&dir->client->mutex); +int Device::devoptab_dirclose(void* fd) { + auto dir = static_cast(fd); delete dir->entries; - std::memset(dir, 0, sizeof(*dir)); - return r->_errno = 0; + return 0; } -int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { - auto device = (Device*)r->deviceData; - SCOPED_MUTEX(&device->mutex); - char path[PATH_MAX]; - - if (!common::fix_path(_path, path)) { - return set_errno(r, ENOENT); +int Device::devoptab_lstat(const char *path, struct stat *st) { + if (!ftp_stat(path, st, false) && !ftp_stat(path, st, true)) { + return -ENOENT; } - if (!mount_ftp(*device)) { - return set_errno(r, EIO); - } - - if (!ftp_stat(*device, path, st, false) && !ftp_stat(*device, path, st, true)) { - return set_errno(r, ENOENT); - } - - return r->_errno = 0; + return 0; } -int devoptab_ftruncate(struct _reent *r, void *fd, off_t len) { +int Device::devoptab_ftruncate(void *fd, off_t len) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->client->mutex); if (!file->write_mode) { log_write("[FTP] Attempt to truncate a read-only file\n"); - return set_errno(r, EBADF); + return EBADF; } file->entry->st.st_size = len; - return r->_errno = 0; + return 0; } -constexpr devoptab_t DEVOPTAB = { - .structSize = sizeof(File), - .open_r = devoptab_open, - .close_r = devoptab_close, - .write_r = devoptab_write, - .read_r = devoptab_read, - .seek_r = devoptab_seek, - .fstat_r = devoptab_fstat, - .stat_r = devoptab_lstat, - .unlink_r = devoptab_unlink, - .rename_r = devoptab_rename, - .mkdir_r = devoptab_mkdir, - .dirStateSize = sizeof(Dir), - .diropen_r = devoptab_diropen, - .dirreset_r = devoptab_dirreset, - .dirnext_r = devoptab_dirnext, - .dirclose_r = devoptab_dirclose, - .ftruncate_r = devoptab_ftruncate, - .rmdir_r = devoptab_rmdir, - .lstat_r = devoptab_lstat, -}; +int Device::devoptab_fsync(void *fd) { + auto file = static_cast(fd); -struct Entry { - Device device{}; - devoptab_t devoptab{}; - fs::FsPath mount{}; - char name[32]{}; - s32 ref_count{}; - - ~Entry() { - if (device.curl) { - curl_easy_cleanup(device.curl); - } - - RemoveDevice(mount); + if (!file->write_mode) { + log_write("[FTP] Attempt to fsync a read-only file\n"); + return -EBADF; } -}; -Mutex g_mutex; -std::array, common::MAX_ENTRIES> g_entries; + return 0; +} } // namespace Result MountFtpAll() { - SCOPED_MUTEX(&g_mutex); - - static const auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int { - auto e = static_cast(UserData); - if (!Section || !Key || !Value) return 1; - - if (!Section || !Key || !Value) { - return 1; - } - - // add new entry if use section changed. - if (e->empty() || std::strcmp(Section, e->back().name.c_str())) { - e->emplace_back(Section); - } - - if (!std::strcmp(Key, "url")) { - e->back().url = Value; - } else if (!std::strcmp(Key, "user")) { - e->back().user = Value; - } else if (!std::strcmp(Key, "pass")) { - e->back().pass = Value; - } else if (!std::strcmp(Key, "port")) { - e->back().port = ini_parse_getl(Value, DEFAULT_FTP_PORT); - } else if (!std::strcmp(Key, "timeout")) { - e->back().timeout = ini_parse_getl(Value, e->back().timeout); - } else if (!std::strcmp(Key, "read_only")) { - e->back().read_only = ini_parse_getbool(Value, e->back().read_only); - } else if (!std::strcmp(Key, "no_stat_file")) { - e->back().no_stat_file = ini_parse_getbool(Value, e->back().no_stat_file); - } else if (!std::strcmp(Key, "no_stat_dir")) { - e->back().no_stat_dir = ini_parse_getbool(Value, e->back().no_stat_dir); - } else { - log_write("[FTP] INI: Unknown key %s=%s\n", Key, Value); - } - - return 1; - }; - - FtpMountConfigs configs; - ini_browse(cb, &configs, "/config/sphaira/ftp.ini"); - log_write("[FTP] Found %zu mount configs\n", configs.size()); - - for (const auto& config : configs) { - // check if we already have the http mounted. - bool already_mounted = false; - for (const auto& entry : g_entries) { - if (entry && entry->mount == config.name) { - already_mounted = true; - break; - } - } - - if (already_mounted) { - log_write("[FTP] Already mounted %s, skipping\n", config.name.c_str()); - continue; - } - - // otherwise, find next free entry. - auto itr = std::ranges::find_if(g_entries, [](auto& e){ - return !e; - }); - - if (itr == g_entries.end()) { - log_write("[FTP] No free entries to mount %s\n", config.name.c_str()); - break; - } - - auto entry = std::make_unique(); - entry->devoptab = DEVOPTAB; - entry->devoptab.name = entry->name; - entry->devoptab.deviceData = &entry->device; - entry->device.config = config; - std::snprintf(entry->name, sizeof(entry->name), "[FTP] %s", config.name.c_str()); - std::snprintf(entry->mount, sizeof(entry->mount), "[FTP] %s:/", config.name.c_str()); - common::update_devoptab_for_read_only(&entry->devoptab, config.read_only); - - R_UNLESS(AddDevice(&entry->devoptab) >= 0, 0x1); - log_write("[FTP] DEVICE SUCCESS %s %s\n", entry->device.config.url.c_str(), entry->name); - - entry->ref_count++; - *itr = std::move(entry); - log_write("[FTP] Mounted %s at /%s\n", config.url.c_str(), config.name.c_str()); - } - - R_SUCCEED(); -} - -void UnmountFtpAll() { - SCOPED_MUTEX(&g_mutex); - - for (auto& entry : g_entries) { - if (entry) { - entry.reset(); - } - } -} - -Result GetFtpMounts(location::StdioEntries& out) { - SCOPED_MUTEX(&g_mutex); - out.clear(); - - for (const auto& entry : g_entries) { - if (entry) { - u32 flags = 0; - if (entry->device.config.read_only) { - flags |= location::FsEntryFlag::FsEntryFlag_ReadOnly; - } - if (entry->device.config.no_stat_file) { - flags |= location::FsEntryFlag::FsEntryFlag_NoStatFile; - } - if (entry->device.config.no_stat_dir) { - flags |= location::FsEntryFlag::FsEntryFlag_NoStatDir; - } - - out.emplace_back(entry->mount, entry->name, flags); - } - } - - R_SUCCEED(); + return common::MountNetworkDevice([](const common::MountConfig& config) { + return std::make_unique(config); + }, + sizeof(File), sizeof(Dir), + "/config/sphaira/ftp.ini", + "FTP" + ); } } // namespace sphaira::devoptab diff --git a/sphaira/source/utils/devoptab_http.cpp b/sphaira/source/utils/devoptab_http.cpp index 31f7884..dc180f8 100644 --- a/sphaira/source/utils/devoptab_http.cpp +++ b/sphaira/source/utils/devoptab_http.cpp @@ -19,37 +19,6 @@ namespace sphaira::devoptab { namespace { -constexpr long DEFAULT_HTTP_TIMEOUT = 3000; // 3 seconds. - -#define CURL_EASY_SETOPT_LOG(handle, opt, v) \ - if (auto r = curl_easy_setopt(handle, opt, v); r != CURLE_OK) { \ - log_write("curl_easy_setopt(%s, %s) msg: %s\n", #opt, #v, curl_easy_strerror(r)); \ - } \ - -#define CURL_EASY_GETINFO_LOG(handle, opt, v) \ - if (auto r = curl_easy_getinfo(handle, opt, v); r != CURLE_OK) { \ - log_write("curl_easy_getinfo(%s, %s) msg: %s\n", #opt, #v, curl_easy_strerror(r)); \ - } \ - -struct HttpMountConfig { - std::string name{}; - std::string url{}; - std::string user{}; - std::string pass{}; - std::optional port{}; - long timeout{DEFAULT_HTTP_TIMEOUT}; - bool no_stat_file{true}; - bool no_stat_dir{true}; -}; -using HttpMountConfigs = std::vector; - -struct Device { - CURL* curl{}; - HttpMountConfig config; - Mutex mutex{}; - bool mounted{}; -}; - struct DirEntry { std::string name{}; std::string href{}; @@ -63,115 +32,51 @@ struct FileEntry { }; struct File { - Device* client; FileEntry* entry; + common::PushPullThreadData* push_pull_thread_data; size_t off; + size_t last_off; }; struct Dir { - Device* client; DirEntries* entries; size_t index; }; -size_t dirlist_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { - auto data = static_cast*>(userdata); +struct Device final : common::MountCurlDevice { + using MountCurlDevice::MountCurlDevice; - // increase by chunk size. - const auto realsize = size * nmemb; - if (data->capacity() < data->size() + realsize) { - const auto rsize = std::max(realsize, data->size() + 1024 * 1024); - data->reserve(rsize); - } +private: + bool Mount() override; + 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; + off_t devoptab_seek(void *fd, off_t pos, int dir) override; + int devoptab_fstat(void *fd, struct stat *st) 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; - // store the data. - const auto offset = data->size(); - data->resize(offset + realsize); - std::memcpy(data->data() + offset, ptr, realsize); + bool http_dirlist(const std::string& path, DirEntries& out); + bool http_stat(const std::string& path, struct stat* st, bool is_dir); - return realsize; -} +private: + bool mounted{}; +}; -size_t write_data_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { - auto data = static_cast*>(userdata); - const auto rsize = std::min(size * nmemb, data->size()); - - std::memcpy(data->data(), ptr, rsize); - *data = data->subspan(rsize); - return rsize; -} - -std::string url_encode(const std::string& str) { - auto escaped = curl_escape(str.c_str(), str.length()); - if (!escaped) { - return str; - } - - std::string result(escaped); - curl_free(escaped); - return result; -} - -std::string build_url(const std::string& base, const std::string& path, bool is_dir) { - std::string url = base; - if (!url.ends_with('/')) { - url += '/'; - } - - url += url_encode(path); - if (is_dir && !url.ends_with('/')) { - url += '/'; // append trailing slash for folder. - } - - return url; -} - -void http_set_common_options(Device& client, const std::string& url) { - curl_easy_reset(client.curl); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_URL, url.c_str()); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_TIMEOUT, client.config.timeout); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_CONNECTTIMEOUT, client.config.timeout); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_AUTOREFERER, 1L); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_FOLLOWLOCATION, 1L); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_SSL_VERIFYPEER, 0L); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_SSL_VERIFYHOST, 0L); - // disabled as i want to see the http core. - // CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_FAILONERROR, 1L); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_NOPROGRESS, 0L); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_BUFFERSIZE, 1024L * 512L); - - if (client.config.port) { - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_PORT, client.config.port.value()); - } - - // enable all forms of compression supported by libcurl. - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_ACCEPT_ENCODING, ""); - - // in most cases, this will use CURLAUTH_BASIC. - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_HTTPAUTH, (long)CURLAUTH_BASIC); - - // enable TE is server supports it. - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_TRANSFER_ENCODING, 1L); - - if (!client.config.user.empty()) { - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_USERNAME, client.config.user.c_str()); - } - if (!client.config.pass.empty()) { - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_PASSWORD, client.config.pass.c_str()); - } -} - -bool http_dirlist(Device& client, const std::string& path, DirEntries& out) { - const auto url = build_url(client.config.url, path, true); +bool Device::http_dirlist(const std::string& path, DirEntries& out) { + const auto url = build_url(path, true); std::vector chunk; log_write("[HTTP] Listing URL: %s path: %s\n", url.c_str(), path.c_str()); - http_set_common_options(client, url); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_WRITEFUNCTION, dirlist_callback); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_WRITEDATA, (void *)&chunk); + curl_set_common_options(this->curl, url); + curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, write_memory_callback); + curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, (void *)&chunk); - const auto res = curl_easy_perform(client.curl); + const auto res = curl_easy_perform(this->curl); if (res != CURLE_OK) { log_write("[HTTP] curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); return false; @@ -218,22 +123,19 @@ bool http_dirlist(Device& client, const std::string& path, DirEntries& out) { } pos = name_end + anchor_tag_end.length(); - const auto href_view = table_view.substr(href_begin, href_end - href_begin); - const auto name_view = table_view.substr(name_begin, name_end - name_begin); + const auto href = url_decode(std::string{table_view.substr(href_begin, href_end - href_begin)}); + auto name = url_decode(std::string{table_view.substr(name_begin, name_end - name_begin)}); // skip empty names/links, root dir entry and links that are not actual files/dirs (e.g. sorting/filter controls). - if (name_view.empty() || href_view.empty() || name_view == "/" || href_view.starts_with('?') || href_view.starts_with('#')) { + if (name.empty() || href.empty() || name == "/" || href.starts_with('?') || href.starts_with('#')) { continue; } // skip parent directory entry and external links. - if (href_view == ".." || name_view == ".." || href_view.starts_with("../") || name_view.starts_with("../") || href_view.find("://") != std::string::npos) { + if (href == ".." || name == ".." || href.starts_with("../") || name.starts_with("../") || href.find("://") != std::string::npos) { continue; } - std::string name{name_view}; - const std::string href{href_view}; - const auto is_dir = name.ends_with('/'); if (is_dir) { name.pop_back(); // remove the trailing '/' @@ -248,38 +150,38 @@ bool http_dirlist(Device& client, const std::string& path, DirEntries& out) { return true; } -bool http_stat(Device& client, const std::string& path, struct stat* st, bool is_dir) { +bool Device::http_stat(const std::string& path, struct stat* st, bool is_dir) { std::memset(st, 0, sizeof(*st)); - const auto url = build_url(client.config.url, path, is_dir); + const auto url = build_url(path, is_dir); - http_set_common_options(client, url); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_NOBODY, 1L); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_FILETIME, 1L); + curl_set_common_options(this->curl, url); + curl_easy_setopt(this->curl, CURLOPT_NOBODY, 1L); + curl_easy_setopt(this->curl, CURLOPT_FILETIME, 1L); - const auto res = curl_easy_perform(client.curl); + const auto res = curl_easy_perform(this->curl); if (res != CURLE_OK) { log_write("[HTTP] curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); return false; } long response_code = 0; - CURL_EASY_GETINFO_LOG(client.curl, CURLINFO_RESPONSE_CODE, &response_code); + curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code); curl_off_t file_size = 0; - CURL_EASY_GETINFO_LOG(client.curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &file_size); + curl_easy_getinfo(this->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &file_size); curl_off_t file_time = 0; - CURL_EASY_GETINFO_LOG(client.curl, CURLINFO_FILETIME_T, &file_time); + curl_easy_getinfo(this->curl, CURLINFO_FILETIME_T, &file_time); const char* content_type{}; - CURL_EASY_GETINFO_LOG(client.curl, CURLINFO_CONTENT_TYPE, &content_type); + curl_easy_getinfo(this->curl, CURLINFO_CONTENT_TYPE, &content_type); const char* effective_url{}; - CURL_EASY_GETINFO_LOG(client.curl, CURLINFO_EFFECTIVE_URL, &effective_url); + curl_easy_getinfo(this->curl, CURLINFO_EFFECTIVE_URL, &effective_url); // handle error codes. if (response_code != 200 && response_code != 206) { - log_write("[HTTP] Unexpected HTTP response code: %ld\n", response_code); + log_write("[WEBDAV] Unexpected HTTP response code: %ld\n", response_code); return false; } @@ -308,113 +210,80 @@ bool http_stat(Device& client, const std::string& path, struct stat* st, bool is return true; } -bool http_read_file_chunk(Device& client, const std::string& path, size_t start, std::span buffer) { - SCOPED_TIMESTAMP("http_read_file_chunk"); - const auto url = build_url(client.config.url, path, false); - - char range[64]; - std::snprintf(range, sizeof(range), "%zu-%zu", start, start + buffer.size() - 1); - log_write("[HTTP] Requesting range: %s\n", range); - - http_set_common_options(client, url); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_RANGE, range); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_WRITEFUNCTION, write_data_callback); - CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_WRITEDATA, (void *)&buffer); - - const auto res = curl_easy_perform(client.curl); - if (res != CURLE_OK) { - log_write("[HTTP] curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); - return false; - } - - return true; -} - -bool mount_http(Device& client) { - if (client.curl) { +bool Device::Mount() { + if (mounted) { return true; } - client.curl = curl_easy_init(); - if (!client.curl) { - log_write("[HTTP] curl_easy_init() failed\n"); + if (!MountCurlDevice::Mount()) { return false; } - return true; + // todo: query server with OPTIONS to see if it supports range requests. + // todo: see ftp for example. + + return mounted = true; } -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; +int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) { auto file = static_cast(fileStruct); - std::memset(file, 0, sizeof(*file)); - SCOPED_MUTEX(&device->mutex); - - // todo: add this check to all devoptabs. - if ((flags & O_ACCMODE) != O_RDONLY) { - log_write("[HTTP] Only read-only mode is supported\n"); - return set_errno(r, EINVAL); - } - - char path[PATH_MAX]{}; - if (!common::fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_http(*device)) { - return set_errno(r, EIO); - } struct stat st; - if (!http_stat(*device, path, &st, false)) { + if (!http_stat(path, &st, false)) { log_write("[HTTP] http_stat() failed for file: %s\n", path); - return set_errno(r, ENOENT); + return -ENOENT; } if (st.st_mode & S_IFDIR) { log_write("[HTTP] Attempted to open a directory as a file: %s\n", path); - return set_errno(r, EISDIR); + return -EISDIR; } - file->client = device; file->entry = new FileEntry{path, st}; - return r->_errno = 0; + return 0; } -int devoptab_close(struct _reent *r, void *fd) { +int Device::devoptab_close(void *fd) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->client->mutex); + delete file->push_pull_thread_data; delete file->entry; - std::memset(file, 0, sizeof(*file)); - return r->_errno = 0; + return 0; } -ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) { +ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->client->mutex); len = std::min(len, file->entry->st.st_size - file->off); if (!len) { return 0; } - if (!http_read_file_chunk(*file->client, file->entry->path, file->off, {ptr, len})) { - return set_errno(r, EIO); + if (file->off != file->last_off) { + log_write("[HTTP] File offset changed from %zu to %zu, resetting download thread\n", file->last_off, file->off); + file->last_off = file->off; + delete file->push_pull_thread_data; + file->push_pull_thread_data = nullptr; } - file->off += len; - return len; + if (!file->push_pull_thread_data) { + log_write("[HTTP] Creating download thread data for file: %s\n", file->entry->path.c_str()); + file->push_pull_thread_data = CreatePushData(this->transfer_curl, build_url(file->entry->path, false), file->off); + if (!file->push_pull_thread_data) { + log_write("[HTTP] Failed to create download thread data for file: %s\n", file->entry->path.c_str()); + return -EIO; + } + } + + const auto ret = file->push_pull_thread_data->PullData(ptr, len); + + file->off += ret; + file->last_off = file->off; + return ret; } -off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) { +off_t Device::devoptab_seek(void *fd, off_t pos, int dir) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->client->mutex); if (dir == SEEK_CUR) { pos += file->off; @@ -422,63 +291,43 @@ off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) { pos = file->entry->st.st_size; } - r->_errno = 0; return file->off = std::clamp(pos, 0, file->entry->st.st_size); } -int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) { +int Device::devoptab_fstat(void *fd, struct stat *st) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->client->mutex); std::memcpy(st, &file->entry->st, sizeof(*st)); - return r->_errno = 0; + return 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)); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]; - if (!common::fix_path(_path, path)) { - set_errno(r, ENOENT); - return NULL; - } - - if (!mount_http(*device)) { - set_errno(r, EIO); - return NULL; - } +int Device::devoptab_diropen(void* fd, const char *path) { + auto dir = static_cast(fd); + log_write("[HTTP] Opening directory: %s\n", path); auto entries = new DirEntries(); - if (!http_dirlist(*device, path, *entries)) { + if (!http_dirlist(path, *entries)) { delete entries; - set_errno(r, ENOENT); - return NULL; + return -ENOENT; } - dir->client = device; + log_write("[HTTP] Opened directory: %s with %zu entries\n", path, entries->size()); dir->entries = entries; - r->_errno = 0; - return dirState; + return 0; } -int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { - auto dir = static_cast(dirState->dirStruct); - SCOPED_MUTEX(&dir->client->mutex); +int Device::devoptab_dirreset(void* fd) { + auto dir = static_cast(fd); dir->index = 0; - return r->_errno = 0; + return 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)); - SCOPED_MUTEX(&dir->client->mutex); +int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) { + auto dir = static_cast(fd); if (dir->index >= dir->entries->size()) { - return set_errno(r, ENOENT); + return -ENOENT; } auto& entry = (*dir->entries)[dir->index]; @@ -492,188 +341,34 @@ int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struc std::strcpy(filename, entry.name.c_str()); dir->index++; - return r->_errno = 0; + return 0; } -int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { - auto dir = static_cast(dirState->dirStruct); - SCOPED_MUTEX(&dir->client->mutex); +int Device::devoptab_dirclose(void* fd) { + auto dir = static_cast(fd); delete dir->entries; - std::memset(dir, 0, sizeof(*dir)); - return r->_errno = 0; + return 0; } -int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { - auto device = (Device*)r->deviceData; - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]; - if (!common::fix_path(_path, path)) { - return set_errno(r, ENOENT); +int Device::devoptab_lstat(const char *path, struct stat *st) { + if (!http_stat(path, st, false) && !http_stat(path, st, true)) { + return -ENOENT; } - if (!mount_http(*device)) { - return set_errno(r, EIO); - } - - if (!http_stat(*device, path, st, false) && !http_stat(*device, path, st, true)) { - return set_errno(r, ENOENT); - } - - return r->_errno = 0; + return 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 mount{}; - char name[32]{}; - s32 ref_count{}; - - ~Entry() { - if (device.curl) { - curl_easy_cleanup(device.curl); - } - RemoveDevice(mount); - } -}; - -Mutex g_mutex; -std::array, common::MAX_ENTRIES> g_entries; - } // namespace Result MountHttpAll() { - SCOPED_MUTEX(&g_mutex); - - static const auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int { - auto e = static_cast(UserData); - if (!Section || !Key || !Value) { - return 1; - } - - // add new entry if use section changed. - if (e->empty() || std::strcmp(Section, e->back().name.c_str())) { - e->emplace_back(Section); - } - - if (!std::strcmp(Key, "url")) { - e->back().url = Value; - } else if (!std::strcmp(Key, "user")) { - e->back().user = Value; - } else if (!std::strcmp(Key, "pass")) { - e->back().pass = Value; - } else if (!std::strcmp(Key, "port")) { - // todo: idk what the default should be. - e->back().port = ini_parse_getl(Value, 8000); - } else if (!std::strcmp(Key, "timeout")) { - e->back().timeout = ini_parse_getl(Value, e->back().timeout); - } else if (!std::strcmp(Key, "no_stat_file")) { - e->back().no_stat_file = ini_parse_getbool(Value, e->back().no_stat_file); - } else if (!std::strcmp(Key, "no_stat_dir")) { - e->back().no_stat_dir = ini_parse_getbool(Value, e->back().no_stat_dir); - } else { - log_write("[HTTP] INI: Unknown key %s=%s\n", Key, Value); - } - - return 1; - }; - - HttpMountConfigs configs; - ini_browse(cb, &configs, "/config/sphaira/http.ini"); - log_write("[HTTP] Found %zu mount configs\n", configs.size()); - - for (const auto& config : configs) { - // check if we already have the http mounted. - bool already_mounted = false; - for (const auto& entry : g_entries) { - if (entry && entry->mount == config.name) { - already_mounted = true; - break; - } - } - - if (already_mounted) { - log_write("[HTTP] Already mounted %s, skipping\n", config.name.c_str()); - continue; - } - - // otherwise, find next free entry. - auto itr = std::ranges::find_if(g_entries, [](auto& e){ - return !e; - }); - - if (itr == g_entries.end()) { - log_write("[HTTP] No free entries to mount %s\n", config.name.c_str()); - break; - } - - auto entry = std::make_unique(); - entry->devoptab = DEVOPTAB; - entry->devoptab.name = entry->name; - entry->devoptab.deviceData = &entry->device; - entry->device.config = config; - std::snprintf(entry->name, sizeof(entry->name), "[HTTP] %s", config.name.c_str()); - std::snprintf(entry->mount, sizeof(entry->mount), "[HTTP] %s:/", config.name.c_str()); - - R_UNLESS(AddDevice(&entry->devoptab) >= 0, 0x1); - log_write("[HTTP] DEVICE SUCCESS %s %s\n", entry->device.config.url.c_str(), entry->name); - - entry->ref_count++; - *itr = std::move(entry); - log_write("[HTTP] Mounted %s at /%s\n", config.url.c_str(), config.name.c_str()); - } - - R_SUCCEED(); -} - -void UnmountHttpAll() { - SCOPED_MUTEX(&g_mutex); - - for (auto& entry : g_entries) { - if (entry) { - entry.reset(); - } - } -} - -Result GetHttpMounts(location::StdioEntries& out) { - SCOPED_MUTEX(&g_mutex); - out.clear(); - - for (const auto& entry : g_entries) { - if (entry) { - u32 flags = 0; - flags |= location::FsEntryFlag::FsEntryFlag_ReadOnly; - if (entry->device.config.no_stat_file) { - flags |= location::FsEntryFlag::FsEntryFlag_NoStatFile; - } - if (entry->device.config.no_stat_dir) { - flags |= location::FsEntryFlag::FsEntryFlag_NoStatDir; - } - - out.emplace_back(entry->mount, entry->name, flags); - } - } - - R_SUCCEED(); + return common::MountNetworkDevice([](const common::MountConfig& config) { + return std::make_unique(config); + }, + sizeof(File), sizeof(Dir), + "/config/sphaira/http.ini", + "HTTP" + ); } } // namespace sphaira::devoptab diff --git a/sphaira/source/utils/devoptab_nfs.cpp b/sphaira/source/utils/devoptab_nfs.cpp index 93fb6c6..f3e949a 100644 --- a/sphaira/source/utils/devoptab_nfs.cpp +++ b/sphaira/source/utils/devoptab_nfs.cpp @@ -15,159 +15,179 @@ namespace sphaira::devoptab { namespace { -constexpr int DEFAULT_NFS_UID = 0; -constexpr int DEFAULT_NFS_GID = 0; -constexpr int DEFAULT_NFS_VERSION = 3; -constexpr int DEFAULT_NFS_TIMEOUT = 3000; // 3 seconds. +struct Device final : common::MountDevice { + Device(const common::MountConfig& cfg) : MountDevice{cfg} {} + ~Device(); -struct NfsMountConfig { - std::string name{}; - std::string url{}; - int uid{DEFAULT_NFS_UID}; - int gid{DEFAULT_NFS_GID}; - int version{DEFAULT_NFS_VERSION}; - int timeout{DEFAULT_NFS_TIMEOUT}; - bool read_only{}; - bool no_stat_file{false}; - bool no_stat_dir{true}; -}; -using NfsMountConfigs = std::vector; +private: + bool Mount() override; + 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_write(void *fd, const char *ptr, size_t len) override; + off_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; -struct Device { +private: nfs_context* nfs{}; - nfs_url* url{}; - NfsMountConfig config; bool mounted{}; - Mutex mutex{}; }; struct File { - Device* device; nfsfh* fd; }; struct Dir { - Device* device; nfsdir* dir; }; -bool mount_nfs(Device& device) { - if (device.mounted) { +Device::~Device() { + if (nfs) { + if (mounted) { + nfs_umount(nfs); + } + + nfs_destroy_context(nfs); + } +} + +bool Device::Mount() { + if (mounted) { return true; } - if (!device.nfs) { - device.nfs = nfs_init_context(); - if (!device.nfs) { + log_write("[NFS] Mounting %s\n", this->config.url.c_str()); + + if (!nfs) { + nfs = nfs_init_context(); + if (!nfs) { log_write("[NFS] nfs_init_context() failed\n"); return false; } - nfs_set_uid(device.nfs, device.config.uid); - nfs_set_gid(device.nfs, device.config.gid); - nfs_set_version(device.nfs, device.config.version); - nfs_set_timeout(device.nfs, device.config.timeout); - nfs_set_readonly(device.nfs, device.config.read_only); - // nfs_set_mountport(device.nfs, device.url->port); + const auto uid = this->config.extra.find("uid"); + if (uid != this->config.extra.end()) { + const auto uid_val = ini_parse_getl(uid->second.c_str(), -1); + if (uid_val < 0) { + log_write("[NFS] Invalid uid value: %s\n", uid->second.c_str()); + } else { + log_write("[NFS] Setting uid: %ld\n", uid_val); + nfs_set_uid(nfs, uid_val); + } + } + + const auto gid = this->config.extra.find("gid"); + if (gid != this->config.extra.end()) { + const auto gid_val = ini_parse_getl(gid->second.c_str(), -1); + if (gid_val < 0) { + log_write("[NFS] Invalid gid value: %s\n", gid->second.c_str()); + } else { + log_write("[NFS] Setting gid: %ld\n", gid_val); + nfs_set_gid(nfs, gid_val); + } + } + + const auto version = this->config.extra.find("version"); + if (version != this->config.extra.end()) { + const auto version_val = ini_parse_getl(version->second.c_str(), -1); + if (version_val != 3 && version_val != 4) { + log_write("[NFS] Invalid version value: %s\n", version->second.c_str()); + } else { + log_write("[NFS] Setting version: %ld\n", version_val); + nfs_set_version(nfs, version_val); + } + } + + nfs_set_timeout(nfs, this->config.timeout); + nfs_set_readonly(nfs, this->config.read_only); + // nfs_set_mountport(nfs, url->port); } // fix the url if needed. - if (!device.config.url.starts_with("nfs://")) { - log_write("[NFS] Prepending nfs:// to url: %s\n", device.config.url.c_str()); - device.config.url = "nfs://" + device.config.url; + auto url = this->config.url; + if (!url.starts_with("nfs://")) { + log_write("[NFS] Prepending nfs:// to url: %s\n", url.c_str()); + url = "nfs://" + url; } - if (!device.url) { - // todo: check all parse options. - device.url = nfs_parse_url_dir(device.nfs, device.config.url.c_str()); - if (!device.url) { - log_write("[NFS] nfs_parse_url() failed for url: %s\n", device.config.url.c_str()); - return false; - } + auto nfs_url = nfs_parse_url_dir(nfs, url.c_str()); + if (!nfs_url) { + log_write("[NFS] nfs_parse_url() failed for url: %s\n", url.c_str()); + return false; } + ON_SCOPE_EXIT(nfs_destroy_url(nfs_url)); - const auto ret = nfs_mount(device.nfs, device.url->server, device.url->path); + const auto ret = nfs_mount(nfs, nfs_url->server, nfs_url->path); if (ret) { - log_write("[NFS] nfs_mount() failed: %s errno: %s\n", nfs_get_error(device.nfs), std::strerror(-ret)); + log_write("[NFS] nfs_mount() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); return false; } - device.mounted = true; - return true; + log_write("[NFS] Mounted %s\n", this->config.url.c_str()); + return mounted = true; } -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 = static_cast(r->deviceData); +int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) { auto file = static_cast(fileStruct); - std::memset(file, 0, sizeof(*file)); - SCOPED_MUTEX(&device->mutex); - if (device->config.read_only && (flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND))) { - return set_errno(r, EROFS); - } - - char path[PATH_MAX]{}; - if (!common::fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_nfs(*device)) { - return set_errno(r, EIO); - } - - const auto ret = nfs_open(device->nfs, path, flags, &file->fd); + const auto ret = nfs_open(nfs, path, flags, &file->fd); if (ret) { - log_write("[NFS] nfs_open() failed: %s errno: %s\n", nfs_get_error(device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[NFS] nfs_open() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; } - file->device = device; - return r->_errno = 0; + return 0; } -int devoptab_close(struct _reent *r, void *fd) { +int Device::devoptab_close(void *fd) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->device->mutex); - if (file->fd) { - nfs_close(file->device->nfs, file->fd); + if (file && file->fd) { + nfs_close(nfs, file->fd); + file->fd = nullptr; } - std::memset(file, 0, sizeof(*file)); - return r->_errno = 0; + return 0; } -ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) { +ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->device->mutex); // todo: uncomment this when it's fixed upstream. #if 0 - const auto ret = nfs_read(file->device->nfs, file->fd, ptr, len); + const auto ret = nfs_read(nfs, file->fd, ptr, len); if (ret < 0) { - log_write("[NFS] nfs_read() failed: %s errno: %s\n", nfs_get_error(file->device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[NFS] nfs_read() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; } return ret; #else // work around for bug upsteam. - const auto max_read = nfs_get_readmax(file->device->nfs); + const auto max_read = nfs_get_readmax(nfs); size_t bytes_read = 0; while (bytes_read < len) { const auto to_read = std::min(len - bytes_read, max_read); - const auto ret = nfs_read(file->device->nfs, file->fd, ptr, to_read); + const auto ret = nfs_read(nfs, file->fd, ptr, to_read); if (ret < 0) { - log_write("[NFS] nfs_read() failed: %s errno: %s\n", nfs_get_error(file->device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[NFS] nfs_read() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; } ptr += ret; @@ -182,21 +202,20 @@ ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) { #endif } -ssize_t devoptab_write(struct _reent *r, void *fd, const char *ptr, size_t len) { +ssize_t Device::devoptab_write(void *fd, const char *ptr, size_t len) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->device->mutex); // unlike read, writing the max size seems to work fine. - const auto max_write = nfs_get_writemax(file->device->nfs) - 1; + const auto max_write = nfs_get_writemax(nfs); size_t written = 0; while (written < len) { const auto to_write = std::min(len - written, max_write); - const auto ret = nfs_write(file->device->nfs, file->fd, ptr, to_write); + const auto ret = nfs_write(nfs, file->fd, ptr, to_write); if (ret < 0) { - log_write("[NFS] nfs_write() failed: %s errno: %s\n", nfs_get_error(file->device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[NFS] nfs_write() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; } ptr += ret; @@ -210,171 +229,103 @@ ssize_t devoptab_write(struct _reent *r, void *fd, const char *ptr, size_t len) return written; } -off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) { +off_t Device::devoptab_seek(void *fd, off_t pos, int dir) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->device->mutex); - u64 current_off; - const auto ret = nfs_lseek(file->device->nfs, file->fd, pos, dir, ¤t_off); + u64 current_offset = 0; + const auto ret = nfs_lseek(nfs, file->fd, pos, dir, ¤t_offset); if (ret < 0) { - log_write("[NFS] nfs_lseek() failed: %s errno: %s\n", nfs_get_error(file->device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[NFS] nfs_lseek() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; } - r->_errno = 0; - return current_off; + return current_offset; } -int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) { +int Device::devoptab_fstat(void *fd, struct stat *st) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->device->mutex); - const auto ret = nfs_fstat(file->device->nfs, file->fd, st); - if (ret < 0) { - log_write("[NFS] nfs_fstat() failed: %s errno: %s\n", nfs_get_error(file->device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); - } - - return r->_errno = 0; -} - -int devoptab_unlink(struct _reent *r, const char *_path) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!common::fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_nfs(*device)) { - return set_errno(r, EIO); - } - - const auto ret = nfs_unlink(device->nfs, path); - if (ret < 0) { - log_write("[NFS] nfs_unlink() failed: %s errno: %s\n", nfs_get_error(device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); - } - - return r->_errno = 0; -} - -int devoptab_rename(struct _reent *r, const char *_oldName, const char *_newName) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char oldName[PATH_MAX]{}; - if (!common::fix_path(_oldName, oldName)) { - return set_errno(r, ENOENT); - } - - char newName[PATH_MAX]{}; - if (!common::fix_path(_newName, newName)) { - return set_errno(r, ENOENT); - } - - if (!mount_nfs(*device)) { - return set_errno(r, EIO); - } - - const auto ret = nfs_rename(device->nfs, oldName, newName); - if (ret < 0) { - log_write("[NFS] nfs_rename() failed: %s errno: %s\n", nfs_get_error(device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); - } - - return r->_errno = 0; -} - -int devoptab_mkdir(struct _reent *r, const char *_path, int mode) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!common::fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_nfs(*device)) { - return set_errno(r, EIO); - } - - const auto ret = nfs_mkdir(device->nfs, path); + const auto ret = nfs_fstat(nfs, file->fd, st); if (ret) { - log_write("[NFS] nfs_mkdir() failed: %s errno: %s\n", nfs_get_error(device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[NFS] nfs_fstat() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; } - return r->_errno = 0; + return 0; } -int devoptab_rmdir(struct _reent *r, const char *_path) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!common::fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_nfs(*device)) { - return set_errno(r, EIO); - } - - const auto ret = nfs_rmdir(device->nfs, path); +int Device::devoptab_unlink(const char *path) { + const auto ret = nfs_unlink(nfs, path); if (ret) { - log_write("[NFS] nfs_rmdir() failed: %s errno: %s\n", nfs_get_error(device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[NFS] nfs_unlink() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; } - return r->_errno = 0; + return 0; } -DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) { - auto device = static_cast(r->deviceData); - auto dir = static_cast(dirState->dirStruct); - std::memset(dir, 0, sizeof(*dir)); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!common::fix_path(_path, path)) { - set_errno(r, ENOENT); - return nullptr; - } - - if (!mount_nfs(*device)) { - set_errno(r, EIO); - return nullptr; - } - - const auto ret = nfs_opendir(device->nfs, path, &dir->dir); +int Device::devoptab_rename(const char *oldName, const char *newName) { + const auto ret = nfs_rename(nfs, oldName, newName); if (ret) { - log_write("[NFS] nfs_opendir() failed: %s errno: %s\n", nfs_get_error(device->nfs), std::strerror(-ret)); - set_errno(r, -ret); - return nullptr; + log_write("[NFS] nfs_rename() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; } - dir->device = device; - return dirState; + return 0; } -int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { - auto dir = static_cast(dirState->dirStruct); - SCOPED_MUTEX(&dir->device->mutex); +int Device::devoptab_mkdir(const char *path, int mode) { + const auto ret = nfs_mkdir(nfs, path); + if (ret) { + log_write("[NFS] nfs_mkdir() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; + } - nfs_rewinddir(dir->device->nfs, dir->dir); - return r->_errno = 0; + return 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)); - SCOPED_MUTEX(&dir->device->mutex); +int Device::devoptab_rmdir(const char *path) { + const auto ret = nfs_rmdir(nfs, path); + if (ret) { + log_write("[NFS] nfs_rmdir() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; + } - const auto entry = nfs_readdir(dir->device->nfs, dir->dir); + return 0; +} + +int Device::devoptab_diropen(void* fd, const char *path) { + auto dir = static_cast(fd); + + const auto ret = nfs_opendir(nfs, path, &dir->dir); + if (ret) { + log_write("[NFS] nfs_opendir() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; + } + + return 0; +} + +int Device::devoptab_dirreset(void* fd) { + auto dir = static_cast(fd); + if (!dir->dir) { + return -EINVAL; + } + + nfs_rewinddir(nfs, dir->dir); + return 0; +} + +int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) { + auto dir = static_cast(fd); + + if (!dir->dir) { + return EINVAL; + } + + const auto entry = nfs_readdir(nfs, dir->dir); if (!entry) { - return set_errno(r, ENOENT); + return -ENOENT; } std::strncpy(filename, entry->name, NAME_MAX); @@ -394,297 +345,88 @@ int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struc filestat->st_blksize = entry->blksize; filestat->st_blocks = entry->blocks; - return r->_errno = 0; + return 0; } -int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { - auto dir = static_cast(dirState->dirStruct); - SCOPED_MUTEX(&dir->device->mutex); +int Device::devoptab_dirclose(void* fd) { + auto dir = static_cast(fd); - if (dir->dir) { - nfs_closedir(dir->device->nfs, dir->dir); + if (dir && dir->dir) { + nfs_closedir(nfs, dir->dir); + dir->dir = nullptr; } - std::memset(dir, 0, sizeof(*dir)); - return r->_errno = 0; + return 0; } -int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!common::fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_nfs(*device)) { - return set_errno(r, EIO); - } - - const auto ret = nfs_stat(device->nfs, path, st); +int Device::devoptab_lstat(const char *path, struct stat *st) { + const auto ret = nfs_stat(nfs, path, st); if (ret) { - log_write("[NFS] nfs_lstat() failed: %s errno: %s\n", nfs_get_error(device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[NFS] nfs_stat() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; } - return r->_errno = 0; + return 0; } -int devoptab_ftruncate(struct _reent *r, void *fd, off_t len) { +int Device::devoptab_ftruncate(void *fd, off_t len) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->device->mutex); - const auto ret = nfs_ftruncate(file->device->nfs, file->fd, len); + const auto ret = nfs_ftruncate(nfs, file->fd, len); if (ret) { - log_write("[NFS] nfs_ftruncate() failed: %s errno: %s\n", nfs_get_error(file->device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[NFS] nfs_ftruncate() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; } - return r->_errno = 0; + return 0; } -int devoptab_statvfs(struct _reent *r, const char *_path, struct statvfs *buf) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!common::fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_nfs(*device)) { - return set_errno(r, EIO); - } - - const auto ret = nfs_statvfs(device->nfs, path, buf); +int Device::devoptab_statvfs(const char *path, struct statvfs *buf) { + const auto ret = nfs_statvfs(nfs, path, buf); if (ret) { - log_write("[NFS] nfs_statvfs() failed: %s errno: %s\n", nfs_get_error(device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[NFS] nfs_statvfs() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; } - return r->_errno = 0; + return 0; } -int devoptab_fsync(struct _reent *r, void *fd) { +int Device::devoptab_fsync(void *fd) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->device->mutex); - const auto ret = nfs_fsync(file->device->nfs, file->fd); + const auto ret = nfs_fsync(nfs, file->fd); if (ret) { - log_write("[NFS] nfs_fsync() failed: %s errno: %s\n", nfs_get_error(file->device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[NFS] nfs_fsync() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; } - return r->_errno = 0; + return 0; } -int devoptab_utimes(struct _reent *r, const char *_path, const struct timeval times[2]) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - if (!times) { - log_write("[NFS] devoptab_utimes() times is null\n"); - return set_errno(r, EINVAL); - } - - char path[PATH_MAX]{}; - if (!common::fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_nfs(*device)) { - return set_errno(r, EIO); - } - +int Device::devoptab_utimes(const char *path, const struct timeval times[2]) { // todo: nfs should accept const times, pr the fix. struct timeval times_copy[2]; std::memcpy(times_copy, times, sizeof(times_copy)); - const auto ret = nfs_utimes(device->nfs, path, times_copy); + const auto ret = nfs_utimes(nfs, path, times_copy); if (ret) { - log_write("[NFS] nfs_utimes() failed: %s errno: %s\n", nfs_get_error(device->nfs), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[NFS] nfs_utimes() failed: %s errno: %s\n", nfs_get_error(nfs), std::strerror(-ret)); + return ret; } - return r->_errno = 0; + return 0; } -constexpr devoptab_t DEVOPTAB = { - .structSize = sizeof(File), - .open_r = devoptab_open, - .close_r = devoptab_close, - .write_r = devoptab_write, - .read_r = devoptab_read, - .seek_r = devoptab_seek, - .fstat_r = devoptab_fstat, - .stat_r = devoptab_lstat, - .unlink_r = devoptab_unlink, - .rename_r = devoptab_rename, - .mkdir_r = devoptab_mkdir, - .dirStateSize = sizeof(Dir), - .diropen_r = devoptab_diropen, - .dirreset_r = devoptab_dirreset, - .dirnext_r = devoptab_dirnext, - .dirclose_r = devoptab_dirclose, - .statvfs_r = devoptab_statvfs, - .ftruncate_r = devoptab_ftruncate, - .fsync_r = devoptab_fsync, - .rmdir_r = devoptab_rmdir, - .lstat_r = devoptab_lstat, - .utimes_r = devoptab_utimes, -}; - -struct Entry { - Device device{}; - devoptab_t devoptab{}; - fs::FsPath mount{}; - char name[32]{}; - s32 ref_count{}; - - ~Entry() { - if (device.nfs) { - if (device.mounted) { - nfs_umount(device.nfs); - } - - if (device.url) { - nfs_destroy_url(device.url); - } - - nfs_destroy_context(device.nfs); - } - - RemoveDevice(mount); - } -}; - -Mutex g_mutex; -std::array, common::MAX_ENTRIES> g_entries; - } // namespace Result MountNfsAll() { - SCOPED_MUTEX(&g_mutex); - - static const auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int { - auto e = static_cast(UserData); - if (!Section || !Key || !Value) { - return 1; - } - - // add new entry if use section changed. - if (e->empty() || std::strcmp(Section, e->back().name.c_str())) { - e->emplace_back(Section); - } - - if (!std::strcmp(Key, "url")) { - e->back().url = Value; - } else if (!std::strcmp(Key, "name")) { - e->back().name = Value; - } else if (!std::strcmp(Key, "uid")) { - e->back().uid = ini_parse_getl(Value, e->back().uid); - } else if (!std::strcmp(Key, "gid")) { - e->back().gid = ini_parse_getl(Value, e->back().gid); - } else if (!std::strcmp(Key, "version")) { - e->back().version = ini_parse_getl(Value, e->back().version); - } else if (!std::strcmp(Key, "timeout")) { - e->back().timeout = ini_parse_getl(Value, e->back().timeout); - } else if (!std::strcmp(Key, "read_only")) { - e->back().read_only = ini_parse_getbool(Value, e->back().read_only); - } else if (!std::strcmp(Key, "no_stat_file")) { - e->back().no_stat_file = ini_parse_getbool(Value, e->back().no_stat_file); - } else if (!std::strcmp(Key, "no_stat_dir")) { - e->back().no_stat_dir = ini_parse_getbool(Value, e->back().no_stat_dir); - } else { - log_write("[NFS] INI: Unknown key %s=%s\n", Key, Value); - } - - return 1; - }; - - NfsMountConfigs configs; - ini_browse(cb, &configs, "/config/sphaira/nfs.ini"); - log_write("[NFS] Found %zu mount configs\n", configs.size()); - - for (const auto& config : configs) { - // check if we already have the http mounted. - bool already_mounted = false; - for (const auto& entry : g_entries) { - if (entry && entry->mount == config.name) { - already_mounted = true; - break; - } - } - - if (already_mounted) { - log_write("[NFS] Already mounted %s, skipping\n", config.name.c_str()); - continue; - } - - // otherwise, find next free entry. - auto itr = std::ranges::find_if(g_entries, [](auto& e){ - return !e; - }); - - if (itr == g_entries.end()) { - log_write("[NFS] No free entries to mount %s\n", config.name.c_str()); - break; - } - - auto entry = std::make_unique(); - - entry->devoptab = DEVOPTAB; - entry->devoptab.name = entry->name; - entry->devoptab.deviceData = &entry->device; - entry->device.config = config; - std::snprintf(entry->name, sizeof(entry->name), "[NFS] %s", config.name.c_str()); - std::snprintf(entry->mount, sizeof(entry->mount), "[NFS] %s:/", config.name.c_str()); - common::update_devoptab_for_read_only(&entry->devoptab, config.read_only); - - R_UNLESS(AddDevice(&entry->devoptab) >= 0, 0x1); - log_write("[NFS] DEVICE SUCCESS %s %s\n", entry->device.config.url.c_str(), entry->name); - - entry->ref_count++; - *itr = std::move(entry); - log_write("[NFS] Mounted %s at /%s\n", config.url.c_str(), config.name.c_str()); - } - - R_SUCCEED(); -} - -void UnmountNfsAll() { - SCOPED_MUTEX(&g_mutex); - - for (auto& entry : g_entries) { - if (entry) { - entry.reset(); - } - } -} - -Result GetNfsMounts(location::StdioEntries& out) { - SCOPED_MUTEX(&g_mutex); - out.clear(); - - for (const auto& entry : g_entries) { - if (entry) { - u32 flags = 0; - if (entry->device.config.read_only) { - flags |= location::FsEntryFlag::FsEntryFlag_ReadOnly; - } - if (entry->device.config.no_stat_file) { - flags |= location::FsEntryFlag::FsEntryFlag_NoStatFile; - } - if (entry->device.config.no_stat_dir) { - flags |= location::FsEntryFlag::FsEntryFlag_NoStatDir; - } - - out.emplace_back(entry->mount, entry->name, flags); - } - } - - R_SUCCEED(); + return common::MountNetworkDevice([](const common::MountConfig& cfg) { + return std::make_unique(cfg); + }, + sizeof(File), sizeof(Dir), + "/config/sphaira/nfs.ini", + "NFS" + ); } } // namespace sphaira::devoptab diff --git a/sphaira/source/utils/devoptab_smb2.cpp b/sphaira/source/utils/devoptab_smb2.cpp index 8e2c3f6..e7bd9c2 100644 --- a/sphaira/source/utils/devoptab_smb2.cpp +++ b/sphaira/source/utils/devoptab_smb2.cpp @@ -1,9 +1,7 @@ #include "utils/devoptab_common.hpp" #include "defines.hpp" #include "log.hpp" -#include "location.hpp" -#include #include #include #include @@ -16,91 +14,48 @@ namespace sphaira::devoptab { namespace { -constexpr int DEFAULT_SMB2_TIMEOUT = 3000; // 3 seconds. +struct Device final : common::MountDevice { + Device(const common::MountConfig& cfg) : MountDevice{cfg} {} + ~Device(); -struct Smb2MountConfig { - std::string name{}; - std::string url{}; - std::string user{}; - std::string pass{}; - std::string domain{}; - std::string workstation{}; - int timeout{DEFAULT_SMB2_TIMEOUT}; - bool read_only{}; - bool no_stat_file{false}; - bool no_stat_dir{true}; -}; -using Smb2MountConfigs = std::vector; +private: + bool fix_path(const char* str, char* out, bool strip_leading_slash = false) override { + return common::fix_path(str, out, true); + } -struct Device { + bool Mount() override; + 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_write(void *fd, const char *ptr, size_t len) override; + off_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; + +private: smb2_context* smb2{}; - smb2_url* url{}; - Smb2MountConfig config; bool mounted{}; - Mutex mutex{}; }; struct File { - Device* device; smb2fh* fd; }; struct Dir { - Device* device; smb2dir* dir; }; -bool mount_smb2(Device& device) { - if (device.mounted) { - return true; - } - - if (!device.smb2) { - device.smb2 = smb2_init_context(); - if (!device.smb2) { - log_write("[SMB2] smb2_init_context() failed\n"); - return false; - } - - smb2_set_security_mode(device.smb2, SMB2_NEGOTIATE_SIGNING_ENABLED); - - if (!device.config.user.empty()) { - smb2_set_user(device.smb2, device.config.user.c_str()); - } - - if (!device.config.pass.empty()) { - smb2_set_password(device.smb2, device.config.pass.c_str()); - } - - if (!device.config.domain.empty()) { - smb2_set_domain(device.smb2, device.config.domain.c_str()); - } - - if (!device.config.workstation.empty()) { - smb2_set_workstation(device.smb2, device.config.workstation.c_str()); - } - - smb2_set_timeout(device.smb2, device.config.timeout); - } - - if (!device.url) { - device.url = smb2_parse_url(device.smb2, device.config.url.c_str()); - if (!device.url) { - log_write("[SMB2] smb2_parse_url() failed: %s\n", smb2_get_error(device.smb2)); - return false; - } - } - - const auto ret = smb2_connect_share(device.smb2, device.url->server, device.url->share, device.url->user); - if (ret) { - log_write("[SMB2] smb2_connect_share() failed: %s errno: %s\n", smb2_get_error(device.smb2), std::strerror(-ret)); - return false; - } - - device.mounted = true; - return true; -} - void fill_stat(struct stat* st, const smb2_stat_64* smb2_st) { if (smb2_st->smb2_type == SMB2_TYPE_FILE) { st->st_mode = S_IFREG; @@ -121,325 +76,256 @@ void fill_stat(struct stat* st, const smb2_stat_64* smb2_st) { st->st_ctime = smb2_st->smb2_ctime; } -bool fix_path(const char* str, char* out) { - return common::fix_path(str, out, true); +Device::~Device() { + if (this->smb2) { + if (this->mounted) { + smb2_disconnect_share(this->smb2); + } + + smb2_destroy_context(this->smb2); + } } -int set_errno(struct _reent *r, int err) { - r->_errno = err; - return -1; +bool Device::Mount() { + if (mounted) { + return true; + } + + if (!this->smb2) { + this->smb2 = smb2_init_context(); + if (!this->smb2) { + log_write("[SMB2] smb2_init_context() failed\n"); + return false; + } + + smb2_set_security_mode(this->smb2, SMB2_NEGOTIATE_SIGNING_ENABLED); + + if (!this->config.user.empty()) { + smb2_set_user(this->smb2, this->config.user.c_str()); + } + + if (!this->config.pass.empty()) { + smb2_set_password(this->smb2, this->config.pass.c_str()); + } + + const auto domain = this->config.extra.find("domain"); + if (domain != this->config.extra.end()) { + smb2_set_domain(this->smb2, domain->second.c_str()); + } + + const auto workstation = this->config.extra.find("workstation"); + if (workstation != this->config.extra.end()) { + smb2_set_workstation(this->smb2, workstation->second.c_str()); + } + + smb2_set_timeout(this->smb2, this->config.timeout); + } + + auto smb2_url = smb2_parse_url(this->smb2, this->config.url.c_str()); + if (!smb2_url) { + log_write("[SMB2] smb2_parse_url() failed: %s\n", smb2_get_error(this->smb2)); + return false; + } + ON_SCOPE_EXIT(smb2_destroy_url(smb2_url)); + + const auto ret = smb2_connect_share(this->smb2, smb2_url->server, smb2_url->share, smb2_url->user); + if (ret) { + log_write("[SMB2] smb2_connect_share() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret)); + return false; + } + + this->mounted = true; + return true; } -int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) { - auto device = static_cast(r->deviceData); +int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) { auto file = static_cast(fileStruct); - std::memset(file, 0, sizeof(*file)); - SCOPED_MUTEX(&device->mutex); - if (device->config.read_only && (flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND))) { - return set_errno(r, EROFS); - } - - char path[PATH_MAX]{}; - if (!fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_smb2(*device)) { - return set_errno(r, EIO); - } - - file->fd = smb2_open(device->smb2, path, flags); + file->fd = smb2_open(this->smb2, path, flags); if (!file->fd) { - log_write("[SMB2] smb2_open() failed: %s\n", smb2_get_error(device->smb2)); - return set_errno(r, EIO); + log_write("[SMB2] smb2_open() failed: %s\n", smb2_get_error(this->smb2)); + return -EIO; } - file->device = device; - return r->_errno = 0; + return 0; } -int devoptab_close(struct _reent *r, void *fd) { +int Device::devoptab_close(void *fd) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->device->mutex); - if (file->fd) { - smb2_close(file->device->smb2, file->fd); - } - - std::memset(file, 0, sizeof(*file)); - return r->_errno = 0; + smb2_close(this->smb2, file->fd); + return 0; } -ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) { +ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->device->mutex); - const auto ret = smb2_read(file->device->smb2, file->fd, reinterpret_cast(ptr), len); + const auto ret = smb2_read(this->smb2, file->fd, reinterpret_cast(ptr), len); if (ret < 0) { - log_write("[SMB2] smb2_read() failed: %s errno: %s\n", smb2_get_error(file->device->smb2), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[SMB2] smb2_read() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret)); + return ret; } return ret; } -ssize_t devoptab_write(struct _reent *r, void *fd, const char *ptr, size_t len) { +ssize_t Device::devoptab_write(void *fd, const char *ptr, size_t len) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->device->mutex); - const auto ret = smb2_write(file->device->smb2, file->fd, reinterpret_cast(ptr), len); + const auto ret = smb2_write(this->smb2, file->fd, reinterpret_cast(ptr), len); if (ret < 0) { - log_write("[SMB2] smb2_write() failed: %s errno: %s\n", smb2_get_error(file->device->smb2), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[SMB2] smb2_write() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret)); + return ret; } return ret; } -off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) { +off_t Device::devoptab_seek(void *fd, off_t pos, int dir) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->device->mutex); - const auto ret = smb2_lseek(file->device->smb2, file->fd, pos, dir, nullptr); + u64 current_offset = 0; + const auto ret = smb2_lseek(this->smb2, file->fd, pos, dir, ¤t_offset); if (ret < 0) { - log_write("[SMB2] smb2_lseek() failed: %s errno: %s\n", smb2_get_error(file->device->smb2), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[SMB2] smb2_lseek() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret)); + return ret; } - r->_errno = 0; - return ret; + return current_offset; } -int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) { +int Device::devoptab_fstat(void *fd, struct stat *st) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->device->mutex); smb2_stat_64 smb2_st{}; - const auto ret = smb2_fstat(file->device->smb2, file->fd, &smb2_st); + const auto ret = smb2_fstat(this->smb2, file->fd, &smb2_st); if (ret < 0) { - log_write("[SMB2] smb2_fstat() failed: %s errno: %s\n", smb2_get_error(file->device->smb2), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[SMB2] smb2_fstat() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret)); + return ret; } fill_stat(st, &smb2_st); - return r->_errno = 0; + return 0; } -int devoptab_unlink(struct _reent *r, const char *_path) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_smb2(*device)) { - return set_errno(r, EIO); - } - - const auto ret = smb2_unlink(device->smb2, path); - if (ret < 0) { - log_write("[SMB2] smb2_unlink() failed: %s errno: %s\n", smb2_get_error(device->smb2), std::strerror(-ret)); - return set_errno(r, -ret); - } - - return r->_errno = 0; -} - -int devoptab_rename(struct _reent *r, const char *_oldName, const char *_newName) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char oldName[PATH_MAX]{}; - if (!fix_path(_oldName, oldName)) { - return set_errno(r, ENOENT); - } - - char newName[PATH_MAX]{}; - if (!fix_path(_newName, newName)) { - return set_errno(r, ENOENT); - } - - if (!mount_smb2(*device)) { - return set_errno(r, EIO); - } - - const auto ret = smb2_rename(device->smb2, oldName, newName); - if (ret < 0) { - log_write("[SMB2] smb2_rename() failed: %s errno: %s\n", smb2_get_error(device->smb2), std::strerror(-ret)); - return set_errno(r, -ret); - } - - return r->_errno = 0; -} - -int devoptab_mkdir(struct _reent *r, const char *_path, int mode) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_smb2(*device)) { - return set_errno(r, EIO); - } - - const auto ret = smb2_mkdir(device->smb2, path); +int Device::devoptab_unlink(const char *path) { + const auto ret = smb2_unlink(this->smb2, path); if (ret) { - log_write("[SMB2] smb2_mkdir() failed: %s errno: %s\n", smb2_get_error(device->smb2), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[SMB2] smb2_unlink() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret)); + return ret; } - return r->_errno = 0; + return 0; } -int devoptab_rmdir(struct _reent *r, const char *_path) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_smb2(*device)) { - return set_errno(r, EIO); - } - - const auto ret = smb2_rmdir(device->smb2, path); +int Device::devoptab_rename(const char *oldName, const char *newName) { + const auto ret = smb2_rename(this->smb2, oldName, newName); if (ret) { - log_write("[SMB2] smb2_rmdir() failed: %s errno: %s\n", smb2_get_error(device->smb2), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[SMB2] smb2_rename() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret)); + return ret; } - return r->_errno = 0; + return 0; } -DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) { - auto device = static_cast(r->deviceData); - auto dir = static_cast(dirState->dirStruct); - std::memset(dir, 0, sizeof(*dir)); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!fix_path(_path, path)) { - set_errno(r, ENOENT); - return nullptr; +int Device::devoptab_mkdir(const char *path, int mode) { + const auto ret = smb2_mkdir(this->smb2, path); + if (ret) { + log_write("[SMB2] smb2_mkdir() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret)); + return ret; } - if (!mount_smb2(*device)) { - set_errno(r, EIO); - return nullptr; + return 0; +} + +int Device::devoptab_rmdir(const char *path) { + const auto ret = smb2_rmdir(this->smb2, path); + if (ret) { + log_write("[SMB2] smb2_rmdir() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret)); + return ret; } - dir->dir = smb2_opendir(device->smb2, path); + return 0; +} + +int Device::devoptab_diropen(void* fd, const char *path) { + auto dir = static_cast(fd); + + dir->dir = smb2_opendir(this->smb2, path); if (!dir->dir) { - log_write("[SMB2] smb2_opendir() failed: %s\n", smb2_get_error(device->smb2)); - set_errno(r, EIO); - return nullptr; + log_write("[SMB2] smb2_opendir() failed: %s\n", smb2_get_error(this->smb2)); + return -EIO; } - dir->device = device; - return dirState; + return 0; } -int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) { - auto dir = static_cast(dirState->dirStruct); - SCOPED_MUTEX(&dir->device->mutex); +int Device::devoptab_dirreset(void* fd) { + auto dir = static_cast(fd); + if (!dir->dir) { + return -EINVAL; + } - smb2_rewinddir(dir->device->smb2, dir->dir); - return r->_errno = 0; + smb2_rewinddir(this->smb2, dir->dir); + return 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)); - SCOPED_MUTEX(&dir->device->mutex); +int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) { + auto dir = static_cast(fd); - const auto entry = smb2_readdir(dir->device->smb2, dir->dir); + if (!dir->dir) { + return EINVAL; + } + + const auto entry = smb2_readdir(this->smb2, dir->dir); if (!entry) { - return set_errno(r, ENOENT); + return -ENOENT; } std::strncpy(filename, entry->name, NAME_MAX); filename[NAME_MAX - 1] = '\0'; fill_stat(filestat, &entry->st); - return r->_errno = 0; + return 0; } -int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) { - auto dir = static_cast(dirState->dirStruct); - SCOPED_MUTEX(&dir->device->mutex); +int Device::devoptab_dirclose(void* fd) { + auto dir = static_cast(fd); - if (dir->dir) { - smb2_closedir(dir->device->smb2, dir->dir); - } - - std::memset(dir, 0, sizeof(*dir)); - return r->_errno = 0; + smb2_closedir(this->smb2, dir->dir); + return 0; } -int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) { - auto device = static_cast(r->deviceData); - std::memset(st, 0, sizeof(*st)); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_smb2(*device)) { - return set_errno(r, EIO); - } - +int Device::devoptab_lstat(const char *path, struct stat *st) { smb2_stat_64 smb2_st{}; - const auto ret = smb2_stat(device->smb2, path, &smb2_st); + const auto ret = smb2_stat(this->smb2, path, &smb2_st); if (ret) { - log_write("[SMB2] smb2_lstat() failed: %s errno: %s\n", smb2_get_error(device->smb2), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[SMB2] smb2_stat() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret)); + return ret; } fill_stat(st, &smb2_st); - return r->_errno = 0; + return 0; } -int devoptab_ftruncate(struct _reent *r, void *fd, off_t len) { +int Device::devoptab_ftruncate(void *fd, off_t len) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->device->mutex); - const auto ret = smb2_ftruncate(file->device->smb2, file->fd, len); + const auto ret = smb2_ftruncate(this->smb2, file->fd, len); if (ret) { - log_write("[SMB2] smb2_ftruncate() failed: %s errno: %s\n", smb2_get_error(file->device->smb2), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[SMB2] smb2_ftruncate() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret)); + return ret; } - return r->_errno = 0; + return 0; } -int devoptab_statvfs(struct _reent *r, const char *_path, struct statvfs *buf) { - auto device = static_cast(r->deviceData); - SCOPED_MUTEX(&device->mutex); - - char path[PATH_MAX]{}; - if (!fix_path(_path, path)) { - return set_errno(r, ENOENT); - } - - if (!mount_smb2(*device)) { - return set_errno(r, EIO); - } - +int Device::devoptab_statvfs(const char *path, struct statvfs *buf) { struct smb2_statvfs smb2_st{}; - const auto ret = smb2_statvfs(device->smb2, path, &smb2_st); + const auto ret = smb2_statvfs(this->smb2, path, &smb2_st); if (ret) { - log_write("[SMB2] smb2_statvfs() failed: %s errno: %s\n", smb2_get_error(device->smb2), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[SMB2] smb2_statvfs() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret)); + return ret; } buf->f_bsize = smb2_st.f_bsize; @@ -454,198 +340,31 @@ int devoptab_statvfs(struct _reent *r, const char *_path, struct statvfs *buf) { buf->f_flag = smb2_st.f_flag; buf->f_namemax = smb2_st.f_namemax; - return r->_errno = 0; + return 0; } -int devoptab_fsync(struct _reent *r, void *fd) { +int Device::devoptab_fsync(void *fd) { auto file = static_cast(fd); - SCOPED_MUTEX(&file->device->mutex); - const auto ret = smb2_fsync(file->device->smb2, file->fd); + const auto ret = smb2_fsync(this->smb2, file->fd); if (ret) { - log_write("[SMB2] smb2_fsync() failed: %s errno: %s\n", smb2_get_error(file->device->smb2), std::strerror(-ret)); - return set_errno(r, -ret); + log_write("[SMB2] smb2_fsync() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret)); + return ret; } - return r->_errno = 0; + return 0; } -constexpr devoptab_t DEVOPTAB = { - .structSize = sizeof(File), - .open_r = devoptab_open, - .close_r = devoptab_close, - .write_r = devoptab_write, - .read_r = devoptab_read, - .seek_r = devoptab_seek, - .fstat_r = devoptab_fstat, - .stat_r = devoptab_lstat, - .unlink_r = devoptab_unlink, - .rename_r = devoptab_rename, - .mkdir_r = devoptab_mkdir, - .dirStateSize = sizeof(Dir), - .diropen_r = devoptab_diropen, - .dirreset_r = devoptab_dirreset, - .dirnext_r = devoptab_dirnext, - .dirclose_r = devoptab_dirclose, - .statvfs_r = devoptab_statvfs, - .ftruncate_r = devoptab_ftruncate, - .fsync_r = devoptab_fsync, - .rmdir_r = devoptab_rmdir, - .lstat_r = devoptab_lstat, -}; - -struct Entry { - Device device{}; - devoptab_t devoptab{}; - fs::FsPath mount{}; - char name[32]{}; - s32 ref_count{}; - - ~Entry() { - if (device.smb2) { - if (device.mounted) { - smb2_disconnect_share(device.smb2); - } - - if (device.url) { - smb2_destroy_url(device.url); - } - - smb2_destroy_context(device.smb2); - } - - RemoveDevice(mount); - } -}; - -Mutex g_mutex; -std::array, common::MAX_ENTRIES> g_entries; - } // namespace Result MountSmb2All() { - SCOPED_MUTEX(&g_mutex); - - static const auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int { - auto e = static_cast(UserData); - if (!Section || !Key || !Value) { - return 1; - } - - // add new entry if use section changed. - if (e->empty() || std::strcmp(Section, e->back().name.c_str())) { - e->emplace_back(Section); - } - - if (!std::strcmp(Key, "url")) { - e->back().url = Value; - } else if (!std::strcmp(Key, "name")) { - e->back().name = Value; - } else if (!std::strcmp(Key, "user")) { - e->back().user = Value; - } else if (!std::strcmp(Key, "pass")) { - e->back().pass = Value; - } else if (!std::strcmp(Key, "domain")) { - e->back().domain = Value; - } else if (!std::strcmp(Key, "workstation")) { - e->back().workstation = Value; - } else if (!std::strcmp(Key, "timeout")) { - e->back().timeout = ini_parse_getl(Value, e->back().timeout); - } else if (!std::strcmp(Key, "read_only")) { - e->back().read_only = ini_parse_getbool(Value, e->back().read_only); - } else if (!std::strcmp(Key, "no_stat_file")) { - e->back().no_stat_file = ini_parse_getbool(Value, e->back().no_stat_file); - } else if (!std::strcmp(Key, "no_stat_dir")) { - e->back().no_stat_dir = ini_parse_getbool(Value, e->back().no_stat_dir); - } else { - log_write("[SMB2] INI: Unknown key %s=%s\n", Key, Value); - } - - return 1; - }; - - Smb2MountConfigs configs{}; - ini_browse(cb, &configs, "/config/sphaira/smb.ini"); - log_write("[SMB2] Found %zu mount configs\n", configs.size()); - - for (const auto& config : configs) { - // check if we already have the http mounted. - bool already_mounted = false; - for (const auto& entry : g_entries) { - if (entry && entry->mount == config.name) { - already_mounted = true; - break; - } - } - - if (already_mounted) { - log_write("[SMB2] Already mounted %s, skipping\n", config.name.c_str()); - continue; - } - - // otherwise, find next free entry. - auto itr = std::ranges::find_if(g_entries, [](auto& e){ - return !e; - }); - - if (itr == g_entries.end()) { - log_write("[SMB2] No free entries to mount %s\n", config.name.c_str()); - break; - } - - auto entry = std::make_unique(); - - entry->devoptab = DEVOPTAB; - entry->devoptab.name = entry->name; - entry->devoptab.deviceData = &entry->device; - entry->device.config = config; - std::snprintf(entry->name, sizeof(entry->name), "[SMB] %s", config.name.c_str()); - std::snprintf(entry->mount, sizeof(entry->mount), "[SMB] %s:/", config.name.c_str()); - common::update_devoptab_for_read_only(&entry->devoptab, config.read_only); - - R_UNLESS(AddDevice(&entry->devoptab) >= 0, 0x1); - log_write("[SMB2] DEVICE SUCCESS %s %s\n", entry->device.config.url.c_str(), entry->name); - - entry->ref_count++; - *itr = std::move(entry); - log_write("[SMB2] Mounted %s at /%s\n", config.user.c_str(), config.name.c_str()); - } - - R_SUCCEED(); -} - -void UnmountSmb2All() { - SCOPED_MUTEX(&g_mutex); - - for (auto& entry : g_entries) { - if (entry) { - entry.reset(); - } - } -} - -Result GetSmb2Mounts(location::StdioEntries& out) { - SCOPED_MUTEX(&g_mutex); - out.clear(); - - for (const auto& entry : g_entries) { - if (entry) { - u32 flags = 0; - if (entry->device.config.read_only) { - flags |= location::FsEntryFlag::FsEntryFlag_ReadOnly; - } - if (entry->device.config.no_stat_file) { - flags |= location::FsEntryFlag::FsEntryFlag_NoStatFile; - } - if (entry->device.config.no_stat_dir) { - flags |= location::FsEntryFlag::FsEntryFlag_NoStatDir; - } - - out.emplace_back(entry->mount, entry->name, flags); - } - } - - R_SUCCEED(); + return common::MountNetworkDevice([](const common::MountConfig& cfg) { + return std::make_unique(cfg); + }, + sizeof(File), sizeof(Dir), + "/config/sphaira/smb.ini", + "SMB" + ); } } // namespace sphaira::devoptab diff --git a/sphaira/source/utils/devoptab_webdav.cpp b/sphaira/source/utils/devoptab_webdav.cpp new file mode 100644 index 0000000..e45baac --- /dev/null +++ b/sphaira/source/utils/devoptab_webdav.cpp @@ -0,0 +1,581 @@ +#include "utils/devoptab_common.hpp" +#include "utils/profile.hpp" + +#include "log.hpp" +#include "defines.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include + +// todo: try to reduce binary size by using a smaller xml parser. +#include + +namespace sphaira::devoptab { +namespace { + +constexpr const char* XPATH_RESPONSE = "//*[local-name()='response']"; +constexpr const char* XPATH_HREF = ".//*[local-name()='href']"; +constexpr const char* XPATH_PROPSTAT_PROP = ".//*[local-name()='propstat']/*[local-name()='prop']"; +constexpr const char* XPATH_PROP = ".//*[local-name()='prop']"; +constexpr const char* XPATH_RESOURCETYPE = ".//*[local-name()='resourcetype']"; +constexpr const char* XPATH_COLLECTION = ".//*[local-name()='collection']"; + +struct DirEntry { + std::string name{}; + bool is_dir{}; +}; +using DirEntries = std::vector; + +struct FileEntry { + std::string path{}; + struct stat st{}; +}; + +struct File { + FileEntry* entry; + common::PushPullThreadData* push_pull_thread_data; + size_t off; + size_t last_off; + bool write_mode; +}; + +struct Dir { + DirEntries* entries; + size_t index; +}; + +struct Device final : common::MountCurlDevice { + using MountCurlDevice::MountCurlDevice; + +private: + 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_write(void *fd, const char *ptr, size_t len) override; + off_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_fsync(void *fd) override; + + std::pair webdav_custom_command(const std::string& path, const std::string& cmd, std::string_view postfields, std::span headers, bool is_dir, std::vector* response_data = nullptr); + bool webdav_dirlist(const std::string& path, DirEntries& out); + bool webdav_stat(const std::string& path, struct stat* st, bool is_dir); + bool webdav_remove_file_folder(const std::string& path, bool is_dir); + bool webdav_unlink(const std::string& path); + bool webdav_rename(const std::string& old_path, const std::string& new_path, bool is_dir); + bool webdav_mkdir(const std::string& path); + bool webdav_rmdir(const std::string& path); +}; + +size_t dummy_data_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { + return size * nmemb; +} + +std::pair Device::webdav_custom_command(const std::string& path, const std::string& cmd, std::string_view postfields, std::span headers, bool is_dir, std::vector* response_data) { + const auto url = build_url(path, is_dir); + + curl_slist* header_list{}; + ON_SCOPE_EXIT(curl_slist_free_all(header_list)); + + for (const auto& header : headers) { + log_write("[WEBDAV] Header: %s\n", header.c_str()); + header_list = curl_slist_append(header_list, header.c_str()); + } + + log_write("[WEBDAV] %s %s\n", cmd.c_str(), url.c_str()); + curl_set_common_options(this->curl, url); + curl_easy_setopt(this->curl, CURLOPT_HTTPHEADER, header_list); + curl_easy_setopt(this->curl, CURLOPT_CUSTOMREQUEST, cmd.c_str()); + if (!postfields.empty()) { + log_write("[WEBDAV] Post fields: %.*s\n", (int)postfields.length(), postfields.data()); + curl_easy_setopt(this->curl, CURLOPT_POSTFIELDS, postfields.data()); + curl_easy_setopt(this->curl, CURLOPT_POSTFIELDSIZE, (long)postfields.length()); + } + + if (response_data) { + response_data->clear(); + curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, write_memory_callback); + curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, (void *)response_data); + } else { + curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, dummy_data_callback); + } + + const auto res = curl_easy_perform(this->curl); + if (res != CURLE_OK) { + log_write("[WEBDAV] curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); + return {false, 0}; + } + + long response_code = 0; + curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code); + return {true, response_code}; +} + +bool Device::webdav_dirlist(const std::string& path, DirEntries& out) { + const std::string_view post_fields = + "" + "" + "" + // "" + "" + "" + ""; + + const std::string custom_headers[] = { + "Content-Type: application/xml; charset=utf-8", + "Depth: 1" + }; + + std::vector chunk; + const auto [success, response_code] = webdav_custom_command(path, "PROPFIND", post_fields, custom_headers, true, &chunk); + if (!success || response_code != 207) { + log_write("[WEBDAV] PROPFIND failed or returned HTTP error code: %ld\n", response_code); + return false; + } + + SCOPED_TIMESTAMP("webdav_dirlist parse"); + + pugi::xml_document doc; + const auto result = doc.load_buffer_inplace(chunk.data(), chunk.size()); + if (!result) { + log_write("[WEBDAV] Failed to parse XML: %s\n", result.description()); + return false; + } + + log_write("\n[WEBDAV] XML parsed successfully\n"); + + auto requested_path = url_decode(path); + if (!requested_path.empty() && requested_path.back() == '/') { + requested_path.pop_back(); + } + + const auto responses = doc.select_nodes(XPATH_RESPONSE); + + for (const auto& rnode : responses) { + const auto response = rnode.node(); + if (!response) { + continue; + } + + const auto href_x = response.select_node(XPATH_HREF); + if (!href_x) { + continue; + } + + const auto href = url_decode(href_x.node().text().as_string()); + if (href.empty() || href == requested_path || href == requested_path + '/') { + continue; + } + + // propstat/prop/resourcetype + auto prop_x = response.select_node(XPATH_PROPSTAT_PROP); + if (!prop_x) { + // try direct prop if structure differs + prop_x = response.select_node(XPATH_PROP); + if (!prop_x) { + continue; + } + } + + const auto prop = prop_x.node(); + const auto rtype_x = prop.select_node(XPATH_RESOURCETYPE); + bool is_dir = false; + if (rtype_x && rtype_x.node().select_node(XPATH_COLLECTION)) { + is_dir = true; + } + + auto name = href; + if (!name.empty() && name.back() == '/') { + name.pop_back(); + } + + const auto pos = name.find_last_of('/'); + if (pos != std::string::npos) { + name = name.substr(pos + 1); + } + + // skip root entry + if (name.empty() || name == ".") { + continue; + } + + out.emplace_back(name, is_dir); + } + + log_write("[WEBDAV] Parsed %zu entries from directory listing\n", out.size()); + + return true; +} + +// todo: use PROPFIND to get file size and time, although it is slower... +bool Device::webdav_stat(const std::string& path, struct stat* st, bool is_dir) { + std::memset(st, 0, sizeof(*st)); + const auto url = build_url(path, is_dir); + + curl_set_common_options(this->curl, url); + curl_easy_setopt(this->curl, CURLOPT_NOBODY, 1L); + curl_easy_setopt(this->curl, CURLOPT_FILETIME, 1L); + + const auto res = curl_easy_perform(this->curl); + if (res != CURLE_OK) { + log_write("[WEBDAV] curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); + return false; + } + + long response_code = 0; + curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code); + + curl_off_t file_size = 0; + curl_easy_getinfo(this->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &file_size); + + curl_off_t file_time = 0; + curl_easy_getinfo(this->curl, CURLINFO_FILETIME_T, &file_time); + + const char* content_type{}; + curl_easy_getinfo(this->curl, CURLINFO_CONTENT_TYPE, &content_type); + + const char* effective_url{}; + curl_easy_getinfo(this->curl, CURLINFO_EFFECTIVE_URL, &effective_url); + + // handle error codes. + if (response_code != 200 && response_code != 206) { + log_write("[WEBDAV] Unexpected HTTP response code: %ld\n", response_code); + return false; + } + + if (effective_url) { + if (std::string_view{effective_url}.ends_with('/')) { + is_dir = true; + } + } + + if (content_type && !std::strcmp(content_type, "text/html")) { + is_dir = true; + } + + if (is_dir) { + st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + } else { + st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + st->st_size = file_size > 0 ? file_size : 0; + } + + st->st_mtime = file_time > 0 ? file_time : 0; + st->st_atime = st->st_mtime; + st->st_ctime = st->st_mtime; + st->st_nlink = 1; + + return true; +} + +bool Device::webdav_remove_file_folder(const std::string& path, bool is_dir) { + const auto [success, response_code] = webdav_custom_command(path, "DELETE", "", {}, is_dir); + if (!success || (response_code != 200 && response_code != 204)) { + log_write("[WEBDAV] DELETE command failed with response code: %ld\n", response_code); + return false; + } + + return true; +} + +bool Device::webdav_unlink(const std::string& path) { + return webdav_remove_file_folder(path, false); +} + +bool Device::webdav_rename(const std::string& old_path, const std::string& new_path, bool is_dir) { + log_write("[WEBDAV] Renaming %s to %s\n", old_path.c_str(), new_path.c_str()); + + const std::string custom_headers[] = { + "Destination: " + build_url(new_path, is_dir), + "Overwrite: T", + }; + + const auto [success, response_code] = webdav_custom_command(old_path, "MOVE", "", custom_headers, is_dir); + if (!success || (response_code != 200 && response_code != 201 && response_code != 204)) { + log_write("[WEBDAV] MOVE command failed with response code: %ld\n", response_code); + return false; + } + + return true; +} + +bool Device::webdav_mkdir(const std::string& path) { + const auto [success, response_code] = webdav_custom_command(path, "MKCOL", "", {}, true); + if (!success || response_code != 201) { + log_write("[WEBDAV] MKCOL command failed with response code: %ld\n", response_code); + return false; + } + + return true; +} + +bool Device::webdav_rmdir(const std::string& path) { + return webdav_remove_file_folder(path, true); +} + +int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) { + auto file = static_cast(fileStruct); + struct stat st{}; + + // append mode is not supported. + if (flags & O_APPEND) { + return -E2BIG; + } + + if ((flags & O_ACCMODE) == O_RDONLY) { + // ensure the file exists and get its size. + if (!webdav_stat(path, &st, false)) { + log_write("[WEBDAV] File not found: %s\n", path); + return -ENOENT; + } + + if (st.st_mode & S_IFDIR) { + log_write("[WEBDAV] Path is a directory, not a file: %s\n", path); + return -EISDIR; + } + } + + log_write("[WEBDAV] Opening file: %s\n", path); + file->entry = new FileEntry{path, st}; + file->write_mode = (flags & (O_WRONLY | O_RDWR)); + + return 0; +} + +int Device::devoptab_close(void *fd) { + auto file = static_cast(fd); + + log_write("[WEBDAV] Closing file: %s\n", file->entry->path.c_str()); + delete file->push_pull_thread_data; + delete file->entry; + return 0; +} + +ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) { + auto file = static_cast(fd); + len = std::min(len, file->entry->st.st_size - file->off); + + if (file->write_mode) { + log_write("[WEBDAV] Attempt to read from a write-only file\n"); + return -EBADF; + } + + if (!len) { + return 0; + } + + if (file->off != file->last_off) { + log_write("[WEBDAV] File offset changed from %zu to %zu, resetting download thread\n", file->last_off, file->off); + file->last_off = file->off; + delete file->push_pull_thread_data; + file->push_pull_thread_data = nullptr; + } + + if (!file->push_pull_thread_data) { + log_write("[WEBDAV] Creating download thread data for file: %s\n", file->entry->path.c_str()); + file->push_pull_thread_data = CreatePushData(this->transfer_curl, build_url(file->entry->path, false), file->off); + if (!file->push_pull_thread_data) { + log_write("[WEBDAV] Failed to create download thread data for file: %s\n", file->entry->path.c_str()); + return -EIO; + } + } + + const auto ret = file->push_pull_thread_data->PullData(ptr, len); + file->off += ret; + file->last_off = file->off; + + return ret; +} + +ssize_t Device::devoptab_write(void *fd, const char *ptr, size_t len) { + auto file = static_cast(fd); + + if (!file->write_mode) { + log_write("[WEBDAV] Attempt to write to a read-only file\n"); + return -EBADF; + } + + if (!len) { + return 0; + } + + if (!file->push_pull_thread_data) { + log_write("[WEBDAV] Creating upload thread data for file: %s\n", file->entry->path.c_str()); + file->push_pull_thread_data = CreatePullData(this->transfer_curl, build_url(file->entry->path, false)); + if (!file->push_pull_thread_data) { + log_write("[WEBDAV] Failed to create upload thread data for file: %s\n", file->entry->path.c_str()); + return -EIO; + } + } + + const auto ret = file->push_pull_thread_data->PushData(ptr, len); + + file->off += ret; + file->entry->st.st_size = std::max(file->entry->st.st_size, file->off); + return ret; +} + +off_t Device::devoptab_seek(void *fd, off_t pos, int dir) { + auto file = static_cast(fd); + + if (dir == SEEK_CUR) { + pos += file->off; + } else if (dir == SEEK_END) { + pos = file->entry->st.st_size; + } + + // for now, random access writes are disabled. + if (file->write_mode && pos != file->off) { + log_write("[WEBDAV] Random access writes are not supported\n"); + return file->off; + } + + return file->off = std::clamp(pos, 0, file->entry->st.st_size); +} + +int Device::devoptab_fstat(void *fd, struct stat *st) { + auto file = static_cast(fd); + + std::memcpy(st, &file->entry->st, sizeof(*st)); + return 0; +} + +int Device::devoptab_unlink(const char *path) { + if (!webdav_unlink(path)) { + return -EIO; + } + + return 0; +} + +int Device::devoptab_rename(const char *oldName, const char *newName) { + if (!webdav_rename(oldName, newName, false) && !webdav_rename(oldName, newName, true)) { + return -EIO; + } + + return 0; +} + +int Device::devoptab_mkdir(const char *path, int mode) { + if (!webdav_mkdir(path)) { + return -EIO; + } + + return 0; +} + +int Device::devoptab_rmdir(const char *path) { + if (!webdav_rmdir(path)) { + return -EIO; + } + + return 0; +} + +int Device::devoptab_diropen(void* fd, const char *path) { + auto dir = static_cast(fd); + + auto entries = new DirEntries(); + if (!webdav_dirlist(path, *entries)) { + delete entries; + return -ENOENT; + } + + dir->entries = entries; + return 0; +} + +int Device::devoptab_dirreset(void* fd) { + auto dir = static_cast(fd); + + dir->index = 0; + return 0; +} + +int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) { + auto dir = static_cast(fd); + + if (dir->index >= dir->entries->size()) { + return -ENOENT; + } + + auto& entry = (*dir->entries)[dir->index]; + if (entry.is_dir) { + filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + } else { + filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + } + + filestat->st_nlink = 1; + std::strcpy(filename, entry.name.c_str()); + + dir->index++; + return 0; +} + +int Device::devoptab_dirclose(void* fd) { + auto dir = static_cast(fd); + + delete dir->entries; + return 0; +} + +int Device::devoptab_lstat(const char *path, struct stat *st) { + if (!webdav_stat(path, st, false) && !webdav_stat(path, st, true)) { + return -ENOENT; + } + + return 0; +} + +int Device::devoptab_ftruncate(void *fd, off_t len) { + auto file = static_cast(fd); + + if (!file->write_mode) { + log_write("[WEBDAV] Attempt to truncate a read-only file\n"); + return EBADF; + } + + file->entry->st.st_size = len; + return 0; +} + +int Device::devoptab_fsync(void *fd) { + auto file = static_cast(fd); + + if (!file->write_mode) { + log_write("[WEBDAV] Attempt to fsync a read-only file\n"); + return -EBADF; + } + + return 0; +} + +} // namespace + +Result MountWebdavAll() { + return common::MountNetworkDevice([](const common::MountConfig& config) { + return std::make_unique(config); + }, + sizeof(File), sizeof(Dir), + "/config/sphaira/webdav.ini", + "WEBDAV" + ); +} + +} // namespace sphaira::devoptab