diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index dcdc8dd..4427400 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -105,6 +105,9 @@ add_executable(sphaira source/utils/devoptab_xci.cpp source/utils/devoptab_zip.cpp source/utils/devoptab_bfsar.cpp + source/utils/devoptab_http.cpp + source/utils/devoptab_nfs.cpp + source/utils/devoptab_smb2.cpp source/usb/base.cpp source/usb/usbds.cpp @@ -255,6 +258,16 @@ FetchContent_Declare(libusbdvd GIT_TAG 3cb0613 ) +FetchContent_Declare(libnfs + GIT_REPOSITORY https://github.com/ITotalJustice/libnfs.git + GIT_TAG 65f3e11 +) + +FetchContent_Declare(libsmb2 + GIT_REPOSITORY https://github.com/ITotalJustice/libsmb2.git + GIT_TAG 867beea +) + set(USE_NEW_ZSTD ON) # has issues with some homebrew and game icons (oxenfree, overwatch2). set(USE_NVJPG OFF) @@ -315,6 +328,8 @@ FetchContent_MakeAvailable( dr_libs id3v2lib libusbdvd + libnfs + libsmb2 ) set(FTPSRV_LIB_BUILD TRUE) @@ -425,6 +440,13 @@ set_target_properties(sphaira PROPERTIES CXX_EXTENSIONS ON ) +# todo: fix this upstream as nfs should export these folders. +target_include_directories(sphaira PRIVATE + ${libnfs_SOURCE_DIR}/include + ${libnfs_SOURCE_DIR}/include/nfsc + ${libnfs_SOURCE_DIR}/nfs +) + target_link_libraries(sphaira PRIVATE ftpsrv_helper libhaze @@ -439,6 +461,8 @@ target_link_libraries(sphaira PRIVATE dr_libs id3v2lib libusbdvd + nfs + smb2 ${minizip_lib} ZLIB::ZLIB diff --git a/sphaira/include/defines.hpp b/sphaira/include/defines.hpp index 378782b..3b06e5b 100644 --- a/sphaira/include/defines.hpp +++ b/sphaira/include/defines.hpp @@ -823,11 +823,40 @@ enum : Result { #define CONCATENATE(s1, s2) CONCATENATE_IMPL(s1, s2) #define ANONYMOUS_VARIABLE(pref) CONCATENATE(pref, __COUNTER__) -#define ON_SCOPE_EXIT(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { _f; }}; +template +struct ScopeGuard { + ScopeGuard(Function&& function) : m_function(std::forward(function)) { + + } + ~ScopeGuard() { + m_function(); + } + + ScopeGuard(const ScopeGuard&) = delete; + void operator=(const ScopeGuard&) = delete; + +private: + const Function m_function; +}; + +struct ScopedMutex { + ScopedMutex(Mutex* mutex) : m_mutex{mutex} { + mutexLock(m_mutex); + } + ~ScopedMutex() { + mutexUnlock(m_mutex); + } + + ScopedMutex(const ScopedMutex&) = delete; + void operator=(const ScopedMutex&) = delete; + +private: + Mutex* const m_mutex; +}; + +// #define ON_SCOPE_EXIT(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { _f; }}; +#define ON_SCOPE_EXIT(_f) ScopeGuard ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { _f; }}; +#define SCOPED_MUTEX(_m) ScopedMutex ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){_m} + // #define ON_SCOPE_FAIL(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_FAILED(rc)) { _f; } }}; // #define ON_SCOPE_SUCCESS(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_SUCCEEDED(rc)) { _f; } }}; - -// mutex helpers. -#define SCOPED_MUTEX(mutex) \ - mutexLock(mutex); \ - ON_SCOPE_EXIT(mutexUnlock(mutex)) diff --git a/sphaira/include/fs.hpp b/sphaira/include/fs.hpp index 0f066b5..6ba086b 100644 --- a/sphaira/include/fs.hpp +++ b/sphaira/include/fs.hpp @@ -138,20 +138,24 @@ struct FsPath { return *this; } + static constexpr bool path_equal(std::string_view a, std::string_view b) { + return a.length() == b.length() && !strncasecmp(a.data(), b.data(), a.length()); + } + constexpr bool operator==(const FsPath& v) const noexcept { - return !strcasecmp(*this, v); + return path_equal(*this, v); } constexpr bool operator==(const char* v) const noexcept { - return !strcasecmp(*this, v); + return path_equal(*this, v); } constexpr bool operator==(const std::string& v) const noexcept { - return !strncasecmp(*this, v.data(), v.length()); + return path_equal(*this, v); } constexpr bool operator==(const std::string_view v) const noexcept { - return !strncasecmp(*this, v.data(), v.length()); + return path_equal(*this, v); } static consteval bool Test(const auto& str) { @@ -229,7 +233,6 @@ struct File { fs::Fs* m_fs{}; FsFile m_native{}; std::FILE* m_stdio{}; - s64 m_stdio_off{}; u32 m_mode{}; }; diff --git a/sphaira/include/utils/devoptab.hpp b/sphaira/include/utils/devoptab.hpp index 506447b..c53afc2 100644 --- a/sphaira/include/utils/devoptab.hpp +++ b/sphaira/include/utils/devoptab.hpp @@ -2,6 +2,7 @@ #include "fs.hpp" #include "yati/source/base.hpp" +#include "location.hpp" #include #include @@ -32,4 +33,16 @@ 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 MountHttpAll(); +Result GetHttpMounts(location::StdioEntries& out); +void UnmountHttpAll(); + +Result MountNfsAll(); +Result GetNfsMounts(location::StdioEntries& out); +void UnmountNfsAll(); + +Result MountSmb2All(); +Result GetSmb2Mounts(location::StdioEntries& out); +void UnmountSmb2All(); + } // namespace sphaira::devoptab diff --git a/sphaira/include/utils/devoptab_common.hpp b/sphaira/include/utils/devoptab_common.hpp index 80feb98..f2aebb8 100644 --- a/sphaira/include/utils/devoptab_common.hpp +++ b/sphaira/include/utils/devoptab_common.hpp @@ -81,6 +81,8 @@ private: std::vector buffered_large{}; // 1MiB }; -bool fix_path(const char* str, char* out); +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); } // namespace sphaira::devoptab::common diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index 6f0afa4..d3f7b5e 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -27,6 +27,7 @@ #include "utils/profile.hpp" #include "utils/thread.hpp" +#include "utils/devoptab.hpp" #include #include @@ -1590,6 +1591,22 @@ App::App(const char* argv0) { curl::Init(); } + // this has to come after curl init as it inits curl global. + { + SCOPED_TIMESTAMP("http init"); + devoptab::MountHttpAll(); + } + + { + SCOPED_TIMESTAMP("nfs init"); + devoptab::MountNfsAll(); + } + + { + SCOPED_TIMESTAMP("smb init"); + devoptab::MountSmb2All(); + } + { SCOPED_TIMESTAMP("timestamp init"); // ini_putl(GetExePath(), "timestamp", m_start_timestamp, App::PLAYLOG_PATH); @@ -2200,6 +2217,22 @@ App::~App() { fatfs::UnmountAll(); } + // this has to come before curl exit as it uses curl global. + { + SCOPED_TIMESTAMP("http exit"); + devoptab::UnmountHttpAll(); + } + + { + SCOPED_TIMESTAMP("nfs exit"); + devoptab::UnmountNfsAll(); + } + + { + SCOPED_TIMESTAMP("smb exit"); + devoptab::UnmountSmb2All(); + } + // do these last as they were signalled to exit. { SCOPED_TIMESTAMP("audio_exit"); diff --git a/sphaira/source/fs.cpp b/sphaira/source/fs.cpp index 2d7bbd1..a8b5d2b 100644 --- a/sphaira/source/fs.cpp +++ b/sphaira/source/fs.cpp @@ -500,9 +500,9 @@ Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_rea } else { R_UNLESS(m_stdio, Result_FsUnknownStdioError); - if (m_stdio_off != off) { - m_stdio_off = off; - std::fseek(m_stdio, off, SEEK_SET); + if (std::ftell(m_stdio) != off) { + const auto ret = std::fseek(m_stdio, off, SEEK_SET); + R_UNLESS(ret == 0, Result_FsUnknownStdioError); } *bytes_read = std::fread(buf, 1, read_size, m_stdio); @@ -510,11 +510,10 @@ Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_rea // if we read less bytes than expected, check if there was an error (ignoring eof). if (*bytes_read < read_size) { if (!std::feof(m_stdio) && std::ferror(m_stdio)) { + log_write("[FS] fread error: %d\n", std::ferror(m_stdio)); R_THROW(Result_FsUnknownStdioError); } } - - m_stdio_off += *bytes_read; } R_SUCCEED(); @@ -528,17 +527,14 @@ Result File::Write(s64 off, const void* buf, u64 write_size, u32 option) { } else { R_UNLESS(m_stdio, Result_FsUnknownStdioError); - if (m_stdio_off != off) { - log_write("[FS] diff seek\n"); - m_stdio_off = off; - std::fseek(m_stdio, off, SEEK_SET); + if (std::ftell(m_stdio) != off) { + const auto ret = std::fseek(m_stdio, off, SEEK_SET); + R_UNLESS(ret == 0, Result_FsUnknownStdioError); } const auto result = std::fwrite(buf, 1, write_size, m_stdio); // log_write("[FS] fwrite res: %zu vs %zu\n", result, write_size); R_UNLESS(result == write_size, Result_FsUnknownStdioError); - - m_stdio_off += write_size; } R_SUCCEED(); diff --git a/sphaira/source/location.cpp b/sphaira/source/location.cpp index b2c31eb..dbc2f74 100644 --- a/sphaira/source/location.cpp +++ b/sphaira/source/location.cpp @@ -2,6 +2,7 @@ #include "fs.hpp" #include "app.hpp" #include "usbdvd.hpp" +#include "utils/devoptab.hpp" #include #include @@ -79,6 +80,30 @@ auto Load() -> Entries { auto GetStdio(bool write) -> StdioEntries { StdioEntries out{}; + { + StdioEntries entries; + if (R_SUCCEEDED(devoptab::GetNfsMounts(entries))) { + log_write("[NFS] got nfs mounts: %zu\n", entries.size()); + out.insert(out.end(), entries.begin(), entries.end()); + } + } + + { + StdioEntries entries; + if (R_SUCCEEDED(devoptab::GetSmb2Mounts(entries))) { + log_write("[SMB2] got smb2 mounts: %zu\n", entries.size()); + out.insert(out.end(), entries.begin(), entries.end()); + } + } + + { + StdioEntries entries; + if (R_SUCCEEDED(devoptab::GetHttpMounts(entries))) { + log_write("[HTTP] got http mounts: %zu\n", entries.size()); + out.insert(out.end(), entries.begin(), entries.end()); + } + } + // try and load usbdvd entry. // todo: check if more than 1 entry is supported. // todo: only call if usbdvd is init. diff --git a/sphaira/source/ui/menus/filebrowser.cpp b/sphaira/source/ui/menus/filebrowser.cpp index 0c4c637..23d85cf 100644 --- a/sphaira/source/ui/menus/filebrowser.cpp +++ b/sphaira/source/ui/menus/filebrowser.cpp @@ -624,8 +624,10 @@ void FsView::Draw(NVGcontext* vg, Theme* theme) { gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%02u/%02u/%u", tm.tm_mday, tm.tm_mon + 1, tm.tm_year + 1900); if ((double)e.file_size / 1024.0 / 1024.0 <= 0.009) { gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f KiB", (double)e.file_size / 1024.0); - } else { + } else if ((double)e.file_size / 1024.0 / 1024.0 / 1024.0 <= 0.009) { gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f MiB", (double)e.file_size / 1024.0 / 1024.0); + } else { + gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f GiB", (double)e.file_size / 1024.0 / 1024.0 / 1024.0); } } }); diff --git a/sphaira/source/utils/devoptab_common.cpp b/sphaira/source/utils/devoptab_common.cpp index fbba6d8..2eb2062 100644 --- a/sphaira/source/utils/devoptab_common.cpp +++ b/sphaira/source/utils/devoptab_common.cpp @@ -156,7 +156,7 @@ Result LruBufferedData::Read(void *_buffer, s64 file_off, s64 read_size, u64* by R_SUCCEED(); } -bool fix_path(const char* str, char* out) { +bool fix_path(const char* str, char* out, bool strip_leading_slash) { // log_write("[SAVE] got path: %s\n", str); str = std::strrchr(str, ':'); @@ -175,9 +175,16 @@ bool fix_path(const char* str, char* out) { continue; } - // add leading slash. - if (!i && str[i] != '/') { - out[len++] = '/'; + if (!i) { + // skip leading slash. + if (strip_leading_slash && str[i] == '/') { + continue; + } + + // add leading slash. + if (!strip_leading_slash && str[i] != '/') { + out[len++] = '/'; + } } // save single char. @@ -197,4 +204,20 @@ bool fix_path(const char* str, char* 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->ftruncate_r = nullptr; + devoptab->fsync_r = nullptr; + devoptab->rmdir_r = nullptr; + devoptab->utimes_r = nullptr; + devoptab->symlink_r = nullptr; + } +} + } // sphaira::devoptab::common diff --git a/sphaira/source/utils/devoptab_http.cpp b/sphaira/source/utils/devoptab_http.cpp new file mode 100644 index 0000000..e6edb69 --- /dev/null +++ b/sphaira/source/utils/devoptab_http.cpp @@ -0,0 +1,647 @@ +#include "utils/devoptab_common.hpp" +#include "utils/profile.hpp" + +#include "location.hpp" +#include "log.hpp" +#include "defines.hpp" +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace sphaira::devoptab { +namespace { + +#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{}; +}; +using HttpMountConfigs = std::vector; + +struct Device { + CURL* curl{}; + HttpMountConfig config; + Mutex mutex{}; + bool mounted{}; +}; + +struct DirEntry { + std::string name{}; + std::string href{}; + bool is_dir{}; +}; +using DirEntries = std::vector; + +struct FileEntry { + std::string path{}; + struct stat st{}; +}; + +struct File { + Device* client; + FileEntry* entry; + size_t 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); + + // 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 write_data_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { + auto data = static_cast*>(userdata); + + const auto realsize = size * nmemb; + if (data->size() < realsize) { + log_write("[HTTP] buffer is too small: %zu < %zu\n", data->size(), realsize); + return 0; // buffer is too small + } + + std::memcpy(data->data(), ptr, realsize); + *data = data->subspan(realsize); // advance the span + + return realsize; +} + +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()); + // todo: make the timeouts configurable. + CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_TIMEOUT, 5L); + CURL_EASY_SETOPT_LOG(client.curl, CURLOPT_CONNECTTIMEOUT, 5L); + 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); + + // 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); + 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); + + 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; + } + + chunk.emplace_back('\0'); // null-terminate the chunk + const char* dilim = ""); + if (!href_end) { + continue; + } + const auto href_len = href_end - href_begin; + + const auto name_begin = href_end + std::strlen("\">"); + const auto name_end = std::strstr(name_begin, ""); + if (!name_end) { + continue; + } + const auto name_len = name_end - name_begin; + + if (href_len <= 0 || name_len <= 0) { + continue; + } + + // skip if inside