devoptab: add webdav, refactor network devices, multi thread r/w to improve perf and support webdav uploads.

This commit is contained in:
ITotalJustice
2025-09-07 12:40:45 +01:00
parent 6ce566aea5
commit b99d1e5dea
14 changed files with 2696 additions and 2038 deletions

View File

@@ -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

View File

@@ -4,6 +4,7 @@
#include <dirent.h>
#include <cstring>
#include <vector>
#include <span>
#include <string>
#include <string_view>
#include <sys/syslimits.h>
@@ -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<u8>& out);
Result write_entire_file(Fs* fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = true);
Result write_entire_file(Fs* fs, const FsPath& path, std::span<const u8> 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<u8>& out) {
return fs::read_entire_file(this, path, out);
}
Result write_entire_file(const FsPath& path, const std::vector<u8>& in) {
Result write_entire_file(const FsPath& path, std::span<const u8> in) {
return fs::write_entire_file(this, path, in, m_ignore_read_only);
}
Result copy_entire_file(const FsPath& dst, const FsPath& src) {

View File

@@ -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

View File

@@ -2,8 +2,13 @@
#include "yati/source/file.hpp"
#include "utils/lru.hpp"
#include "location.hpp"
#include <memory>
#include <optional>
#include <span>
#include <functional>
#include <unordered_map>
#include <curl/curl.h>
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<char> 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<long> port{};
int timeout{3000}; // 3 seconds.
bool read_only{};
bool no_stat_file{true};
bool no_stat_dir{true};
std::unordered_map<std::string, std::string> 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<std::unique_ptr<MountDevice>(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

View File

@@ -543,7 +543,14 @@ void App::Loop() {
}
auto App::Push(std::unique_ptr<ui::Widget>&& 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);

View File

@@ -134,7 +134,7 @@ Result read_entire_file(Fs* fs, const FsPath& path, std::vector<u8>& out) {
R_SUCCEED();
}
Result write_entire_file(Fs* fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
Result write_entire_file(Fs* fs, const FsPath& path, std::span<const u8> 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 = {};
}

View File

@@ -8,12 +8,15 @@
namespace sphaira::i18n {
namespace {
std::vector<u8> g_i18n_data;
yyjson_doc* json;
yyjson_val* root;
std::unordered_map<std::string, std::string> g_tr_cache;
std::vector<u8> g_i18n_data{};
yyjson_doc* json{};
yyjson_val* root{};
std::unordered_map<std::string, std::string> 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() );
@@ -128,6 +133,8 @@ bool init(long index) {
}
void exit() {
SCOPED_MUTEX(&g_mutex);
if (json) {
yyjson_doc_free(json);
json = nullptr;

View File

@@ -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);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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<long> port{};
long timeout{DEFAULT_HTTP_TIMEOUT};
bool no_stat_file{true};
bool no_stat_dir{true};
};
using HttpMountConfigs = std::vector<HttpMountConfig>;
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<std::vector<char>*>(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<std::span<char>*>(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<char> 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<char> buffer) {
SCOPED_TIMESTAMP("http_read_file_chunk");
const auto url = build_url(client.config.url, path, false);
bool Device::Mount() {
if (mounted) {
return true;
}
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));
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;
}
bool mount_http(Device& client) {
if (client.curl) {
return true;
}
client.curl = curl_easy_init();
if (!client.curl) {
log_write("[HTTP] curl_easy_init() failed\n");
return false;
}
return 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<File*>(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<File*>(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<File*>(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;
}
}
off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
const auto ret = file->push_pull_thread_data->PullData(ptr, len);
file->off += ret;
file->last_off = file->off;
return ret;
}
off_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(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<u64>(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<File*>(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<Dir*>(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<Dir*>(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<Dir*>(dirState->dirStruct);
SCOPED_MUTEX(&dir->client->mutex);
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(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<Dir*>(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<Dir*>(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<Dir*>(dirState->dirStruct);
SCOPED_MUTEX(&dir->client->mutex);
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(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);
return 0;
}
if (!http_stat(*device, path, st, false) && !http_stat(*device, path, st, true)) {
return set_errno(r, ENOENT);
}
return r->_errno = 0;
}
constexpr devoptab_t DEVOPTAB = {
.structSize = sizeof(File),
.open_r = devoptab_open,
.close_r = devoptab_close,
.read_r = devoptab_read,
.seek_r = devoptab_seek,
.fstat_r = devoptab_fstat,
.stat_r = devoptab_lstat,
.dirStateSize = sizeof(Dir),
.diropen_r = devoptab_diropen,
.dirreset_r = devoptab_dirreset,
.dirnext_r = devoptab_dirnext,
.dirclose_r = devoptab_dirclose,
.lstat_r = devoptab_lstat,
};
struct Entry {
Device device{};
devoptab_t devoptab{};
fs::FsPath mount{};
char name[32]{};
s32 ref_count{};
~Entry() {
if (device.curl) {
curl_easy_cleanup(device.curl);
}
RemoveDevice(mount);
}
};
Mutex g_mutex;
std::array<std::unique_ptr<Entry>, 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<HttpMountConfigs*>(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>();
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<Device>(config);
},
sizeof(File), sizeof(Dir),
"/config/sphaira/http.ini",
"HTTP"
);
}
} // namespace sphaira::devoptab

View File

@@ -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<NfsMountConfig>;
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());
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<Device*>(r->deviceData);
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(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<File*>(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<File*>(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<size_t>(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<File*>(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<size_t>(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<File*>(fd);
SCOPED_MUTEX(&file->device->mutex);
u64 current_off;
const auto ret = nfs_lseek(file->device->nfs, file->fd, pos, dir, &current_off);
u64 current_offset = 0;
const auto ret = nfs_lseek(nfs, file->fd, pos, dir, &current_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<File*>(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<Device*>(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<Device*>(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<Device*>(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<Device*>(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<Device*>(r->deviceData);
auto dir = static_cast<Dir*>(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<Dir*>(dirState->dirStruct);
SCOPED_MUTEX(&dir->device->mutex);
nfs_rewinddir(dir->device->nfs, dir->dir);
return r->_errno = 0;
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;
}
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(filestat, 0, sizeof(*filestat));
SCOPED_MUTEX(&dir->device->mutex);
return 0;
}
const auto entry = nfs_readdir(dir->device->nfs, dir->dir);
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;
}
return 0;
}
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(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<Dir*>(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<Dir*>(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<Dir*>(dirState->dirStruct);
SCOPED_MUTEX(&dir->device->mutex);
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(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<Device*>(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<File*>(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<Device*>(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<File*>(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;
}
int devoptab_utimes(struct _reent *r, const char *_path, const struct timeval times[2]) {
auto device = static_cast<Device*>(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);
return 0;
}
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<std::unique_ptr<Entry>, 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<NfsMountConfigs*>(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>();
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<Device>(cfg);
},
sizeof(File), sizeof(Dir),
"/config/sphaira/nfs.ini",
"NFS"
);
}
} // namespace sphaira::devoptab

View File

@@ -1,9 +1,7 @@
#include "utils/devoptab_common.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "location.hpp"
#include <sys/iosupport.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
@@ -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<Smb2MountConfig>;
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);
}
int set_errno(struct _reent *r, int err) {
r->_errno = err;
return -1;
smb2_destroy_context(this->smb2);
}
}
int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) {
auto device = static_cast<Device*>(r->deviceData);
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 Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
auto file = static_cast<File*>(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<File*>(fd);
SCOPED_MUTEX(&file->device->mutex);
if (file->fd) {
smb2_close(file->device->smb2, file->fd);
smb2_close(this->smb2, file->fd);
return 0;
}
std::memset(file, 0, sizeof(*file));
return r->_errno = 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<File*>(fd);
SCOPED_MUTEX(&file->device->mutex);
const auto ret = smb2_read(file->device->smb2, file->fd, reinterpret_cast<uint8_t*>(ptr), len);
const auto ret = smb2_read(this->smb2, file->fd, reinterpret_cast<uint8_t*>(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<File*>(fd);
SCOPED_MUTEX(&file->device->mutex);
const auto ret = smb2_write(file->device->smb2, file->fd, reinterpret_cast<const uint8_t*>(ptr), len);
const auto ret = smb2_write(this->smb2, file->fd, reinterpret_cast<const uint8_t*>(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<File*>(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, &current_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);
}
r->_errno = 0;
log_write("[SMB2] smb2_lseek() failed: %s errno: %s\n", smb2_get_error(this->smb2), std::strerror(-ret));
return ret;
}
int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) {
return current_offset;
}
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(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<Device*>(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<Device*>(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<Device*>(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<Device*>(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<Device*>(r->deviceData);
auto dir = static_cast<Dir*>(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;
}
dir->dir = smb2_opendir(device->smb2, path);
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;
}
return 0;
}
int Device::devoptab_diropen(void* fd, const char *path) {
auto dir = static_cast<Dir*>(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<Dir*>(dirState->dirStruct);
SCOPED_MUTEX(&dir->device->mutex);
smb2_rewinddir(dir->device->smb2, dir->dir);
return r->_errno = 0;
int Device::devoptab_dirreset(void* fd) {
auto dir = static_cast<Dir*>(fd);
if (!dir->dir) {
return -EINVAL;
}
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(filestat, 0, sizeof(*filestat));
SCOPED_MUTEX(&dir->device->mutex);
smb2_rewinddir(this->smb2, dir->dir);
return 0;
}
const auto entry = smb2_readdir(dir->device->smb2, dir->dir);
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(fd);
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<Dir*>(dirState->dirStruct);
SCOPED_MUTEX(&dir->device->mutex);
int Device::devoptab_dirclose(void* fd) {
auto dir = static_cast<Dir*>(fd);
if (dir->dir) {
smb2_closedir(dir->device->smb2, dir->dir);
}
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<Device*>(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);
smb2_closedir(this->smb2, dir->dir);
return 0;
}
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<File*>(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;
}
int devoptab_statvfs(struct _reent *r, const char *_path, struct statvfs *buf) {
auto device = static_cast<Device*>(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);
return 0;
}
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<File*>(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<std::unique_ptr<Entry>, 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<Smb2MountConfigs*>(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>();
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<Device>(cfg);
},
sizeof(File), sizeof(Dir),
"/config/sphaira/smb.ini",
"SMB"
);
}
} // namespace sphaira::devoptab

View File

@@ -0,0 +1,581 @@
#include "utils/devoptab_common.hpp"
#include "utils/profile.hpp"
#include "log.hpp"
#include "defines.hpp"
#include <fcntl.h>
#include <curl/curl.h>
#include <string>
#include <vector>
#include <memory>
#include <cstring>
#include <optional>
#include <sys/stat.h>
// todo: try to reduce binary size by using a smaller xml parser.
#include <pugixml.hpp>
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<DirEntry>;
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<bool, long> webdav_custom_command(const std::string& path, const std::string& cmd, std::string_view postfields, std::span<const std::string> headers, bool is_dir, std::vector<char>* 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<bool, long> Device::webdav_custom_command(const std::string& path, const std::string& cmd, std::string_view postfields, std::span<const std::string> headers, bool is_dir, std::vector<char>* 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 =
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
"<d:propfind xmlns:d=\"DAV:\">"
"<d:prop>"
// "<d:getcontentlength/>"
"<d:resourcetype/>"
"</d:prop>"
"</d:propfind>";
const std::string custom_headers[] = {
"Content-Type: application/xml; charset=utf-8",
"Depth: 1"
};
std::vector<char> 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<File*>(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<File*>(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<File*>(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<File*>(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<off_t>(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<File*>(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<u64>(pos, 0, file->entry->st.st_size);
}
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(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<Dir*>(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<Dir*>(fd);
dir->index = 0;
return 0;
}
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(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<Dir*>(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<File*>(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<File*>(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<Device>(config);
},
sizeof(File), sizeof(Dir),
"/config/sphaira/webdav.ini",
"WEBDAV"
);
}
} // namespace sphaira::devoptab