diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index 0b3ea25..7d9bfa2 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -128,6 +128,7 @@ add_executable(sphaira source/utils/devoptab_bfsar.cpp source/utils/devoptab_vfs.cpp source/utils/devoptab_fatfs.cpp + source/utils/devoptab.cpp source/usb/base.cpp source/usb/usbds.cpp diff --git a/sphaira/include/ui/sidebar.hpp b/sphaira/include/ui/sidebar.hpp index 60f875c..bfadc70 100644 --- a/sphaira/include/ui/sidebar.hpp +++ b/sphaira/include/ui/sidebar.hpp @@ -175,7 +175,10 @@ private: class SidebarEntryTextInput final : public SidebarEntryTextBase { public: + // uses normal keyboard. explicit SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide = {}, s64 len_min = -1, s64 len_max = PATH_MAX, const std::string& info = ""); + // uses numpad. + explicit SidebarEntryTextInput(const std::string& title, s64 value, const std::string& guide = {}, s64 len_min = -1, s64 len_max = PATH_MAX, const std::string& info = ""); private: const std::string m_guide; @@ -202,10 +205,8 @@ public: using Items = std::vector>; public: - explicit Sidebar(const std::string& title, Side side, Items&& items); - explicit Sidebar(const std::string& title, Side side); - explicit Sidebar(const std::string& title, const std::string& sub, Side side, Items&& items); - explicit Sidebar(const std::string& title, const std::string& sub, Side side); + explicit Sidebar(const std::string& title, Side side, float width = 450.f); + explicit Sidebar(const std::string& title, const std::string& sub, Side side, float width = 450.f); auto Update(Controller* controller, TouchInfo* touch) -> void override; auto Draw(NVGcontext* vg, Theme* theme) -> void override; @@ -242,4 +243,11 @@ private: static constexpr Vec2 m_box_size{400.f, 70.f}; }; +class FormSidebar : public Sidebar { +public: + // explicit FormSidebar(const std::string& title) : Sidebar{title, Side::RIGHT, 540.f} { + explicit FormSidebar(const std::string& title) : Sidebar{title, Side::RIGHT} { + } +}; + } // namespace sphaira::ui diff --git a/sphaira/include/utils/devoptab.hpp b/sphaira/include/utils/devoptab.hpp index 480ecd3..7797137 100644 --- a/sphaira/include/utils/devoptab.hpp +++ b/sphaira/include/utils/devoptab.hpp @@ -36,4 +36,6 @@ void UmountNeworkDevice(const fs::FsPath& mount); // SEE: https://github.com/devkitPro/newlib/issues/35 void FixDkpBug(); +void DisplayDevoptabSideBar(); + } // namespace sphaira::devoptab diff --git a/sphaira/include/utils/devoptab_common.hpp b/sphaira/include/utils/devoptab_common.hpp index c2f5730..46184c9 100644 --- a/sphaira/include/utils/devoptab_common.hpp +++ b/sphaira/include/utils/devoptab_common.hpp @@ -131,7 +131,7 @@ struct MountConfig { std::string user{}; std::string pass{}; std::string dump_path{}; - std::optional port{}; + long port{}; long timeout{}; bool read_only{}; bool no_stat_file{true}; @@ -141,6 +141,7 @@ struct MountConfig { std::unordered_map extra{}; }; +using MountConfigs = std::vector; struct PullThreadData final : PushPullThreadData { using PushPullThreadData::PushPullThreadData; @@ -214,6 +215,8 @@ private: bool m_mounted{}; }; +void LoadConfigsFromIni(const fs::FsPath& path, MountConfigs& out_configs); + using CreateDeviceCallback = std::function(const MountConfig& config)>; Result MountNetworkDevice(const CreateDeviceCallback& create_device, size_t file_size, size_t dir_size, const char* name, bool force_read_only = false); diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index 2b028ef..4332d70 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -1953,6 +1953,12 @@ void App::DisplayAdvancedOptions(bool left_side) { }, "When enabled, it replaces /hbmenu.nro with Sphaira, creating a backup of hbmenu to /switch/hbmenu.nro\n\n" \ "Disabling will give you the option to restore hbmenu."_i18n); + options->Add("Add / modify mounts"_i18n, [](){ + devoptab::DisplayDevoptabSideBar(); + }, "Create, modify, delete network mounts (HTTP, FTP, SFTP, SMB, NFS).\n" + "Mount options only require a URL and Name be set, with other fields being optional, such as port, user, pass etc.\n\n" + "Any changes made will require restarting Sphaira to take effect."_i18n); + options->Add("Boost CPU during transfer"_i18n, App::GetApp()->m_progress_boost_mode, "Enables boost mode during transfers which can improve transfer speed. " "This sets the CPU to 1785mhz and lowers the GPU 76mhz"_i18n); diff --git a/sphaira/source/ui/menus/filebrowser.cpp b/sphaira/source/ui/menus/filebrowser.cpp index 4846eb4..0ea914a 100644 --- a/sphaira/source/ui/menus/filebrowser.cpp +++ b/sphaira/source/ui/menus/filebrowser.cpp @@ -53,7 +53,7 @@ namespace { using RomDatabaseIndexs = std::vector; -struct ForwarderForm final : public Sidebar { +struct ForwarderForm final : public FormSidebar { explicit ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndexs& db_indexs, const FileEntry& entry, const fs::FsPath& arg_path); private: @@ -288,7 +288,7 @@ auto GetRomIcon(std::string filename, const RomDatabaseIndexs& db_indexs, const } ForwarderForm::ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndexs& db_indexs, const FileEntry& entry, const fs::FsPath& arg_path) -: Sidebar{"Forwarder Creation", Side::RIGHT} +: FormSidebar{"Forwarder Creation"} , m_assoc{assoc} , m_db_indexs{db_indexs} , m_arg_path{arg_path} { diff --git a/sphaira/source/ui/sidebar.cpp b/sphaira/source/ui/sidebar.cpp index 4a2f6a0..0e433f3 100644 --- a/sphaira/source/ui/sidebar.cpp +++ b/sphaira/source/ui/sidebar.cpp @@ -314,6 +314,16 @@ SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, const std }); } +SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, s64 value, const std::string& guide, s64 len_min, s64 len_max, const std::string& info) +: SidebarEntryTextInput{title, std::to_string(value), guide, len_min, len_max, info} { + SetCallback([this](){ + s64 out = std::stoul(GetValue()); + if (R_SUCCEEDED(swkbd::ShowNumPad(out, m_guide.c_str(), GetValue().c_str(), m_len_min, m_len_max))) { + SetValue(std::to_string(out)); + } + }); +} + SidebarEntryFilePicker::SidebarEntryFilePicker(const std::string& title, const std::string& value, const std::vector& filter, const std::string& info) : SidebarEntryTextBase{title, value, {}, info}, m_filter{filter} { @@ -328,26 +338,21 @@ SidebarEntryFilePicker::SidebarEntryFilePicker(const std::string& title, const s }); } -Sidebar::Sidebar(const std::string& title, Side side, Items&& items) -: Sidebar{title, "", side, std::forward(items)} { +Sidebar::Sidebar(const std::string& title, Side side, float width) +: Sidebar{title, "", side, width} { } -Sidebar::Sidebar(const std::string& title, Side side) -: Sidebar{title, "", side, {}} { -} - -Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side, Items&& items) +Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side, float width) : m_title{title} , m_sub{sub} -, m_side{side} -, m_items{std::forward(items)} { +, m_side{side} { switch (m_side) { case Side::LEFT: - SetPos(Vec4{0.f, 0.f, 450.f, SCREEN_HEIGHT}); + SetPos(Vec4{0.f, 0.f, width, SCREEN_HEIGHT}); break; case Side::RIGHT: - SetPos(Vec4{SCREEN_WIDTH - 450.f, 0.f, 450.f, SCREEN_HEIGHT}); + SetPos(Vec4{SCREEN_WIDTH - width, 0.f, width, SCREEN_HEIGHT}); break; } @@ -365,11 +370,6 @@ Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side, It m_list->SetScrollBarPos(GetX() + GetW() - 20, m_base_pos.y - 10, pos.h - m_base_pos.y + 48); } -Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side) -: Sidebar{title, sub, side, {}} { -} - - auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void { Widget::Update(controller, touch); diff --git a/sphaira/source/utils/devoptab.cpp b/sphaira/source/utils/devoptab.cpp new file mode 100644 index 0000000..6ba4108 --- /dev/null +++ b/sphaira/source/utils/devoptab.cpp @@ -0,0 +1,294 @@ +#include "utils/devoptab_common.hpp" +#include "utils/thread.hpp" + +#include "ui/sidebar.hpp" +#include "ui/popup_list.hpp" +#include "ui/option_box.hpp" + +#include "app.hpp" +#include "defines.hpp" +#include "log.hpp" +#include "download.hpp" +#include "i18n.hpp" + +#include +#include +#include +#include +#include + +namespace sphaira::devoptab { +namespace { + +using namespace sphaira::ui; +using namespace sphaira::devoptab::common; + +enum class DevoptabType { + HTTP, + FTP, + SFTP, + NFS, + SMB, + WEBDAV, +}; + +struct TypeEntry { + const char* name; + DevoptabType type; +}; + +const TypeEntry TYPE_ENTRIES[] = { + {"HTTP", DevoptabType::HTTP}, + {"FTP", DevoptabType::FTP}, + {"SFTP", DevoptabType::SFTP}, + {"NFS", DevoptabType::NFS}, + {"SMB", DevoptabType::SMB}, + {"WEBDAV", DevoptabType::WEBDAV}, +}; + +struct TypeConfig { + TypeEntry type; + MountConfig config; +}; +using TypeConfigs = std::vector; + +auto BuildIniPathFromType(DevoptabType type) -> fs::FsPath { + switch (type) { + case DevoptabType::HTTP: return "/config/sphaira/mount/http.ini"; + case DevoptabType::FTP: return "/config/sphaira/mount/ftp.ini"; + case DevoptabType::SFTP: return "/config/sphaira/mount/sftp.ini"; + case DevoptabType::NFS: return "/config/sphaira/mount/nfs.ini"; + case DevoptabType::SMB: return "/config/sphaira/mount/smb.ini"; + case DevoptabType::WEBDAV: return "/config/sphaira/mount/webdav.ini"; + } + + std::unreachable(); +} + +auto GetTypeName(const TypeConfig& type_config) -> std::string { + char name[128]{}; + std::snprintf(name, sizeof(name), "[%s] %s", type_config.type.name, type_config.config.name.c_str()); + return name; +} + +void LoadAllConfigs(TypeConfigs& out_configs) { + out_configs.clear(); + + for (const auto& e : TYPE_ENTRIES) { + const auto ini_path = BuildIniPathFromType(e.type); + + MountConfigs configs{}; + LoadConfigsFromIni(ini_path, configs); + + for (const auto& config : configs) { + out_configs.emplace_back(e, config); + } + } +} + +struct DevoptabForm final : public FormSidebar { + // create new. + explicit DevoptabForm(); + // modify existing. + explicit DevoptabForm(DevoptabType type, const MountConfig& config); + +private: + void SetupButtons(bool type_change); + +private: + DevoptabType m_type{}; + MountConfig m_config{}; + + SidebarEntryTextInput* m_name{}; + SidebarEntryTextInput* m_url{}; + SidebarEntryTextInput* m_port{}; + // SidebarEntryTextInput* m_timeout{}; + SidebarEntryTextInput* m_user{}; + SidebarEntryTextInput* m_pass{}; + SidebarEntryTextInput* m_dump_path{}; +}; + +DevoptabForm::DevoptabForm(DevoptabType type, const MountConfig& config) +: FormSidebar{"Mount Creator"} +, m_type{type} +, m_config{config} { + SetupButtons(false); +} + +DevoptabForm::DevoptabForm() : FormSidebar{"Mount Creator"} { + SetupButtons(true); +} + +void DevoptabForm::SetupButtons(bool type_change) { + if (type_change) { + SidebarEntryArray::Items items; + for (const auto& e : TYPE_ENTRIES) { + items.emplace_back(e.name); + } + + this->Add( + "Type", items, [this](s64& index) { + m_type = TYPE_ENTRIES[index].type; + }, + (s64)m_type, + "Select the type of the forwarder."_i18n + ); + } + + m_name = this->Add( + "Name", m_config.name, "", -1, 32, + "Set the name of the application"_i18n + ); + + m_url = this->Add( + "URL", m_config.url, "", -1, PATH_MAX, + "Set the URL of the application"_i18n + ); + + m_port = this->Add( + "Port", m_config.port, "Port number", 1, 5, + "Optional: Set the port of the server. If left empty, the default port for the protocol will be used."_i18n + ); + + #if 0 + m_timeout = this->Add( + "Timeout", m_config.timeout, "Timeout in milliseconds", 1, 5, + "Optional: Set the timeout in seconds."_i18n + ); + #endif + + m_user = this->Add( + "User", m_config.user, "", -1, PATH_MAX, + "Optional: Set the username of the application"_i18n + ); + + m_pass = this->Add( + "Pass", m_config.pass, "", -1, PATH_MAX, + "Optional: Set the password of the application"_i18n + ); + + m_dump_path = this->Add( + "Dump path", m_config.dump_path, "", -1, PATH_MAX, + "Optional: Set the dump path used when exporting games and saves."_i18n + ); + + this->Add( + "Read only", m_config.read_only, + "Mount the filesystem as read only.\n\n" + "Setting this option also hidens the mount from being show as an export option."_i18n + ); + + this->Add( + "No stat file", m_config.no_stat_file, + "Enabling stops the file browser from checking the file size and timestamp of each file. " + "This improves browsing performance."_i18n + ); + + this->Add( + "No stat dir", m_config.no_stat_dir, + "Enabling stops the file browser from checking how many files and folders are in a folder. " + "This improves browsing performance, especially for servers that has slow directory listing."_i18n + ); + + this->Add( + "FS hidden", m_config.fs_hidden, + "Hide the mount from being visible in the file browser."_i18n + ); + + this->Add( + "Export hidden", m_config.dump_hidden, + "Hide the mount from being visible as a export option for games and saves."_i18n + ); + + const auto callback = this->Add("Save", [this](){ + m_config.name = m_name->GetValue(); + m_config.url = m_url->GetValue(); + m_config.user = m_user->GetValue(); + m_config.pass = m_pass->GetValue(); + m_config.dump_path = m_dump_path->GetValue(); + m_config.port = std::stoul(m_port->GetValue()); + // m_config.timeout = m_timeout->GetValue(); + + const auto ini_path = BuildIniPathFromType(m_type); + + ini_puts(m_config.name.c_str(), "url", m_config.url.c_str(), ini_path); + ini_puts(m_config.name.c_str(), "user", m_config.user.c_str(), ini_path); + ini_puts(m_config.name.c_str(), "pass", m_config.pass.c_str(), ini_path); + ini_puts(m_config.name.c_str(), "dump_path", m_config.dump_path.c_str(), ini_path); + ini_putl(m_config.name.c_str(), "port", m_config.port, ini_path); + ini_putl(m_config.name.c_str(), "timeout", m_config.timeout, ini_path); + // todo: update minini to have put_bool. + ini_puts(m_config.name.c_str(), "read_only", m_config.read_only ? "true" : "false", ini_path); + ini_puts(m_config.name.c_str(), "no_stat_file", m_config.no_stat_file ? "true" : "false", ini_path); + ini_puts(m_config.name.c_str(), "no_stat_dir", m_config.no_stat_dir ? "true" : "false", ini_path); + ini_puts(m_config.name.c_str(), "fs_hidden", m_config.fs_hidden ? "true" : "false", ini_path); + ini_puts(m_config.name.c_str(), "dump_hidden", m_config.dump_hidden ? "true" : "false", ini_path); + + App::Notify("Mount entry saved. Restart Sphaira to apply changes."_i18n); + + this->SetPop(); + }, "Saves the mount entry.\n\n" + "NOTE: You must restart Sphaira for changes to take effect!"_i18n); + + // ensure that all fields are valid. + callback->Depends([this](){ + return + !m_name->GetValue().empty() && + !m_url->GetValue().empty(); + }, "Name and URL must be set!"_i18n); +} + +} // namespace + +void DisplayDevoptabSideBar() { + auto options = std::make_unique("Devoptab Options"_i18n, Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(std::move(options))); + + options->Add("Create New Entry"_i18n, [](){ + App::Push(); + }, "Creates a new mount option.\n\n" + "NOTE: You must restart Sphaira for changes to take effect!"_i18n); + + options->Add("Modify Existing Entry"_i18n, [](){ + PopupList::Items items; + TypeConfigs configs; + LoadAllConfigs(configs); + + for (const auto& e : configs) { + items.emplace_back(GetTypeName(e)); + } + + App::Push("Modify Entry"_i18n, items, [configs](std::optional index){ + if (!index.has_value()) { + return; + } + + const auto& entry = configs[index.value()]; + App::Push(entry.type.type, entry.config); + }); + }, "Modify an existing mount option.\n\n" + "NOTE: You must restart Sphaira for changes to take effect!"_i18n); + + options->Add("Delete Existing Entry"_i18n, [](){ + PopupList::Items items; + TypeConfigs configs; + LoadAllConfigs(configs); + + for (const auto& e : configs) { + items.emplace_back(GetTypeName(e)); + } + + App::Push("Delete Entry"_i18n, items, [configs](std::optional index){ + if (!index.has_value()) { + return; + } + + const auto& entry = configs[index.value()]; + const auto ini_path = BuildIniPathFromType(entry.type.type); + ini_puts(entry.config.name.c_str(), nullptr, nullptr, ini_path); + }); + }, "Delete an existing mount option.\n\n" + "NOTE: You must restart Sphaira for changes to take effect!"_i18n); +} + +} // namespace sphaira::devoptab diff --git a/sphaira/source/utils/devoptab_common.cpp b/sphaira/source/utils/devoptab_common.cpp index e894902..6d9ad7f 100644 --- a/sphaira/source/utils/devoptab_common.cpp +++ b/sphaira/source/utils/devoptab_common.cpp @@ -769,6 +769,58 @@ void update_devoptab_for_read_only(devoptab_t* devoptab, bool read_only) { } } +void LoadConfigsFromIni(const fs::FsPath& path, MountConfigs& out_configs) { + static const auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int { + auto e = static_cast(UserData); + if (!Section || !Key || !Value) { + return 1; + } + + // add new entry if use section changed. + if (e->empty() || std::strcmp(Section, e->back().name.c_str())) { + e->emplace_back(Section); + } + + if (!std::strcmp(Key, "url")) { + e->back().url = Value; + } else if (!std::strcmp(Key, "user")) { + e->back().user = Value; + } else if (!std::strcmp(Key, "pass")) { + e->back().pass = Value; + } else if (!std::strcmp(Key, "dump_path")) { + e->back().dump_path = Value; + } else if (!std::strcmp(Key, "port")) { + const auto port = ini_parse_getl(Value, -1); + if (port < 0 || port > 65535) { + log_write("[DEVOPTAB] INI: invalid port %s\n", Value); + } else { + e->back().port = port; + } + } else if (!std::strcmp(Key, "timeout")) { + e->back().timeout = ini_parse_getl(Value, e->back().timeout); + } else if (!std::strcmp(Key, "read_only")) { + e->back().read_only = ini_parse_getbool(Value, e->back().read_only); + } else if (!std::strcmp(Key, "no_stat_file")) { + e->back().no_stat_file = ini_parse_getbool(Value, e->back().no_stat_file); + } else if (!std::strcmp(Key, "no_stat_dir")) { + e->back().no_stat_dir = ini_parse_getbool(Value, e->back().no_stat_dir); + } else if (!std::strcmp(Key, "fs_hidden")) { + e->back().fs_hidden = ini_parse_getbool(Value, e->back().fs_hidden); + } else if (!std::strcmp(Key, "dump_hidden")) { + e->back().dump_hidden = ini_parse_getbool(Value, e->back().dump_hidden); + } else { + log_write("[DEVOPTAB] INI: extra key %s=%s\n", Key, Value); + e->back().extra.emplace(Key, Value); + } + + return 1; + }; + + out_configs.resize(0); + ini_browse(cb, &out_configs, path); + log_write("[DEVOPTAB] Found %zu mount configs\n", out_configs.size()); +} + bool MountNetworkDevice2(std::unique_ptr&& device, const MountConfig& config, size_t file_size, size_t dir_size, const char* name, const char* mount_name) { if (!device) { log_write("[DEVOPTAB] No device for %s\n", mount_name); @@ -877,60 +929,11 @@ Result MountNetworkDevice(const CreateDeviceCallback& create_device, size_t file SCOPED_RWLOCK(&g_rwlock, true); - using MountConfigs = std::vector; - - static const auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int { - auto e = static_cast(UserData); - if (!Section || !Key || !Value) { - return 1; - } - - // add new entry if use section changed. - if (e->empty() || std::strcmp(Section, e->back().name.c_str())) { - e->emplace_back(Section); - } - - if (!std::strcmp(Key, "url")) { - e->back().url = Value; - } else if (!std::strcmp(Key, "user")) { - e->back().user = Value; - } else if (!std::strcmp(Key, "pass")) { - e->back().pass = Value; - } else if (!std::strcmp(Key, "dump_path")) { - e->back().dump_path = Value; - } else if (!std::strcmp(Key, "port")) { - const auto port = ini_parse_getl(Value, -1); - if (port < 0 || port > 65535) { - log_write("[DEVOPTAB] INI: invalid port %s\n", Value); - } else { - e->back().port = port; - } - } else if (!std::strcmp(Key, "timeout")) { - e->back().timeout = ini_parse_getl(Value, e->back().timeout); - } else if (!std::strcmp(Key, "read_only")) { - e->back().read_only = ini_parse_getbool(Value, e->back().read_only); - } else if (!std::strcmp(Key, "no_stat_file")) { - e->back().no_stat_file = ini_parse_getbool(Value, e->back().no_stat_file); - } else if (!std::strcmp(Key, "no_stat_dir")) { - e->back().no_stat_dir = ini_parse_getbool(Value, e->back().no_stat_dir); - } else if (!std::strcmp(Key, "fs_hidden")) { - e->back().fs_hidden = ini_parse_getbool(Value, e->back().fs_hidden); - } else if (!std::strcmp(Key, "dump_hidden")) { - e->back().dump_hidden = ini_parse_getbool(Value, e->back().dump_hidden); - } else { - log_write("[DEVOPTAB] INI: extra key %s=%s\n", Key, Value); - e->back().extra.emplace(Key, Value); - } - - return 1; - }; - fs::FsPath config_path{}; std::snprintf(config_path, sizeof(config_path), "/config/sphaira/mount/%s.ini", name); MountConfigs configs{}; - ini_browse(cb, &configs, config_path); - log_write("[DEVOPTAB] Found %zu mount configs\n", configs.size()); + LoadConfigsFromIni(config_path, configs); for (auto& config : configs) { if (config.name.empty()) { @@ -1192,8 +1195,8 @@ bool MountCurlDevice::Mount() { return false; } - if (config.port.has_value()) { - rc = curl_url_set(curlu, CURLUPART_PORT, std::to_string(config.port.value()).c_str(), flags); + if (config.port > 0) { + rc = curl_url_set(curlu, CURLUPART_PORT, std::to_string(config.port).c_str(), flags); if (rc != CURLUE_OK) { log_write("[CURL] curl_url_set() port failed: %s\n", curl_url_strerror_wrap(rc)); } diff --git a/sphaira/source/utils/devoptab_sftp.cpp b/sphaira/source/utils/devoptab_sftp.cpp index 56ca385..cc16dd6 100644 --- a/sphaira/source/utils/devoptab_sftp.cpp +++ b/sphaira/source/utils/devoptab_sftp.cpp @@ -193,7 +193,7 @@ bool Device::Mount() { hints.ai_socktype = SOCK_STREAM; addrinfo* res{}; - const auto port = this->config.port.value_or(22); + const auto port = this->config.port > 0 ? this->config.port : 22; const auto port_str = std::to_string(port); auto ret = getaddrinfo(this->config.url.c_str(), port_str.c_str(), &hints, &res); if (ret != 0) {