From 385e149ccca98aa6d2be59d646fccdf7cd959ca1 Mon Sep 17 00:00:00 2001 From: xirvik Date: Sun, 16 Aug 2026 19:17:00 +0000 Subject: [PATCH 01/34] Let the download list own its downloads through a shared pointer. A multicall could then dispatch commands on an erased download. --- src/command_events.cc | 28 ++++++++++++--- src/command_tracker.cc | 4 +-- src/core/download.h | 1 + src/core/download_list.cc | 22 ++++++------ src/core/download_list.h | 13 +++++-- src/core/manager.cc | 8 ++--- src/core/view.cc | 56 ++++++++++++++++++++--------- src/core/view.h | 11 +++--- src/display/window_download_list.cc | 10 +++--- src/rpc/command_map.h | 5 +++ src/ui/download.cc | 7 ++-- src/ui/download.h | 3 +- src/ui/download_list.cc | 2 +- src/ui/element_download_list.cc | 2 +- 14 files changed, 116 insertions(+), 56 deletions(-) diff --git a/src/command_events.cc b/src/command_events.cc index 2afd600d..2538944a 100644 --- a/src/command_events.cc +++ b/src/command_events.cc @@ -51,7 +51,7 @@ apply_on_ratio(const torrent::Object& rawArgs) { !(max_ratio > 0 && total_upload * 100 > total_done * max_ratio)) continue; - downloads.push_back(*itr); + downloads.push_back(itr->get()); } auto ratio_command = "group." + group_name + ".ratio.command"; @@ -173,7 +173,7 @@ apply_close_low_diskspace(int64_t arg, uint32_t skip_priority) { torrent::FileList::cache_list cache; - for (auto download : *control->core()->download_list()) { + for (const auto& download : *control->core()->download_list()) { if (!download->is_downloading()) continue; if (download->priority() >= skip_priority) @@ -240,15 +240,25 @@ d_multicall(const torrent::Object::list_type& args) { // Add some pre-parsing of the commands, so we don't spend time // parsing and searching command map for every single call. - std::vector dlist((*view_itr)->begin_visible(), (*view_itr)->end_visible()); + // Hold a reference to each download so a command that erases one does not + // leave the rest of the loop dispatching on freed memory. + core::View::base_type dlist((*view_itr)->begin_visible(), (*view_itr)->end_visible()); torrent::Object resultRaw = torrent::Object::create_list(); torrent::Object::list_type& result = resultRaw.as_list(); - for (auto download : dlist) { + for (const auto& download : dlist) { + if (download.use_count() == 1) + continue; + torrent::Object::list_type& row = result.insert(result.end(), torrent::Object::create_list())->as_list(); for (torrent::Object::list_const_iterator cItr = ++args.begin(); cItr != args.end(); cItr++) { + // A command may erase this download, which destroys the torrent object it + // wraps; the list dropping its reference is what tells us. + if (download.use_count() == 1) + break; + auto& cmd = cItr->as_string(); row.push_back(rpc::parse_command(rpc::make_target(download), cmd.c_str(), cmd.c_str() + cmd.size()).first); } @@ -271,10 +281,12 @@ d_multicall_filtered(const torrent::Object::list_type& args) { if (view_itr == viewManager->end()) throw torrent::input_error("Could not find view '" + arg->as_string() + "'."); - // Make a filtered copy of the current item list + // Make a filtered copy of the current item list, holding a reference to each + // download so a command that erases one cannot free it under us. core::View::base_type dlist; (*view_itr)->filter_by(*++arg, dlist); + // Generate result by iterating over all items auto resultRaw = torrent::Object::create_list(); auto& result = resultRaw.as_list(); @@ -282,11 +294,17 @@ d_multicall_filtered(const torrent::Object::list_type& args) { ++arg; // skip to first command for (const auto& item : dlist) { + if (item.use_count() == 1) + continue; + // Add empty row to result torrent::Object::list_type& row = result.insert(result.end(), torrent::Object::create_list())->as_list(); // Call the provided commands and assemble their results for (torrent::Object::list_const_iterator command = arg; command != args.end(); command++) { + if (item.use_count() == 1) + break; + auto& cmdstr = command->as_string(); row.push_back(rpc::parse_command(rpc::make_target(item), cmdstr.c_str(), cmdstr.c_str() + cmdstr.size()).first); } diff --git a/src/command_tracker.cc b/src/command_tracker.cc index f600143a..49f851b1 100644 --- a/src/command_tracker.cc +++ b/src/command_tracker.cc @@ -68,11 +68,11 @@ apply_dht_add_node(const std::string& arg) { torrent::Object apply_enable_trackers(int64_t arg) { if (arg == 0) { - for (auto download : *control->core()->download_list()) + for (const auto& download : *control->core()->download_list()) download->tracker_controller().for_each([](auto& tracker) { tracker.disable(); }); } else { - for (auto download : *control->core()->download_list()) + for (const auto& download : *control->core()->download_list()) download->tracker_controller().for_each([](auto& tracker) { tracker.enable(); }); } diff --git a/src/core/download.h b/src/core/download.h index 67049c36..61c34867 100644 --- a/src/core/download.h +++ b/src/core/download.h @@ -1,6 +1,7 @@ #ifndef RTORRENT_CORE_DOWNLOAD_H #define RTORRENT_CORE_DOWNLOAD_H +#include #include #include #include diff --git a/src/core/download_list.cc b/src/core/download_list.cc index 79854c6a..a0acb4b2 100644 --- a/src/core/download_list.cc +++ b/src/core/download_list.cc @@ -37,7 +37,7 @@ namespace core { inline void DownloadList::check_contains([[maybe_unused]] Download* d) { #ifdef USE_EXTRA_DEBUG - if (std::find(begin(), end(), d) == end()) + if (std::find_if(begin(), end(), [d](const auto& entry) { return entry.get() == d; }) == end()) throw torrent::internal_error("DownloadList::check_contains(...) failed."); #endif } @@ -54,7 +54,6 @@ DownloadList::clear() { base_type::pop_back(); torrent::download_remove(*download->download()); - delete download; } catch (torrent::internal_error& e) { lt_log_print(torrent::LOG_ERROR, "DownloadList::clear() failed to close or remove download: %s", e.what()); @@ -70,7 +69,7 @@ DownloadList::clear() { void DownloadList::session_save() { for (auto& download : *this) - session_thread::manager()->save_resume_download(download); + session_thread::manager()->save_resume_download(download.get()); control->dht_manager()->save_dht_cache(); control->ui()->save_input_history(); @@ -78,7 +77,7 @@ DownloadList::session_save() { DownloadList::iterator DownloadList::find(const torrent::HashString& hash) { - return std::find_if(begin(), end(), [hash](Download* d) { return hash == d->info()->hash(); }); + return std::find_if(begin(), end(), [hash](const auto& d) { return hash == d->info()->hash(); }); } DownloadList::iterator @@ -91,14 +90,14 @@ DownloadList::find_hex(const char* hash) { if (torrent::utils::transform_from_hex(hash, hash + 40, key) != key.end()) return end(); - return std::find_if(begin(), end(), [key](Download* d) { return key == d->info()->hash(); }); + return std::find_if(begin(), end(), [key](const auto& d) { return key == d->info()->hash(); }); } Download* DownloadList::find_hex_ptr(const char* hash) { iterator itr = find_hex(hash); - return itr != end() ? *itr : NULL; + return itr != end() ? itr->get() : NULL; } Download* @@ -159,7 +158,7 @@ DownloadList::create(std::istream* str, uint32_t tracker_key, bool printLog) { DownloadList::iterator DownloadList::insert(Download* download) { - iterator itr = base_type::insert(end(), download); + iterator itr = base_type::insert(end(), std::shared_ptr(download)); lt_log_print_info(torrent::LOG_TORRENT_INFO, download->info(), "download_list", "Inserting download."); @@ -170,7 +169,7 @@ DownloadList::insert(Download* download) { // This needs to be separated into two different calls to ensure // the download remains in the view. for (auto v : *control->view_manager()) - v->insert(download); + v->insert(*itr); for (auto v : *control->view_manager()) v->filter_download(download); @@ -187,7 +186,7 @@ DownloadList::insert(Download* download) { void DownloadList::erase_ptr(Download* download) { - erase(std::find(begin(), end(), download)); + erase(std::find_if(begin(), end(), [download](const auto& entry) { return entry.get() == download; })); } DownloadList::iterator @@ -201,15 +200,14 @@ DownloadList::erase(iterator itr) { (*itr)->set_hash_failed(true); close(*itr); - session_thread::manager()->remove_download(*itr); + session_thread::manager()->remove_download(itr->get()); DL_TRIGGER_EVENT(*itr, "event.download.erased"); for (auto v : *control->view_manager()) - v->erase(*itr); + v->erase(itr->get()); torrent::download_remove(*(*itr)->download()); - delete *itr; return base_type::erase(itr); } diff --git a/src/core/download_list.h b/src/core/download_list.h index 40d24d83..e860876c 100644 --- a/src/core/download_list.h +++ b/src/core/download_list.h @@ -3,6 +3,7 @@ #include #include +#include #include namespace torrent { @@ -20,9 +21,9 @@ class Download; // // Fix apply_on_ratio if the base_type is changed. -class DownloadList : private std::list { +class DownloadList : private std::list> { public: - typedef std::list base_type; + typedef std::list> base_type; using base_type::iterator; using base_type::const_iterator; @@ -64,6 +65,14 @@ public: bool open(Download* d); void open_throw(Download* d); + // Overloads for the list's own entries, so callers iterating it do not need + // to unwrap. + bool open(const value_type& d) { return open(d.get()); } + void close(const value_type& d) { close(d.get()); } + void close_quick(const value_type& d) { close_quick(d.get()); } + void pause(const value_type& d) { pause(d.get()); } + void resume(const value_type& d) { resume(d.get()); } + void close(Download* d); void close_directly(Download* d); void close_quick(Download* d); diff --git a/src/core/manager.cc b/src/core/manager.cc index 340c0557..2e4d5f67 100644 --- a/src/core/manager.cc +++ b/src/core/manager.cc @@ -154,10 +154,10 @@ Manager::cleanup() { void Manager::shutdown(bool force) { if (!force) { - for (auto d : *m_download_list) - m_download_list->pause_default(d); + for (const auto& d : *m_download_list) + m_download_list->pause_default(d.get()); } else { - for (auto d : *m_download_list) + for (const auto& d : *m_download_list) m_download_list->close_quick(d); } } @@ -405,7 +405,7 @@ Manager::receive_hashing_changed() { continue; try { - m_download_list->open_throw(*itr); + m_download_list->open_throw(itr->get()); // Since the bitfield is allocated on loading of resume load or // hash start, and unallocated on close, we know that if it it diff --git a/src/core/view.cc b/src/core/view.cc index 4f5310ee..b5093867 100644 --- a/src/core/view.cc +++ b/src/core/view.cc @@ -16,10 +16,20 @@ namespace core { // Also add focus thingie here? +// Matches the list entry that owns a given download. +inline auto +entry_is(Download* download) { + return [download](const std::shared_ptr& entry) { return entry.get() == download; }; +} + struct view_downloads_compare { view_downloads_compare(const torrent::Object& cmd) : m_command(cmd) {} + bool operator()(const std::shared_ptr& d1, const std::shared_ptr& d2) const { + return (*this)(d1.get(), d2.get()); + } + bool operator()(Download* d1, Download* d2) const { try { if (m_command.is_empty()) @@ -54,6 +64,10 @@ struct view_downloads_filter { view_downloads_filter(const torrent::Object& cmd, const torrent::Object& cmd2) : m_command(cmd), m_command2(cmd2) {} + bool operator()(const std::shared_ptr& d1) const { + return (*this)(d1.get()); + } + bool operator()(Download* d1) const { return this->evalCmd(m_command, d1) && this->evalCmd(m_command2, d1); } @@ -152,7 +166,7 @@ View::initialize(const std::string& name) { void View::erase(Download* download) { - iterator itr = std::find(base_type::begin(), base_type::end(), download); + iterator itr = std::find_if(base_type::begin(), base_type::end(), entry_is(download)); if (itr >= end_visible()) { erase_internal(itr); @@ -165,22 +179,24 @@ View::erase(Download* download) { void View::set_visible(Download* download) { - iterator itr = std::find(begin_filtered(), end_filtered(), download); + iterator itr = std::find_if(begin_filtered(), end_filtered(), entry_is(download)); if (itr == end_filtered()) return; // Don't optimize erase since we want to keep the order of the // non-visible elements. + auto entry = *itr; + base_type::erase(itr); - insert_visible(download); + insert_visible(entry); rpc::call_object_nothrow(m_event_added, rpc::make_target(download)); } void View::set_not_visible(Download* download) { - iterator itr = std::find(begin_visible(), end_visible(), download); + iterator itr = std::find_if(begin_visible(), end_visible(), entry_is(download)); if (itr == end_visible()) return; @@ -190,8 +206,10 @@ View::set_not_visible(Download* download) { // Don't optimize erase since we want to keep the order of the // non-visible elements. + auto entry = *itr; + base_type::erase(itr); - base_type::push_back(download); + base_type::push_back(entry); rpc::call_object_nothrow(m_event_removed, rpc::make_target(download)); } @@ -241,12 +259,12 @@ View::prev_focus(unsigned int i) { void View::sort() { - Download* curFocus = focus() != end_visible() ? *focus() : NULL; + Download* curFocus = focus() != end_visible() ? focus()->get() : NULL; // Don't go randomly switching around equivalent elements. std::stable_sort(begin(), end_visible(), view_downloads_compare(m_sortCurrent)); - m_focus = position(std::find(begin(), end_visible(), curFocus)); + m_focus = position(std::find_if(begin(), end_visible(), entry_is(curFocus))); emit_changed(); } @@ -280,10 +298,10 @@ View::filter() { // set the elements to NULL as we trigger commands on them. Or // perhaps always clear them, thus not throwing anything. if (!m_event_removed.is_empty()) - std::for_each(changed.begin(), splitChanged, std::bind(&rpc::call_object_d_nothrow, m_event_removed, std::placeholders::_1)); + std::for_each(changed.begin(), splitChanged, [this](const auto& d) { rpc::call_object_d_nothrow(m_event_removed, d.get()); }); if (!m_event_added.is_empty()) - std::for_each(changed.begin(), splitChanged, std::bind(&rpc::call_object_d_nothrow, m_event_added, std::placeholders::_1)); + std::for_each(changed.begin(), splitChanged, [this](const auto& d) { rpc::call_object_d_nothrow(m_event_added, d.get()); }); emit_changed(); } @@ -294,21 +312,23 @@ View::filter_by(const torrent::Object& condition, View::base_type& result) { view_downloads_filter matches = view_downloads_filter(condition, m_temp_filter); for (iterator itr = begin_visible(); itr != end_visible(); ++itr) - if (matches(*itr)) + if (matches(itr->get())) result.push_back(*itr); } void View::filter_download(core::Download* download) { - iterator itr = std::find(base_type::begin(), base_type::end(), download); + iterator itr = std::find_if(base_type::begin(), base_type::end(), entry_is(download)); if (itr == base_type::end()) throw torrent::internal_error("View::filter_download(...) could not find download."); if (view_downloads_filter(m_filter, m_temp_filter)(download)) { if (itr >= end_visible()) { + auto entry = *itr; + erase_internal(itr); - insert_visible(download); + insert_visible(entry); rpc::call_object_nothrow(m_event_added, rpc::make_target(download)); @@ -317,16 +337,20 @@ View::filter_download(core::Download* download) { // already visible. // // Consider removing this. + auto entry = *itr; + erase_internal(itr); - insert_visible(download); + insert_visible(entry); } } else { if (itr >= end_visible()) return; + auto entry = *itr; + erase_internal(itr); - base_type::push_back(download); + base_type::push_back(entry); rpc::call_object_nothrow(m_event_removed, rpc::make_target(download)); } @@ -345,8 +369,8 @@ View::clear_filter_on() { } inline void -View::insert_visible(Download* d) { - auto itr = std::find_if(begin_visible(), end_visible(), [this, d](auto d2) { return view_downloads_compare(m_sortNew)(d, d2); }); +View::insert_visible(const std::shared_ptr& d) { + auto itr = std::find_if(begin_visible(), end_visible(), [this, &d](const auto& d2) { return view_downloads_compare(m_sortNew)(d.get(), d2.get()); }); m_size++; m_focus += (m_focus >= position(itr)); diff --git a/src/core/view.h b/src/core/view.h index abbf0bef..3ddd01f2 100644 --- a/src/core/view.h +++ b/src/core/view.h @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -26,9 +27,9 @@ namespace core { class Download; -class View : private std::vector { +class View : private std::vector> { public: - typedef std::vector base_type; + typedef std::vector> base_type; typedef std::function slot_void; typedef std::list signal_void; @@ -72,7 +73,7 @@ public: emit_changed(); } - void insert(Download* download) { base_type::push_back(download); } + void insert(const std::shared_ptr& download) { base_type::push_back(download); } void erase(Download* download); void set_visible(Download* download); @@ -124,9 +125,9 @@ private: View(const View&); void operator=(const View&); - void push_back(Download* d) { base_type::push_back(d); } + void push_back(const std::shared_ptr& d) { base_type::push_back(d); } - inline void insert_visible(Download* d); + inline void insert_visible(const std::shared_ptr& d); inline void erase_internal(iterator itr); void emit_changed(); diff --git a/src/display/window_download_list.cc b/src/display/window_download_list.cc index 39b5e067..9744e048 100644 --- a/src/display/window_download_list.cc +++ b/src/display/window_download_list.cc @@ -37,7 +37,7 @@ WindowDownloadList::set_view(core::View* l) { // Return a pair of ints, representing a) the ncurses attributes and b) the ncurses color pair ID to use std::pair WindowDownloadList::get_attr_color(core::View::iterator selected) { - core::Download* item = *selected; + core::Download* item = selected->get(); unsigned long focus_attr = selected == m_view->focus() ? m_canvas->attr_map().at(RCOLOR_FOCUS) : 0; int offset = (((selected - m_view->begin_visible()) & 1) + 1) * RCOLOR_MAX; // Determine the even/odd offset for the color pair bool active = item->is_open() && item->is_active(); @@ -132,15 +132,15 @@ WindowDownloadList::redraw() { ColorKind focus_color = is_focused ? RCOLOR_FOCUS : RCOLOR_LABEL; auto attr_color = get_attr_color(range.first); - print_download_title(buffer.data(), last, *range.first); + print_download_title(buffer.data(), last, range.first->get()); m_canvas->print(0, pos, "%c %s", focus_char, buffer.data()); m_canvas->set_attr(2, pos++, -1, attr_color.first, attr_color.second); - print_download_info_full(buffer.data(), last, *range.first); + print_download_info_full(buffer.data(), last, range.first->get()); m_canvas->print(0, pos, "%c %s", focus_char, buffer.data()); m_canvas->set_attr(2, pos++, -1, focus_color); - print_download_status(buffer.data(), last, *range.first); + print_download_status(buffer.data(), last, range.first->get()); m_canvas->print(0, pos, "%c %s", focus_char, buffer.data()); m_canvas->set_attr(2, pos++, -1, focus_color); @@ -152,7 +152,7 @@ WindowDownloadList::redraw() { char focus_char = range.first == m_view->focus() ? '*' : ' '; auto attr_color = get_attr_color(range.first); - print_download_info_compact(buffer.data(), last, *range.first); + print_download_info_compact(buffer.data(), last, range.first->get()); m_canvas->print(0, pos, "%c %s", focus_char, buffer.data()); m_canvas->set_attr(2, pos++, -1, attr_color.first, attr_color.second); diff --git a/src/rpc/command_map.h b/src/rpc/command_map.h index c67cb1fd..50472333 100644 --- a/src/rpc/command_map.h +++ b/src/rpc/command_map.h @@ -1,6 +1,7 @@ #ifndef RTORRENT_RPC_COMMAND_MAP_H #define RTORRENT_RPC_COMMAND_MAP_H +#include #include #include #include @@ -104,6 +105,10 @@ inline target_type make_target(T target) { return target_type((int)target_type_id::value, target); } +inline target_type make_target(const std::shared_ptr& target) { + return make_target(target.get()); +} + template inline target_type make_target_pair(T target1, T target2) { return target_type((int)target_type_id::value, target1, target2); diff --git a/src/ui/download.cc b/src/ui/download.cc index 9f8de921..de935da2 100644 --- a/src/ui/download.cc +++ b/src/ui/download.cc @@ -1,3 +1,4 @@ +#include #include "config.h" #include @@ -30,8 +31,10 @@ namespace ui { -Download::Download(core::Download* d) - : m_download(d) { +Download::Download(const std::shared_ptr& download) + : m_download(download.get()) { + + auto* d = download.get(); m_windowDownloadStatus = std::make_unique(d); m_windowDownloadStatus->set_bottom(true); diff --git a/src/ui/download.h b/src/ui/download.h index 2654a410..bb7a39ed 100644 --- a/src/ui/download.h +++ b/src/ui/download.h @@ -2,6 +2,7 @@ #define RTORRENT_UI_DOWNLOAD_H #include +#include #include #include "display/manager.h" @@ -36,7 +37,7 @@ public: DISPLAY_MAX_SIZE } Display; - Download(core::Download* d); + Download(const std::shared_ptr& d); ~Download(); void activate(display::Frame* frame, bool focus = true); diff --git a/src/ui/download_list.cc b/src/ui/download_list.cc index c9aca0ff..17c9eb97 100644 --- a/src/ui/download_list.cc +++ b/src/ui/download_list.cc @@ -89,7 +89,7 @@ DownloadList::unfocus_download(core::Download* d) { if (m_state == DISPLAY_DOWNLOAD && d == static_cast(m_uiArray[DISPLAY_DOWNLOAD])->download()) activate_display(DISPLAY_DOWNLOAD_LIST); - if (current_view()->focus() < current_view()->end_visible() && *current_view()->focus() == d) + if (current_view()->focus() < current_view()->end_visible() && current_view()->focus()->get() == d) current_view()->next_focus(); } diff --git a/src/ui/element_download_list.cc b/src/ui/element_download_list.cc index 11ecbc71..9f651fd6 100644 --- a/src/ui/element_download_list.cc +++ b/src/ui/element_download_list.cc @@ -196,7 +196,7 @@ ElementDownloadList::receive_cycle_throttle() { if (m_view->focus() == m_view->end_visible()) return; - core::Download* download = *m_view->focus(); + core::Download* download = m_view->focus()->get(); if (download->is_active()) { lt_log_print(torrent::LOG_TORRENT_WARN, "Cannot change throttle on active download."); return; From 4c6f7afdc7b5208cc121307160ac84b9d23873f6 Mon Sep 17 00:00:00 2001 From: Auska Date: Tue, 11 Aug 2026 06:29:07 +0800 Subject: [PATCH 02/34] Add network.local_port commands for tracker port reporting Mirror the network.local_address.* command family for ports: - network.local_port report the best-match local port - network.local_port.set set both the ipv4 and ipv6 local ports - network.local_port.ipv4(.set) get/set the ipv4 local port only - network.local_port.ipv6(.set) get/set the ipv6 local port only Values are restricted to 0-65535; 0 (default) means unset, and trackers then report the listening port. Why: behind multiple layers of NAT the port peers must connect to can differ from the port rtorrent is listening on. These commands let the user manually advertise the forwarded public port per address family, e.g.: network.local_port.ipv4.set = 6881 network.local_port.ipv6.set = 6882 network.local_port is marked safe for RPC use alongside network.local_address. --- doc/rtorrent.rc | 8 ++++++++ src/command_network.cc | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/doc/rtorrent.rc b/doc/rtorrent.rc index aa2e6fdf..eceb12c4 100644 --- a/doc/rtorrent.rc +++ b/doc/rtorrent.rc @@ -63,6 +63,14 @@ #network.local_address.set = 127.0.0.1 #network.local_address.set = rakshasa.no +# The port reported to the tracker, separate for ipv4/ipv6. A value of 0 +# (the default) falls back to reporting the listening port. network.local_port.set +# sets both the ipv4 and ipv6 ports at once. +# +#network.local_port.set = 6881 +#network.local_port.ipv4.set = 6881 +#network.local_port.ipv6.set = 6882 + # The IP address the listening socket and outgoing connections is # bound to. # diff --git a/src/command_network.cc b/src/command_network.cc index a8c6b027..23530f37 100644 --- a/src/command_network.cc +++ b/src/command_network.cc @@ -379,6 +379,25 @@ initialize_command_network() { CMD_ANY ("network.local_address.ipv6", [nw_config](auto, auto) { return nw_config->local_inet6_address_str(); }); CMD_ANY_STRING_V("network.local_address.ipv6.set", [nw_config](auto, auto& str) { return nw_config->set_local_inet6_address_str(str); }); + CMD_ANY ("network.local_port", [nw_config](auto, auto) { return nw_config->local_port_best_match(); }); + CMD_ANY_VALUE_V ("network.local_port.set", [nw_config](auto, auto& value) { + if (value < 0 || value > 65535) + throw torrent::input_error("Invalid local port number."); + return nw_config->set_local_port(value); + }); + CMD_ANY ("network.local_port.ipv4", [nw_config](auto, auto) { return nw_config->local_inet_port(); }); + CMD_ANY_VALUE_V ("network.local_port.ipv4.set", [nw_config](auto, auto& value) { + if (value < 0 || value > 65535) + throw torrent::input_error("Invalid local port number."); + return nw_config->set_local_inet_port(value); + }); + CMD_ANY ("network.local_port.ipv6", [nw_config](auto, auto) { return nw_config->local_inet6_port(); }); + CMD_ANY_VALUE_V ("network.local_port.ipv6.set", [nw_config](auto, auto& value) { + if (value < 0 || value > 65535) + throw torrent::input_error("Invalid local port number."); + return nw_config->set_local_inet6_port(value); + }); + CMD_ANY ("network.proxy.global", [](auto, auto) { return torrent::runtime::proxy_manager()->proxy_url(); }); CMD_ANY_STRING_V("network.proxy.global.set", [](auto, auto& str) { return torrent::runtime::proxy_manager()->set_proxy_url(str); }); CMD_ANY ("network.proxy.http", [](auto, auto) { return torrent::runtime::proxy_manager()->http_proxy_url(); }); @@ -437,6 +456,7 @@ initialize_command_network() { rpc::rpc.mark_safe("network.receive_buffer.size"); rpc::rpc.mark_safe("network.bind_address"); rpc::rpc.mark_safe("network.local_address"); + rpc::rpc.mark_safe("network.local_port"); rpc::rpc.mark_safe("network.xmlrpc.size_limit"); rpc::rpc.mark_safe("network.open_sockets"); From 8378c0a83ad5b9269ae09c95bb30b674811ca875 Mon Sep 17 00:00:00 2001 From: Auska Date: Mon, 17 Aug 2026 08:14:44 +0800 Subject: [PATCH 03/34] Fix review: mark ipv4/ipv6 local_port safe, dedup port validation - mark network.local_port.ipv4/ipv6 safe for untrusted RPC connections (mark_safe matches exact command keys). - Replace duplicated multi-line port validation with a shared checked_local_port_value helper and single-line lambdas. --- src/command_network.cc | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/command_network.cc b/src/command_network.cc index 23530f37..660d238d 100644 --- a/src/command_network.cc +++ b/src/command_network.cc @@ -54,6 +54,14 @@ set_listen_port_range(const std::string& arg) { torrent::runtime::client_config()->set_listen_port_range(port_first, port_last); } +uint16_t +checked_local_port_value(int64_t value, const char* label) { + if (value < 0 || value > 65535) + throw torrent::input_error(std::string("Invalid ") + label + " port number."); + + return static_cast(value); +} + torrent::Object get_encryption() { auto encryption_modes = torrent::runtime::network_config()->encryption_modes(); @@ -380,23 +388,11 @@ initialize_command_network() { CMD_ANY_STRING_V("network.local_address.ipv6.set", [nw_config](auto, auto& str) { return nw_config->set_local_inet6_address_str(str); }); CMD_ANY ("network.local_port", [nw_config](auto, auto) { return nw_config->local_port_best_match(); }); - CMD_ANY_VALUE_V ("network.local_port.set", [nw_config](auto, auto& value) { - if (value < 0 || value > 65535) - throw torrent::input_error("Invalid local port number."); - return nw_config->set_local_port(value); - }); + CMD_ANY_VALUE_V ("network.local_port.set", [nw_config](auto, auto& value) { return nw_config->set_local_port(checked_local_port_value(value, "local")); }); CMD_ANY ("network.local_port.ipv4", [nw_config](auto, auto) { return nw_config->local_inet_port(); }); - CMD_ANY_VALUE_V ("network.local_port.ipv4.set", [nw_config](auto, auto& value) { - if (value < 0 || value > 65535) - throw torrent::input_error("Invalid local port number."); - return nw_config->set_local_inet_port(value); - }); + CMD_ANY_VALUE_V ("network.local_port.ipv4.set", [nw_config](auto, auto& value) { return nw_config->set_local_inet_port(checked_local_port_value(value, "local ipv4")); }); CMD_ANY ("network.local_port.ipv6", [nw_config](auto, auto) { return nw_config->local_inet6_port(); }); - CMD_ANY_VALUE_V ("network.local_port.ipv6.set", [nw_config](auto, auto& value) { - if (value < 0 || value > 65535) - throw torrent::input_error("Invalid local port number."); - return nw_config->set_local_inet6_port(value); - }); + CMD_ANY_VALUE_V ("network.local_port.ipv6.set", [nw_config](auto, auto& value) { return nw_config->set_local_inet6_port(checked_local_port_value(value, "local ipv6")); }); CMD_ANY ("network.proxy.global", [](auto, auto) { return torrent::runtime::proxy_manager()->proxy_url(); }); CMD_ANY_STRING_V("network.proxy.global.set", [](auto, auto& str) { return torrent::runtime::proxy_manager()->set_proxy_url(str); }); @@ -457,6 +453,8 @@ initialize_command_network() { rpc::rpc.mark_safe("network.bind_address"); rpc::rpc.mark_safe("network.local_address"); rpc::rpc.mark_safe("network.local_port"); + rpc::rpc.mark_safe("network.local_port.ipv4"); + rpc::rpc.mark_safe("network.local_port.ipv6"); rpc::rpc.mark_safe("network.xmlrpc.size_limit"); rpc::rpc.mark_safe("network.open_sockets"); From 7db356cae4f280a264d654d5d04b63f3446b9d2b Mon Sep 17 00:00:00 2001 From: xirvik Date: Mon, 17 Aug 2026 15:11:53 +0000 Subject: [PATCH 04/34] Add a depth limit to json_to_object. Deeply nested params recursed until the main thread's stack was exhausted. --- src/rpc/jsonrpc.cc | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/rpc/jsonrpc.cc b/src/rpc/jsonrpc.cc index 77e68479..395b5999 100644 --- a/src/rpc/jsonrpc.cc +++ b/src/rpc/jsonrpc.cc @@ -26,10 +26,12 @@ constexpr int JSONRPC_METHOD_NOT_FOUND_ERROR = -32601; constexpr int JSONRPC_INVALID_PARAMS_ERROR = -32602; constexpr int JSONRPC_INTERNAL_ERROR = -32000; +constexpr uint32_t max_json_depth = 1024; + using json = nlohmann::json; torrent::Object -json_to_object(const json& value) { +json_to_object(const json& value, uint32_t depth = 0) { switch (value.type()) { case json::value_t::number_unsigned: case json::value_t::number_integer: @@ -42,20 +44,26 @@ json_to_object(const json& value) { return torrent::Object(value.get()); case json::value_t::array: { + if (++depth >= max_json_depth) + throw torrent::input_error("invalid parameters: max depth reached"); + auto array_raw = torrent::Object::create_list(); auto& array = array_raw.as_list(); for (const auto& entry : value) - array.push_back(json_to_object(entry)); + array.push_back(json_to_object(entry, depth)); return array_raw; } case json::value_t::object: { + if (++depth >= max_json_depth) + throw torrent::input_error("invalid parameters: max depth reached"); + auto map_raw = torrent::Object::create_map(); auto& map = map_raw.as_map(); for (const auto& entry : value.items()) - map[entry.key()] = json_to_object(entry.value()); + map[entry.key()] = json_to_object(entry.value(), depth); return map_raw; } From d73f245692ce3738f338e85c1f15110994eae520 Mon Sep 17 00:00:00 2001 From: xirvik Date: Mon, 17 Aug 2026 15:16:28 +0000 Subject: [PATCH 05/34] Add a depth limit to the command parser. A million nested braces in one argument exhausted the main thread's stack. --- src/rpc/parse.cc | 35 ++++++++++++++++++++--------------- src/rpc/parse.h | 6 +++--- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/rpc/parse.cc b/src/rpc/parse.cc index 3953fa84..4bdaa6c7 100644 --- a/src/rpc/parse.cc +++ b/src/rpc/parse.cc @@ -10,6 +10,8 @@ namespace rpc { +constexpr uint32_t max_parse_depth = 1024; + const char* parse_skip_wspace(const char* first, const char* last) { while (first != last && parse_is_space(*first)) @@ -142,10 +144,13 @@ parse_value_nothrow(const char* src, int64_t* value, int base, int unit) { // Somewhat ugly... const char* -parse_object(const char* first, const char* last, torrent::Object* dest, bool (*delim)(const char)) { +parse_object(const char* first, const char* last, torrent::Object* dest, bool (*delim)(const char), uint32_t depth) { + if (++depth >= max_parse_depth) + throw torrent::input_error("Max parse depth reached."); + if (*first == '{') { *dest = torrent::Object::create_list(); - first = parse_list(first + 1, last, dest, &parse_is_delim_block); + first = parse_list(first + 1, last, dest, &parse_is_delim_block, depth); first = parse_skip_wspace(first, last); if (first == last || *first != '}') @@ -154,18 +159,18 @@ parse_object(const char* first, const char* last, torrent::Object* dest, bool (* return ++first; } else if (*first == '(') { - int32_t depth = 1; + int32_t parentheses = 1; while (first + 1 != last && *(first + 1) == '(') { first++; - depth++; + parentheses++; } - if (depth > 3) + if (parentheses > 3) throw torrent::input_error("Max 3 parentheses per object allowed."); *dest = torrent::Object::create_dict_key(); - dest->set_flags(torrent::Object::flag_function << (depth - 1)); + dest->set_flags(torrent::Object::flag_function << (parentheses - 1)); first = parse_string(first + 1, last, &dest->as_dict_key(), &parse_is_delim_func); first = parse_skip_wspace(first, last); @@ -176,16 +181,16 @@ parse_object(const char* first, const char* last, torrent::Object* dest, bool (* if (*first == ',') { // This will always create a list even for single argument functions... dest->as_dict_obj() = torrent::Object::create_list(); - first = parse_list(first + 1, last, &dest->as_dict_obj(), &parse_is_delim_func); + first = parse_list(first + 1, last, &dest->as_dict_obj(), &parse_is_delim_func, depth); first = parse_skip_wspace(first, last); } - while (depth != 0 && first != last && *first == ')') { + while (parentheses != 0 && first != last && *first == ')') { first++; - depth--; + parentheses--; } - if (depth != 0) + if (parentheses != 0) throw torrent::input_error("Parentheses mismatch."); return first; @@ -198,7 +203,7 @@ parse_object(const char* first, const char* last, torrent::Object* dest, bool (* } const char* -parse_list(const char* first, const char* last, torrent::Object* dest, bool (*delim)(const char)) { +parse_list(const char* first, const char* last, torrent::Object* dest, bool (*delim)(const char), uint32_t depth) { if (!dest->is_list()) throw torrent::internal_error("parse_list(...) !dest->is_list()."); @@ -206,7 +211,7 @@ parse_list(const char* first, const char* last, torrent::Object* dest, bool (*de torrent::Object tmp; first = parse_skip_wspace(first, last); - first = parse_object(first, last, &tmp, delim); + first = parse_object(first, last, &tmp, delim, depth); first = parse_skip_wspace(first, last); dest->as_list().push_back(tmp); @@ -221,9 +226,9 @@ parse_list(const char* first, const char* last, torrent::Object* dest, bool (*de } const char* -parse_whole_list(const char* first, const char* last, torrent::Object* dest, bool (*delim)(const char)) { +parse_whole_list(const char* first, const char* last, torrent::Object* dest, bool (*delim)(const char), uint32_t depth) { first = parse_skip_wspace(first, last); - first = parse_object(first, last, dest, delim); + first = parse_object(first, last, dest, delim, depth); first = parse_skip_wspace(first, last); if (first != last && parse_is_seperator(*first)) { @@ -231,7 +236,7 @@ parse_whole_list(const char* first, const char* last, torrent::Object* dest, boo tmp.swap(*dest); dest->as_list().push_back(tmp); - first = parse_list(++first, last, dest, delim); + first = parse_list(++first, last, dest, delim, depth); } return first; diff --git a/src/rpc/parse.h b/src/rpc/parse.h index 4af4b7c1..a7b8f422 100644 --- a/src/rpc/parse.h +++ b/src/rpc/parse.h @@ -77,9 +77,9 @@ const char* parse_value_nothrow(const char* first, const char* last, int64_t* va void parse_whole_value(const char* src, int64_t* value, int base = 0, int unit = 1); bool parse_whole_value_nothrow(const char* src, int64_t* value, int base = 0, int unit = 1); -const char* parse_object (const char* first, const char* last, torrent::Object* dest, bool (*delim)(const char) = &parse_is_delim_default); -const char* parse_list (const char* first, const char* last, torrent::Object* dest, bool (*delim)(const char) = &parse_is_delim_default); -const char* parse_whole_list(const char* first, const char* last, torrent::Object* dest, bool (*delim)(const char) = &parse_is_delim_default); +const char* parse_object (const char* first, const char* last, torrent::Object* dest, bool (*delim)(const char) = &parse_is_delim_default, uint32_t depth = 0); +const char* parse_list (const char* first, const char* last, torrent::Object* dest, bool (*delim)(const char) = &parse_is_delim_default, uint32_t depth = 0); +const char* parse_whole_list(const char* first, const char* last, torrent::Object* dest, bool (*delim)(const char) = &parse_is_delim_default, uint32_t depth = 0); std::string convert_to_string(const torrent::Object& src); From 13e073d9ffddded1ca418f24e91fca01da13a2b4 Mon Sep 17 00:00:00 2001 From: xirvik Date: Mon, 17 Aug 2026 15:19:31 +0000 Subject: [PATCH 06/34] Reject division overflow in math.div and math.mod. Dividing the smallest integer by -1 traps and takes the process with it. --- src/command_ui.cc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/command_ui.cc b/src/command_ui.cc index f6222ac2..2658e00c 100644 --- a/src/command_ui.cc +++ b/src/command_ui.cc @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -674,8 +675,13 @@ apply_math_basic(const char* name, Comp op, const torrent::Object::list_type& ar throw torrent::input_error(std::string(name) + ": Wrong argument type"); } - if (divides && !rhs && itr != args.begin()) - throw torrent::input_error(std::string(name) + ": Division by zero!"); + if (divides && itr != args.begin()) { + if (rhs == 0) + throw torrent::input_error(std::string(name) + ": Division by zero!"); + + if (val == std::numeric_limits::min() && rhs == -1) + throw torrent::input_error(std::string(name) + ": Division overflow!"); + } val = itr == args.begin() ? rhs : op(val, rhs); From a2fabb10de82f1f5ad290fc718019e276def7e79 Mon Sep 17 00:00:00 2001 From: xirvik Date: Mon, 17 Aug 2026 15:23:44 +0000 Subject: [PATCH 07/34] Range check the socket allocation and size values. int64 arguments were truncated into uint32 setters, so -1 became 4294967295. --- src/command_local.cc | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/command_local.cc b/src/command_local.cc index 2a7d983b..96ade552 100644 --- a/src/command_local.cc +++ b/src/command_local.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -185,6 +186,14 @@ cmd_file_append(const torrent::Object::list_type& args) { return torrent::Object(); } +uint32_t +checked_socket_value(int64_t value, const char* label) { + if (value < 0 || value > std::numeric_limits::max()) + throw torrent::input_error(std::string("Invalid ") + label + " value."); + + return static_cast(value); +} + void initialize_command_local() { core::DownloadList* dList = control->core()->download_list(); @@ -241,7 +250,7 @@ initialize_command_local() { CMD_ANY ("system.sockets.size", [](auto, auto) { return torrent::runtime::socket_manager()->size(); }); CMD_ANY ("system.sockets.max_size", [](auto, auto) { return torrent::runtime::socket_manager()->max_size(); }); - CMD_ANY_VALUE_V ("system.sockets.max_size.set", [](auto, auto& value) { return torrent::runtime::socket_manager()->set_max_size_and_adjust(value); }); + CMD_ANY_VALUE_V ("system.sockets.max_size.set", [](auto, auto& value) { return torrent::runtime::socket_manager()->set_max_size_and_adjust(checked_socket_value(value, "socket max size")); }); CMD_ANY_V ("system.sockets.adjust_alloc", [](auto, auto) { torrent::runtime::socket_manager()->adjust_allocation(); }); CMD_ANY ("system.sockets.reserved_alloc", [](auto, auto) { return torrent::runtime::socket_manager()->reserved_allocation(); }); CMD_ANY ("system.sockets.available_alloc", [](auto, auto) { return torrent::runtime::socket_manager()->available_allocation(); }); @@ -262,8 +271,8 @@ initialize_command_local() { CMD_ANY (category_name + ".min_alloc.limit", [category](auto, auto) { return torrent::runtime::socket_manager()->category_alloc_minimum(category); }); CMD_ANY (category_name + ".min_alloc", [category](auto, auto) { return torrent::runtime::socket_manager()->category_min_allocation(category); }); CMD_ANY (category_name + ".max_alloc", [category](auto, auto) { return torrent::runtime::socket_manager()->category_max_allocation(category); }); - CMD_ANY_VALUE_V(category_name + ".min_alloc.set", [category](auto, auto& value) { torrent::runtime::socket_manager()->set_category_min_allocation(category, value); }); - CMD_ANY_VALUE_V(category_name + ".max_alloc.set", [category](auto, auto& value) { torrent::runtime::socket_manager()->set_category_max_allocation(category, value); }); + CMD_ANY_VALUE_V(category_name + ".min_alloc.set", [category](auto, auto& value) { torrent::runtime::socket_manager()->set_category_min_allocation(category, checked_socket_value(value, "socket min alloc")); }); + CMD_ANY_VALUE_V(category_name + ".max_alloc.set", [category](auto, auto& value) { torrent::runtime::socket_manager()->set_category_max_allocation(category, checked_socket_value(value, "socket max alloc")); }); } CMD_ANY ("pieces.sync.always_safe", [](auto, auto) { return torrent::runtime::memory_manager()->safe_sync(); }); From 9066afc0637d12a8b1beb50f1909b22d2d7779b7 Mon Sep 17 00:00:00 2001 From: xirvik Date: Mon, 17 Aug 2026 15:21:24 +0000 Subject: [PATCH 08/34] Guard the unit multiplication in value commands against overflow. The kb variants multiply the argument by 1024 without checking the range. --- src/rpc/command.cc | 10 +++++++++- src/rpc/parse.cc | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/rpc/command.cc b/src/rpc/command.cc index 0bd7da79..26c45b3c 100644 --- a/src/rpc/command.cc +++ b/src/rpc/command.cc @@ -1,5 +1,7 @@ #include "config.h" +#include + #include "core/download.h" #include "parse.h" @@ -41,7 +43,13 @@ command_base_call_value_base(command_base* command_raw, target_type target, cons return command_base::_call::type, T>(command_raw, target, val); } - return command_base::_call::type, T>(command_raw, target, unit * arg.as_value()); + auto value = arg.as_value(); + + if (value > std::numeric_limits::max() / unit || + value < std::numeric_limits::min() / unit) + throw torrent::input_error("Value out of range."); + + return command_base::_call::type, T>(command_raw, target, unit * value); } template const torrent::Object diff --git a/src/rpc/parse.cc b/src/rpc/parse.cc index 4bdaa6c7..e9407bdc 100644 --- a/src/rpc/parse.cc +++ b/src/rpc/parse.cc @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -136,7 +137,13 @@ parse_value_nothrow(const char* src, int64_t* value, int base, int unit) { // case ' ': // case '\0': *value = *value * unit; break; // default: throw torrent::input_error("Could not parse value."); - default: *value = *value * unit; break; + default: + if (*value > std::numeric_limits::max() / unit || + *value < std::numeric_limits::min() / unit) + return src; // overflow guard + + *value = *value * unit; + break; } return last; From af6d8a10ada22458cbb1744490fccfabd238b82b Mon Sep 17 00:00:00 2001 From: xirvik Date: Mon, 17 Aug 2026 15:31:20 +0000 Subject: [PATCH 09/34] Add a timeout to scgi tasks. A hundred connections sending a partial header held the whole task pool. --- src/rpc/scgi_task.cc | 11 +++++++++++ src/rpc/scgi_task.h | 9 +++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/rpc/scgi_task.cc b/src/rpc/scgi_task.cc index 647851b7..ed7a5e2d 100644 --- a/src/rpc/scgi_task.cc +++ b/src/rpc/scgi_task.cc @@ -14,6 +14,7 @@ #include #include #include +#include #include #include "control.h" @@ -27,6 +28,8 @@ namespace rpc { SCgiTask::SCgiTask() : m_callback_id(torrent::system::make_callback_id()) { + m_task_timeout.slot() = [this]() { close(); }; + reset_file_descriptor(); } @@ -50,6 +53,8 @@ SCgiTask::open(SCgi* parent, int fd) { torrent::this_thread::poll()->open(this); torrent::this_thread::poll()->insert_read(this); + torrent::this_thread::scheduler()->update_wait_for_ceil_seconds(&m_task_timeout, timeout_request); + auto lock = std::lock_guard(m_result_mutex); // Leave room for terminating nul byte for parsing the header. @@ -61,6 +66,8 @@ SCgiTask::cancel_open() { if (!is_open()) return; + torrent::this_thread::scheduler()->erase(&m_task_timeout); + torrent::this_thread::poll()->remove_and_close(this); torrent::fd_close(file_descriptor()); @@ -72,6 +79,8 @@ SCgiTask::close() { if (!is_open()) return; + torrent::this_thread::scheduler()->erase(&m_task_timeout); + torrent::system::cancel_callback_and_wait(m_callback_id, scgi_thread::thread(), torrent::main_thread::thread()); torrent::runtime::socket_manager()->close_event_or_throw(this, [this]() { @@ -158,6 +167,8 @@ SCgiTask::event_read() { torrent::this_thread::poll()->remove_read(this); + torrent::this_thread::scheduler()->update_wait_for_ceil_seconds(&m_task_timeout, timeout_request); + if (m_parent->log_fd() >= 0) { [[maybe_unused]] int result; diff --git a/src/rpc/scgi_task.h b/src/rpc/scgi_task.h index bffe8101..33b7c181 100644 --- a/src/rpc/scgi_task.h +++ b/src/rpc/scgi_task.h @@ -1,10 +1,12 @@ #ifndef RTORRENT_RPC_SCGI_TASK_H #define RTORRENT_RPC_SCGI_TASK_H +#include #include #include #include #include +#include namespace rpc { @@ -16,6 +18,8 @@ public: static constexpr int max_header_size = 2000; static constexpr int max_content_size = (2 << 23); + static constexpr auto timeout_request = std::chrono::seconds(60); + enum ContentType { XML, JSON }; SCgiTask(); @@ -54,8 +58,9 @@ private: void plaintext_response(const char* buffer, uint32_t content_length); void gzip_response(const char* buffer, uint32_t content_length); - SCgi* m_parent{}; - torrent::system::callback_id m_callback_id; + SCgi* m_parent{}; + torrent::system::callback_id m_callback_id; + torrent::system::SchedulerEntry m_task_timeout; std::mutex m_result_mutex; From 3ae91b65365d119d0121db5c2e26fd32c1204a19 Mon Sep 17 00:00:00 2001 From: xirvik Date: Mon, 17 Aug 2026 15:24:44 +0000 Subject: [PATCH 10/34] Check flag_public_rpc when dispatching jsonrpc calls. Private commands and non-exported redirects were reachable over jsonrpc. --- src/rpc/jsonrpc.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc/jsonrpc.cc b/src/rpc/jsonrpc.cc index 395b5999..611c35aa 100644 --- a/src/rpc/jsonrpc.cc +++ b/src/rpc/jsonrpc.cc @@ -146,7 +146,7 @@ jsonrpc_call_command(const std::string& method, const json& params) { CommandMap::iterator itr = commands.find(method.c_str()); - if (itr == commands.end()) { + if (itr == commands.end() || !(itr->second.m_flags & CommandMap::flag_public_rpc)) { throw rpc_error(JSONRPC_METHOD_NOT_FOUND_ERROR, "method not found: " + method); } From dcfbf82a38a068e011f3441b5021eac0c37d0b54 Mon Sep 17 00:00:00 2001 From: xirvik Date: Tue, 18 Aug 2026 04:51:49 +0000 Subject: [PATCH 11/34] Mark view.list and the system.time getters as untrusted safe. Read-only getters a client needs to list views and make sense of timestamps. --- src/command_local.cc | 3 +++ src/command_ui.cc | 1 + 2 files changed, 4 insertions(+) diff --git a/src/command_local.cc b/src/command_local.cc index 96ade552..c1f320d7 100644 --- a/src/command_local.cc +++ b/src/command_local.cc @@ -361,6 +361,9 @@ initialize_command_local() { rpc::rpc.mark_safe("system.api_version"); rpc::rpc.mark_safe("system.client_version"); rpc::rpc.mark_safe("system.library_version"); + rpc::rpc.mark_safe("system.time"); + rpc::rpc.mark_safe("system.time_seconds"); + rpc::rpc.mark_safe("system.time_usec"); rpc::rpc.mark_safe("system.file.max_size"); rpc::rpc.mark_safe("system.file.split_size"); rpc::rpc.mark_safe("system.file.split_suffix"); diff --git a/src/command_ui.cc b/src/command_ui.cc index 2658e00c..6e0bd8c8 100644 --- a/src/command_ui.cc +++ b/src/command_ui.cc @@ -922,6 +922,7 @@ initialize_command_ui() { rpc::rpc.mark_safe("view.set_visible"); rpc::rpc.mark_safe("view.set_not_visible"); + rpc::rpc.mark_safe("view.list"); rpc::rpc.mark_safe("cat"); rpc::rpc.mark_safe("if"); From d7b2cfe4f20b08672129679ee3d3a89528920c46 Mon Sep 17 00:00:00 2001 From: xirvik Date: Fri, 21 Aug 2026 16:02:07 +0000 Subject: [PATCH 12/34] Clamp the elapsed time conversion instead of overflowing. convert.elapsed_time subtracted an unchecked value from the cached seconds. --- src/command_ui.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/command_ui.cc b/src/command_ui.cc index 6e0bd8c8..1d53b1e6 100644 --- a/src/command_ui.cc +++ b/src/command_ui.cc @@ -408,8 +408,9 @@ apply_to_time(const torrent::Object& rawArgs, int flags) { torrent::Object apply_to_elapsed_time(const torrent::Object& rawArgs) { auto cached_seconds = torrent::this_thread::cached_seconds().count(); + auto value = rawArgs.as_value(); - uint64_t arg = cached_seconds - rawArgs.as_value(); + uint64_t arg = value >= 0 && value <= cached_seconds ? cached_seconds - value : 0; char buffer[48]; snprintf(buffer, 48, "%2d:%02d:%02d", (int)(arg / 3600), (int)((arg / 60) % 60), (int)(arg % 60)); From ac1b2685d5ed224b36c2ded94694c1ff8350ca1d Mon Sep 17 00:00:00 2001 From: xirvik Date: Thu, 20 Aug 2026 05:00:29 +0000 Subject: [PATCH 13/34] Guard the value suffixes against negative overflow. The k, m and g guards only checked the positive side, so -4611686018427387904K became 0. --- src/rpc/parse.cc | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/rpc/parse.cc b/src/rpc/parse.cc index e9407bdc..e49dde66 100644 --- a/src/rpc/parse.cc +++ b/src/rpc/parse.cc @@ -102,6 +102,12 @@ parse_whole_value_nothrow(const char* src, int64_t* value, int base, int unit) { return true; } +static bool +value_fits_shifted(int64_t value, int shift) { + return value <= (std::numeric_limits::max() >> shift) && + value >= (std::numeric_limits::min() >> shift); +} + const char* parse_value_nothrow(const char* src, int64_t* value, int base, int unit) { if (unit <= 0) @@ -124,15 +130,15 @@ parse_value_nothrow(const char* src, int64_t* value, int base, int unit) { case 'B': ++last; break; case 'k': case 'K': - if (*value > (int64_t)0x1FFFFFFFFFFFFF) return src; // overflow guard + if (!value_fits_shifted(*value, 10)) return src; // overflow guard *value = *value << 10; ++last; break; case 'm': case 'M': - if (*value > (int64_t)0x7FFFFFFFFFF) return src; // overflow guard + if (!value_fits_shifted(*value, 20)) return src; // overflow guard *value = *value << 20; ++last; break; case 'g': case 'G': - if (*value > (int64_t)0x1FFFFFFFF) return src; // overflow guard + if (!value_fits_shifted(*value, 30)) return src; // overflow guard *value = *value << 30; ++last; break; // case ' ': // case '\0': *value = *value * unit; break; From 109a20c09c3cab9eb13c2d96ea79362ac6c318fc Mon Sep 17 00:00:00 2001 From: rakshasa Date: Wed, 26 Aug 2026 08:51:40 +0200 Subject: [PATCH 14/34] Tagged release 0.16.21. --- configure.ac | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/configure.ac b/configure.ac index 5d75cf3d..cc459306 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ m4_pattern_allow([PKG_CHECK_EXISTS]) -AC_INIT([rtorrent],[0.16.20],[sundell.software@gmail.com]) +AC_INIT([rtorrent],[0.16.21],[sundell.software@gmail.com]) AC_CONFIG_HEADERS([config.h]) AC_CONFIG_MACRO_DIRS([scripts]) @@ -14,7 +14,7 @@ AX_CXX_COMPILE_STDCXX(20, noext, mandatory) PKG_PROG_PKG_CONFIG -AC_DEFINE([API_VERSION], [25], [api version]) +AC_DEFINE([API_VERSION], [26], [api version]) RAK_CHECK_CFLAGS RAK_CHECK_CXXFLAGS @@ -49,7 +49,7 @@ fi PKG_CHECK_MODULES([CPPUNIT], [cppunit],, [no_cppunit="yes"]) PKG_CHECK_MODULES([ZLIB], [zlib]) -PKG_CHECK_MODULES([DEPENDENCIES], [libtorrent >= 0.16.20]) +PKG_CHECK_MODULES([DEPENDENCIES], [libtorrent >= 0.16.21]) AC_LANG_PUSH(C++) TORRENT_WITH_XMLRPC_C From 8be1fcb3a4db89abcd6e8c9e3e66a5a48237345c Mon Sep 17 00:00:00 2001 From: xirvik Date: Wed, 26 Aug 2026 02:03:53 +0000 Subject: [PATCH 15/34] Return m_use_fsyncdisk from SessionManager::use_fsyncdisk. system.files.session.fdatasync always reported true even when disabled. --- src/session/session_manager.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/session/session_manager.h b/src/session/session_manager.h index eff135ac..5a135552 100644 --- a/src/session/session_manager.h +++ b/src/session/session_manager.h @@ -126,7 +126,7 @@ private: inline bool SessionManager::is_used() const { return !m_path.empty(); } inline std::string SessionManager::path() const { return m_path; } -inline bool SessionManager::use_fsyncdisk() const { return true; } +inline bool SessionManager::use_fsyncdisk() const { return m_use_fsyncdisk; } inline bool SessionManager::use_lock() const { return m_use_lock; } inline void SessionManager::flush_all_pending_builds() { process_pending_builds(true); } From 2cd3ca052de821ef1340e2b95a96f1a40e45bdcb Mon Sep 17 00:00:00 2001 From: xirvik Date: Wed, 26 Aug 2026 02:33:53 +0000 Subject: [PATCH 16/34] Pass the gz flag to log.append_gz_file. --- src/command_logging.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command_logging.cc b/src/command_logging.cc index 3ee952c8..8dd47cd3 100644 --- a/src/command_logging.cc +++ b/src/command_logging.cc @@ -135,7 +135,7 @@ initialize_command_logging() { CMD2_ANY_LIST ("log.open_gz_file_pid", std::bind(&apply_log_open, log_flag_append_pid | log_flag_use_gz, std::placeholders::_2)); CMD2_ANY_LIST ("log.append_file", std::bind(&apply_log_open, log_flag_append_file, std::placeholders::_2)); CMD2_ANY_LIST ("log.append_file.flush", std::bind(&apply_log_open, log_flag_append_file | log_flag_flush, std::placeholders::_2)); - CMD2_ANY_LIST ("log.append_gz_file", std::bind(&apply_log_open, log_flag_append_file, std::placeholders::_2)); + CMD2_ANY_LIST ("log.append_gz_file", std::bind(&apply_log_open, log_flag_append_file | log_flag_use_gz, std::placeholders::_2)); CMD2_ANY_STRING_V("log.close", std::bind(&torrent::log_close_output_str, std::placeholders::_2)); From c8da93bd30388333e88067fdbedc095b4c9fecf6 Mon Sep 17 00:00:00 2001 From: xirvik Date: Wed, 26 Aug 2026 02:33:53 +0000 Subject: [PATCH 17/34] Warn that pieces.sync.timeout_safe.set is no longer supported. --- src/command_local.cc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/command_local.cc b/src/command_local.cc index c1f320d7..b4461d74 100644 --- a/src/command_local.cc +++ b/src/command_local.cc @@ -15,6 +15,7 @@ #include #include #include +#include #include #include "core/download.h" @@ -280,10 +281,10 @@ initialize_command_local() { CMD_ANY ("pieces.sync.safe_free_diskspace", [](auto, auto) { return torrent::runtime::memory_manager()->sync_safe_free_diskspace(); }); CMD_ANY ("pieces.sync.timeout", [](auto, auto) { return torrent::runtime::memory_manager()->timeout_sync().count(); }); CMD_ANY_VALUE_V ("pieces.sync.timeout.set", [](auto, auto& value) { return torrent::runtime::memory_manager()->set_timeout_sync(value); }); - // CMD_ANY ("pieces.sync.timeout_safe", [](auto, auto) { return torrent::runtime::memory_manager()->timeout_safe_sync(); }); - // CMD_ANY_VALUE_V ("pieces.sync.timeout_safe.set", [](auto, auto& value) { return torrent::runtime::memory_manager()->set_timeout_safe_sync(value); }); CMD_ANY ("pieces.sync.timeout_safe", [](auto, auto) { return 0; }); - CMD_ANY_VALUE_V ("pieces.sync.timeout_safe.set", [](auto, auto) { }); + CMD_ANY_VALUE_V ("pieces.sync.timeout_safe.set", [](auto, auto) { + lt_log_print(torrent::LOG_WARN, "pieces.sync.timeout_safe.set is no longer supported and does nothing."); + }); CMD_ANY ("pieces.sync.queue_size", [](auto, auto) { return torrent::runtime::memory_manager()->sync_queue_block_count(); }); CMD_ANY ("pieces.preload.type", [](auto, auto) { return torrent::runtime::memory_manager()->preload_type(); }); From 16bfaf2694c362e8430f6bf7b7e356a6fe8b5a82 Mon Sep 17 00:00:00 2001 From: xirvik Date: Wed, 26 Aug 2026 14:27:06 +0000 Subject: [PATCH 18/34] Reject an XMLRPC size limit above the SCGI content size limit. --- src/rpc/rpc_manager.cc | 8 ++++++++ src/rpc/rpc_manager.h | 2 +- src/rpc/xmlrpc_c.cc | 3 --- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/rpc/rpc_manager.cc b/src/rpc/rpc_manager.cc index 6e9cd5c5..39faf7a1 100644 --- a/src/rpc/rpc_manager.cc +++ b/src/rpc/rpc_manager.cc @@ -167,6 +167,14 @@ RpcManager::cleanup() { m_jsonrpc.cleanup(); } +void +RpcManager::set_size_limit(uint64_t size) { + if (size > SCgiTask::max_content_size) + throw torrent::input_error("XMLRPC size limit cannot exceed the SCGI content size limit."); + + m_xmlrpc.set_size_limit(size); +} + void RpcManager::insert_command(const char* name, const char* parm, const char* doc) { m_xmlrpc.insert_command(name, parm, doc); diff --git a/src/rpc/rpc_manager.h b/src/rpc/rpc_manager.h index beb99647..e9b84b8c 100644 --- a/src/rpc/rpc_manager.h +++ b/src/rpc/rpc_manager.h @@ -62,7 +62,7 @@ public: void cleanup(); int64_t size_limit() { return m_xmlrpc.size_limit(); }; - void set_size_limit(uint64_t size) { m_xmlrpc.set_size_limit(size); }; + void set_size_limit(uint64_t size); int dialect() { return m_xmlrpc.dialect(); } void set_dialect(int dialect) { m_xmlrpc.set_dialect(dialect); } diff --git a/src/rpc/xmlrpc_c.cc b/src/rpc/xmlrpc_c.cc index ab04166d..08544b03 100644 --- a/src/rpc/xmlrpc_c.cc +++ b/src/rpc/xmlrpc_c.cc @@ -529,9 +529,6 @@ XmlRpc::size_limit() { void XmlRpc::set_size_limit(uint64_t size) { - if (size >= (64 << 20)) - throw torrent::input_error("Invalid XMLRPC limit size."); - xmlrpc_limit_set(XMLRPC_XML_SIZE_LIMIT_ID, size); } From 072cbae0c609c9885630dc5506c2972b80faa6be Mon Sep 17 00:00:00 2001 From: Jari Sundell Date: Thu, 27 Aug 2026 20:18:25 +0200 Subject: [PATCH 19/34] Expose system.files.close_idle for FileManager idle FD timeout by @j-c-m --- src/command_local.cc | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/command_local.cc b/src/command_local.cc index b4461d74..5b5a1aea 100644 --- a/src/command_local.cc +++ b/src/command_local.cc @@ -197,8 +197,7 @@ checked_socket_value(int64_t value, const char* label) { void initialize_command_local() { - core::DownloadList* dList = control->core()->download_list(); - torrent::FileManager* fileManager = torrent::file_manager(); + core::DownloadList* dList = control->core()->download_list(); CMD_ANY ("system.hostname", std::bind(&system_hostname)); CMD_ANY ("system.pid", std::bind(&getpid)); @@ -220,16 +219,19 @@ initialize_command_local() { CMD_VAR_LIST ("file.prioritize_toc.first"); CMD_VAR_LIST ("file.prioritize_toc.last"); - CMD_ANY ("system.files.advise_random", std::bind(&FM_t::advise_random, fileManager)); - CMD_ANY_VALUE_V ("system.files.advise_random.set", std::bind(&FM_t::set_advise_random, fileManager, std::placeholders::_2)); - CMD_ANY ("system.files.advise_random.hashing", std::bind(&FM_t::advise_random_hashing, fileManager)); - CMD_ANY_VALUE_V ("system.files.advise_random.hashing.set", std::bind(&FM_t::set_advise_random_hashing, fileManager, std::placeholders::_2)); + CMD_ANY ("system.files.advise_random", [](auto, auto) { return torrent::file_manager()->advise_random(); }); + CMD_ANY_VALUE_V ("system.files.advise_random.set", [](auto, auto& value) { return torrent::file_manager()->set_advise_random(value); }); + CMD_ANY ("system.files.advise_random.hashing", [](auto, auto) { return torrent::file_manager()->advise_random_hashing(); }); + CMD_ANY_VALUE_V ("system.files.advise_random.hashing.set", [](auto, auto& value) { return torrent::file_manager()->set_advise_random_hashing(value); }); CMD_ANY ("system.files.session.fdatasync", [](auto, auto) { return session_thread::manager()->use_fsyncdisk(); }); CMD_ANY_VALUE_V ("system.files.session.fdatasync.set", [](auto, auto& value) { return session_thread::manager()->set_use_fsyncdisk(value); }); - CMD_ANY ("system.files.opened_counter", std::bind(&FM_t::files_opened_counter, fileManager)); - CMD_ANY ("system.files.closed_counter", std::bind(&FM_t::files_closed_counter, fileManager)); - CMD_ANY ("system.files.failed_counter", std::bind(&FM_t::files_failed_counter, fileManager)); + CMD_ANY ("system.files.close_idle", [](auto, auto) { return torrent::file_manager()->close_idle_timeout().count(); }); + CMD_ANY_VALUE_V ("system.files.close_idle.set", [](auto, auto& value) { return torrent::file_manager()->set_close_idle_timeout(value * 1s); }); + + CMD_ANY ("system.files.opened_counter", [](auto, auto) { return torrent::file_manager()->files_opened_counter(); }); + CMD_ANY ("system.files.closed_counter", [](auto, auto) { return torrent::file_manager()->files_closed_counter(); }); + CMD_ANY ("system.files.failed_counter", [](auto, auto) { return torrent::file_manager()->files_failed_counter(); }); CMD_ANY_STRING ("system.env", [](auto, auto& str) { return system_env(str); }); From 60eb8f03831830b9d6c175ac0c392e29d608d389 Mon Sep 17 00:00:00 2001 From: noctuum <25441068+noctuum@users.noreply.github.com> Date: Fri, 28 Aug 2026 06:28:35 +0500 Subject: [PATCH 20/34] Split the file closing in DownloadList::close_directly() into close_files(). Callers that only need the files released should not depend on how a stop behaves. --- src/command_download.cc | 2 +- src/core/download.cc | 2 +- src/core/download_list.cc | 13 +++++++++++-- src/core/download_list.h | 1 + 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/command_download.cc b/src/command_download.cc index cb208307..36aee870 100644 --- a/src/command_download.cc +++ b/src/command_download.cc @@ -718,7 +718,7 @@ initialize_command_download() { CMD2_DL_V ("d.pause", std::bind(&core::DownloadList::pause_default, control->core()->download_list(), std::placeholders::_1)); CMD2_DL_V ("d.open", std::bind(&core::DownloadList::open_throw, control->core()->download_list(), std::placeholders::_1)); CMD2_DL_V ("d.close", std::bind(&core::DownloadList::close_throw, control->core()->download_list(), std::placeholders::_1)); - CMD2_DL_V ("d.close.directly", std::bind(&core::DownloadList::close_directly, control->core()->download_list(), std::placeholders::_1)); + CMD2_DL_V ("d.close.directly", std::bind(&core::DownloadList::close_files, control->core()->download_list(), std::placeholders::_1)); CMD2_DL_V ("d.erase", std::bind(&core::DownloadList::erase_ptr, control->core()->download_list(), std::placeholders::_1)); CMD2_DL_V ("d.check_hash", std::bind(&core::DownloadList::check_hash, control->core()->download_list(), std::placeholders::_1)); diff --git a/src/core/download.cc b/src/core/download.cc index c28badc8..4dabdc70 100644 --- a/src/core/download.cc +++ b/src/core/download.cc @@ -126,7 +126,7 @@ Download::set_root_directory(const std::string& path) { throw torrent::input_error("Cannot change the directory of an open download after the files have been moved."); } - control->core()->download_list()->close_directly(this); + control->core()->download_list()->close_files(this); file_list->set_root_dir(expand_path(path)); bencode()->get_key("rtorrent").insert_key("directory", path); diff --git a/src/core/download_list.cc b/src/core/download_list.cc index a0acb4b2..3a4b3214 100644 --- a/src/core/download_list.cc +++ b/src/core/download_list.cc @@ -254,9 +254,11 @@ DownloadList::close(Download* download) { } } +// Releases the files without changing the download's state, for callers that +// need the files closed and will keep using the download. void -DownloadList::close_directly(Download* download) { - lt_log_print_info(torrent::LOG_TORRENT_INFO, download->info(), "download_list", "Closing download directly."); +DownloadList::close_files(Download* download) { + lt_log_print_info(torrent::LOG_TORRENT_INFO, download->info(), "download_list", "Closing download files."); if (download->download()->info()->is_active()) { download->download()->stop(torrent::Download::stop_skip_tracker); @@ -269,6 +271,13 @@ DownloadList::close_directly(Download* download) { download->download()->close(); } +void +DownloadList::close_directly(Download* download) { + lt_log_print_info(torrent::LOG_TORRENT_INFO, download->info(), "download_list", "Closing download directly."); + + close_files(download); +} + void DownloadList::close_quick(Download* download) { lt_log_print_info(torrent::LOG_TORRENT_INFO, download->info(), "download_list", "Closing download quickly."); diff --git a/src/core/download_list.h b/src/core/download_list.h index e860876c..f3fe5b5f 100644 --- a/src/core/download_list.h +++ b/src/core/download_list.h @@ -75,6 +75,7 @@ public: void close(Download* d); void close_directly(Download* d); + void close_files(Download* d); void close_quick(Download* d); void close_throw(Download* d); From e4e858cc402a5449f6892e9538b5e4ef0e103320 Mon Sep 17 00:00:00 2001 From: noctuum <25441068+noctuum@users.noreply.github.com> Date: Fri, 28 Aug 2026 06:31:21 +0500 Subject: [PATCH 21/34] Stop the download in close_directly() rather than leaving the state behind. It closed the files while d.state and the started view still called it running. --- src/core/download.cc | 1 - src/core/download_list.cc | 41 ++++++++++++++++++++++++++++++--------- src/core/download_list.h | 3 +++ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/core/download.cc b/src/core/download.cc index 4dabdc70..98772344 100644 --- a/src/core/download.cc +++ b/src/core/download.cc @@ -120,7 +120,6 @@ Download::set_root_directory(const std::string& path) { !file_stat.update(file_list->front()->frozen_path().str()))) { set_message("Cannot change the directory of an open download after the files have been moved."); - rpc::call_command("d.state.set", (int64_t)0, rpc::make_target(this)); control->core()->download_list()->close_directly(this); throw torrent::input_error("Cannot change the directory of an open download after the files have been moved."); diff --git a/src/core/download_list.cc b/src/core/download_list.cc index 3a4b3214..8dfd86d3 100644 --- a/src/core/download_list.cc +++ b/src/core/download_list.cc @@ -275,7 +275,38 @@ void DownloadList::close_directly(Download* download) { lt_log_print_info(torrent::LOG_TORRENT_INFO, download->info(), "download_list", "Closing download directly."); + bool was_active = download->download()->info()->is_active(); + bool was_open = download->download()->info()->is_open(); + close_files(download); + set_state_stopped(download); + + if (was_active) { + DL_TRIGGER_EVENT(download, "event.download.paused"); + update_paused_state(download); + } + + if (was_open) { + DL_TRIGGER_EVENT(download, "event.download.hash_removed"); + DL_TRIGGER_EVENT(download, "event.download.closed"); + } +} + +void +DownloadList::set_state_stopped(Download* download) { + control->view_manager()->find_ptr_throw("stopped")->set_visible(download); + rpc::call_command("d.state.set", (int64_t)0, rpc::make_target(download)); +} + +void +DownloadList::update_paused_state(Download* download) { + rpc::call_command("d.state_changed.set", torrent::this_thread::cached_seconds().count(), rpc::make_target(download)); + rpc::call_command("d.state_counter.set", rpc::call_command_value("d.state_counter", rpc::make_target(download)), rpc::make_target(download)); + + // If initial seeding is complete, don't try it again when restarting. + if (download->is_done() && + rpc::call_command("d.connection_current", torrent::Object(), rpc::make_target(download)).as_string() == "initial_seed") + rpc::call_command("d.connection_seed.set", rpc::call_command("d.connection_current", torrent::Object(), rpc::make_target(download)), rpc::make_target(download)); } void @@ -452,15 +483,7 @@ DownloadList::pause(Download* download, int flags) { // view. DL_TRIGGER_EVENT(download, "event.download.paused"); - auto cached_seconds = torrent::this_thread::cached_seconds().count(); - - rpc::call_command("d.state_changed.set", cached_seconds, rpc::make_target(download)); - rpc::call_command("d.state_counter.set", rpc::call_command_value("d.state_counter", rpc::make_target(download)), rpc::make_target(download)); - - // If initial seeding is complete, don't try it again when restarting. - if (download->is_done() && - rpc::call_command("d.connection_current", torrent::Object(), rpc::make_target(download)).as_string() == "initial_seed") - rpc::call_command("d.connection_seed.set", rpc::call_command("d.connection_current", torrent::Object(), rpc::make_target(download)), rpc::make_target(download)); + update_paused_state(download); // Save the state after all the slots, etc have been called so we // include the modifications they may make. diff --git a/src/core/download_list.h b/src/core/download_list.h index f3fe5b5f..c79bbbae 100644 --- a/src/core/download_list.h +++ b/src/core/download_list.h @@ -133,6 +133,9 @@ private: void hash_done(Download* d); void hash_queue(Download* d, int type); + void set_state_stopped(Download* d); + void update_paused_state(Download* d); + inline void check_contains(Download* d); void received_finished(Download* d); From 21019fde412e33ee6f31769f57244d9d5ca6f154 Mon Sep 17 00:00:00 2001 From: noctuum <25441068+noctuum@users.noreply.github.com> Date: Fri, 28 Aug 2026 06:32:53 +0500 Subject: [PATCH 22/34] Add the download to the stopped view when a hash check contradicts d.complete. Setting d.state alone left it in the started view, where d.start cannot restore it. --- src/core/download_list.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/download_list.cc b/src/core/download_list.cc index 8dfd86d3..b67f590a 100644 --- a/src/core/download_list.cc +++ b/src/core/download_list.cc @@ -554,7 +554,7 @@ DownloadList::hash_done(Download* download) { // If the download was previously completed but the files were // f.ex deleted, then we clear the state and complete. if (rpc::call_command_value("d.complete", rpc::make_target(download)) && !download->is_done()) { - rpc::call_command("d.state.set", (int64_t)0, rpc::make_target(download)); + set_state_stopped(download); download->set_message("Download registered as completed, but hash check returned unfinished chunks."); } From 2804be2c9ca8aae86ad07c5868eead90b812511f Mon Sep 17 00:00:00 2001 From: Jari Sundell Date: Sat, 29 Aug 2026 11:31:16 +0200 Subject: [PATCH 23/34] Optionally sanitize download and file names by replacing slash with underscore. --- src/Makefile.am | 1 + src/command_download.cc | 22 +- src/command_helpers.cc | 2 + src/command_local.cc | 187 +---------------- src/command_system.cc | 195 ++++++++++++++++++ src/control.h | 2 + src/core/download.cc | 13 +- src/core/download.h | 3 +- src/main.cc | 6 + test/Makefile.am | 4 +- ...ommand_local.cc => test_command_system.cc} | 14 +- ..._command_local.h => test_command_system.h} | 4 +- 12 files changed, 239 insertions(+), 214 deletions(-) create mode 100644 src/command_system.cc rename test/src/{test_command_local.cc => test_command_system.cc} (83%) rename test/src/{test_command_local.h => test_command_system.h} (69%) diff --git a/src/Makefile.am b/src/Makefile.am index e53c2f96..1c4ccfef 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -195,6 +195,7 @@ libsub_root_a_SOURCES = \ command_tracker.cc \ command_scheduler.cc \ command_string.cc \ + command_system.cc \ command_ui.cc \ control.cc \ control.h \ diff --git a/src/command_download.cc b/src/command_download.cc index 36aee870..a910c178 100644 --- a/src/command_download.cc +++ b/src/command_download.cc @@ -142,16 +142,6 @@ apply_d_delete_tied(core::Download* download) { return torrent::Object(); } -void -apply_d_directory(core::Download* download, const std::string& name) { - if (!download->file_list()->is_multi_file()) - download->set_root_directory(name); - else if (name.empty() || *name.rbegin() == '/') - download->set_root_directory(name + download->info()->name().str()); - else - download->set_root_directory(name + "/" + download->info()->name().str()); -} - torrent::Object apply_d_connection_type(core::Download* download, const std::string& name) { torrent::Download::ConnectionType t = @@ -884,12 +874,11 @@ initialize_command_download() { CMD2_DL_LIST ("d.tracker.insert", std::bind(&download_tracker_insert, std::placeholders::_1, std::placeholders::_2)); CMD2_DL_VALUE_V ("d.tracker.send_scrape", [](auto download, uint64_t arg) { download->tracker_controller().scrape_request(arg); }); - CMD2_DL ("d.directory", CMD2_ON_FL(root_dir)); - CMD2_DL ("d.directory.realpath.or_empty", [](auto* download, auto) { return resolve_path(download->file_list()->root_dir()); }); - CMD2_DL ("d.directory.realpath.or_throw", [](auto* download, auto) { return resolve_path_or_throw(download->file_list()->root_dir()); }); - CMD2_DL_STRING_V("d.directory.set", std::bind(&apply_d_directory, std::placeholders::_1, std::placeholders::_2)); - CMD2_DL ("d.directory_base", CMD2_ON_FL(root_dir)); - CMD2_DL_STRING_V("d.directory_base.set", std::bind(&core::Download::set_root_directory, std::placeholders::_1, std::placeholders::_2)); + CMD2_DL ("d.directory", CMD2_ON_FL(root_dir)); + CMD2_DL ("d.directory.realpath.or_empty", [](auto* download, auto) { return resolve_path(download->file_list()->root_dir()); }); + CMD2_DL ("d.directory.realpath.or_throw", [](auto* download, auto) { return resolve_path_or_throw(download->file_list()->root_dir()); }); + CMD2_DL_STRING_V("d.directory.set", [](auto* download, auto arg) { download->set_directory(arg); }); + CMD2_DL_STRING_V("d.directory.base.set", [](auto* download, auto arg) { download->set_base_directory(arg); }); CMD2_DL ("d.priority", std::bind(&core::Download::priority, std::placeholders::_1)); CMD2_DL ("d.priority_str", std::bind(&retrieve_d_priority_str, std::placeholders::_1)); @@ -950,7 +939,6 @@ initialize_command_download() { rpc::rpc.mark_safe("d.name.or_base64"); rpc::rpc.mark_safe("d.name.or_as_binary"); rpc::rpc.mark_safe("d.directory"); - rpc::rpc.mark_safe("d.directory_base"); rpc::rpc.mark_safe("d.creation_date"); rpc::rpc.mark_safe("d.load_date"); rpc::rpc.mark_safe("d.up.rate"); diff --git a/src/command_helpers.cc b/src/command_helpers.cc index 512d1a6a..fee22018 100644 --- a/src/command_helpers.cc +++ b/src/command_helpers.cc @@ -20,6 +20,7 @@ void initialize_command_throttle(); void initialize_command_tracker(); void initialize_command_scheduler(); void initialize_command_string(); +void initialize_command_system(); void initialize_command_ui(); void @@ -39,4 +40,5 @@ initialize_commands() { initialize_command_tracker(); initialize_command_scheduler(); initialize_command_string(); + initialize_command_system(); } diff --git a/src/command_local.cc b/src/command_local.cc index 5b5a1aea..ab73fe15 100644 --- a/src/command_local.cc +++ b/src/command_local.cc @@ -1,35 +1,16 @@ #include "config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include #include -#include +#include "command_helpers.h" +#include "control.h" #include "core/download.h" -#include "core/download_list.h" #include "core/manager.h" #include "rpc/parse_commands.h" +#include "rpc/lua.h" #include "rpc/scgi.h" #include "session/session_manager.h" -#include "utils/file_status_cache.h" - -#include "globals.h" -#include "rpc/lua.h" -#include "control.h" -#include "command_helpers.h" typedef torrent::ChunkManager CM_t; typedef torrent::FileManager FM_t; @@ -45,49 +26,6 @@ apply_pieces_stats_total_size() { return size; } -torrent::Object -system_env(const torrent::Object::string_type& arg) { - if (arg.empty()) - throw torrent::input_error("system.env: Missing variable name."); - - char* val = getenv(arg.c_str()); - return std::string(val ? val : ""); -} - -torrent::Object -system_hostname() { - char buffer[1024]; - - if (gethostname(buffer, 1023) == -1) - throw torrent::input_error("Unable to read hostname."); - -// if (shorten) -// *std::find(buffer, buffer + 1023, '.') = '\0'; - - return std::string(buffer); -} - -torrent::Object -system_get_cwd() { - char* buffer = getcwd(NULL, 0); - - if (buffer == NULL) - throw torrent::input_error("Unable to read cwd."); - - torrent::Object result = torrent::Object(std::string(buffer)); - free(buffer); - - return result; -} - -torrent::Object -system_set_cwd(const torrent::Object::string_type& rawArgs) { - if (::chdir(rawArgs.c_str()) != 0) - throw torrent::input_error("Could not change current working directory."); - - return torrent::Object(); -} - inline torrent::Object::list_const_iterator post_increment(torrent::Object::list_const_iterator& itr, const torrent::Object::list_const_iterator& last) { if (itr == last) @@ -187,97 +125,10 @@ cmd_file_append(const torrent::Object::list_type& args) { return torrent::Object(); } -uint32_t -checked_socket_value(int64_t value, const char* label) { - if (value < 0 || value > std::numeric_limits::max()) - throw torrent::input_error(std::string("Invalid ") + label + " value."); - - return static_cast(value); -} - void initialize_command_local() { core::DownloadList* dList = control->core()->download_list(); - CMD_ANY ("system.hostname", std::bind(&system_hostname)); - CMD_ANY ("system.pid", std::bind(&getpid)); - - CMD_VAR_C_STRING("system.api_version", (int64_t)API_VERSION); - CMD_VAR_C_STRING("system.client_version", PACKAGE_VERSION); - CMD_VAR_C_STRING("system.library_version", torrent::runtime::version()); - - CMD_VAR_VALUE ("system.file.allocate", 0); - CMD_VAR_VALUE ("system.file.max_size", (int64_t)512 << 30); - CMD_VAR_VALUE ("system.file.split_size", -1); - CMD_VAR_STRING ("system.file.split_suffix", ".part"); - - CMD_ANY ("system.file_status_cache.size", std::bind(&utils::FileStatusCache::size, - (utils::FileStatusCache::base_type*)control->core()->file_status_cache())); - CMD_ANY_V ("system.file_status_cache.prune", std::bind(&utils::FileStatusCache::prune, control->core()->file_status_cache())); - - CMD_VAR_BOOL ("file.prioritize_toc", 0); - CMD_VAR_LIST ("file.prioritize_toc.first"); - CMD_VAR_LIST ("file.prioritize_toc.last"); - - CMD_ANY ("system.files.advise_random", [](auto, auto) { return torrent::file_manager()->advise_random(); }); - CMD_ANY_VALUE_V ("system.files.advise_random.set", [](auto, auto& value) { return torrent::file_manager()->set_advise_random(value); }); - CMD_ANY ("system.files.advise_random.hashing", [](auto, auto) { return torrent::file_manager()->advise_random_hashing(); }); - CMD_ANY_VALUE_V ("system.files.advise_random.hashing.set", [](auto, auto& value) { return torrent::file_manager()->set_advise_random_hashing(value); }); - CMD_ANY ("system.files.session.fdatasync", [](auto, auto) { return session_thread::manager()->use_fsyncdisk(); }); - CMD_ANY_VALUE_V ("system.files.session.fdatasync.set", [](auto, auto& value) { return session_thread::manager()->set_use_fsyncdisk(value); }); - - CMD_ANY ("system.files.close_idle", [](auto, auto) { return torrent::file_manager()->close_idle_timeout().count(); }); - CMD_ANY_VALUE_V ("system.files.close_idle.set", [](auto, auto& value) { return torrent::file_manager()->set_close_idle_timeout(value * 1s); }); - - CMD_ANY ("system.files.opened_counter", [](auto, auto) { return torrent::file_manager()->files_opened_counter(); }); - CMD_ANY ("system.files.closed_counter", [](auto, auto) { return torrent::file_manager()->files_closed_counter(); }); - CMD_ANY ("system.files.failed_counter", [](auto, auto) { return torrent::file_manager()->files_failed_counter(); }); - - CMD_ANY_STRING ("system.env", [](auto, auto& str) { return system_env(str); }); - - CMD_ANY ("system.time", [](auto, auto) { return torrent::this_thread::cached_seconds().count(); }); - CMD_ANY ("system.time_seconds", [](auto, auto) { return torrent::utils::cast_seconds(torrent::utils::time_since_epoch()).count(); }); - CMD_ANY ("system.time_usec", [](auto, auto) { return torrent::utils::time_since_epoch().count(); }); - - CMD_ANY_VALUE_V ("system.umask.set", [](auto, auto& value) { return ::umask(value); }); - - CMD_VAR_BOOL ("system.daemon", false); - - CMD_ANY_V ("system.shutdown.normal", [](auto, auto) { control->receive_normal_shutdown(); }); - CMD_ANY_V ("system.shutdown.quick", [](auto, auto) { control->receive_quick_shutdown(); }); - - CMD_REDIRECT_NO_EXPORT("system.shutdown", "system.shutdown.normal"); - - CMD_ANY ("system.cwd", [](auto, auto) { return system_get_cwd(); }); - CMD_ANY_STRING ("system.cwd.set", [](auto, auto& str) { return system_set_cwd(str); }); - - CMD_ANY ("system.sockets.size", [](auto, auto) { return torrent::runtime::socket_manager()->size(); }); - CMD_ANY ("system.sockets.max_size", [](auto, auto) { return torrent::runtime::socket_manager()->max_size(); }); - CMD_ANY_VALUE_V ("system.sockets.max_size.set", [](auto, auto& value) { return torrent::runtime::socket_manager()->set_max_size_and_adjust(checked_socket_value(value, "socket max size")); }); - CMD_ANY_V ("system.sockets.adjust_alloc", [](auto, auto) { torrent::runtime::socket_manager()->adjust_allocation(); }); - CMD_ANY ("system.sockets.reserved_alloc", [](auto, auto) { return torrent::runtime::socket_manager()->reserved_allocation(); }); - CMD_ANY ("system.sockets.available_alloc", [](auto, auto) { return torrent::runtime::socket_manager()->available_allocation(); }); - - for (uint32_t i = 0; i < torrent::runtime::SocketManager::category_count; ++i) { - auto category = static_cast(i); - auto category_name = "system.sockets." + torrent::option_to_str_or_throw(torrent::OPTION_SOCKET_CATEGORY, i); - - CMD_ANY (category_name + ".size", [category](auto, auto) { return torrent::runtime::socket_manager()->category_managed_size(category); }); - CMD_ANY (category_name + ".max_size", [category](auto, auto) { return torrent::runtime::socket_manager()->category_max_size(category); }); - - if (i == 0) { - CMD_ANY (category_name + ".min_alloc", [](auto, auto) { return torrent::runtime::socket_manager()->generic_min_allocation(); }); - continue; - } - - CMD_ANY (category_name + ".max_alloc.limit", [category](auto, auto) { return torrent::runtime::socket_manager()->category_alloc_limit(category); }); - CMD_ANY (category_name + ".min_alloc.limit", [category](auto, auto) { return torrent::runtime::socket_manager()->category_alloc_minimum(category); }); - CMD_ANY (category_name + ".min_alloc", [category](auto, auto) { return torrent::runtime::socket_manager()->category_min_allocation(category); }); - CMD_ANY (category_name + ".max_alloc", [category](auto, auto) { return torrent::runtime::socket_manager()->category_max_allocation(category); }); - CMD_ANY_VALUE_V(category_name + ".min_alloc.set", [category](auto, auto& value) { torrent::runtime::socket_manager()->set_category_min_allocation(category, checked_socket_value(value, "socket min alloc")); }); - CMD_ANY_VALUE_V(category_name + ".max_alloc.set", [category](auto, auto& value) { torrent::runtime::socket_manager()->set_category_max_allocation(category, checked_socket_value(value, "socket max alloc")); }); - } - CMD_ANY ("pieces.sync.always_safe", [](auto, auto) { return torrent::runtime::memory_manager()->safe_sync(); }); CMD_ANY_VALUE_V ("pieces.sync.always_safe.set", [](auto, auto& value) { return torrent::runtime::memory_manager()->set_safe_sync(value); }); CMD_ANY ("pieces.sync.safe_free_diskspace", [](auto, auto) { return torrent::runtime::memory_manager()->sync_safe_free_diskspace(); }); @@ -361,38 +212,6 @@ initialize_command_local() { CMD_ANY_LIST ("group.insert", std::bind(&group_insert, std::placeholders::_2)); - rpc::rpc.mark_safe("system.api_version"); - rpc::rpc.mark_safe("system.client_version"); - rpc::rpc.mark_safe("system.library_version"); - rpc::rpc.mark_safe("system.time"); - rpc::rpc.mark_safe("system.time_seconds"); - rpc::rpc.mark_safe("system.time_usec"); - rpc::rpc.mark_safe("system.file.max_size"); - rpc::rpc.mark_safe("system.file.split_size"); - rpc::rpc.mark_safe("system.file.split_suffix"); - - rpc::rpc.mark_safe("system.sockets.size"); - rpc::rpc.mark_safe("system.sockets.max_size"); - rpc::rpc.mark_safe("system.sockets.reserved_alloc"); - rpc::rpc.mark_safe("system.sockets.available_alloc"); - - for (uint32_t i = 0; i < torrent::runtime::SocketManager::category_count; ++i) { - auto category_name = "system.sockets." + torrent::option_to_str_or_throw(torrent::OPTION_SOCKET_CATEGORY, i); - - rpc::rpc.mark_safe(category_name + ".size"); - rpc::rpc.mark_safe(category_name + ".max_size"); - - if (i == 0) { - rpc::rpc.mark_safe(category_name + ".min_alloc"); - continue; - } - - rpc::rpc.mark_safe(category_name + ".max_alloc.limit"); - rpc::rpc.mark_safe(category_name + ".min_alloc.limit"); - rpc::rpc.mark_safe(category_name + ".min_alloc"); - rpc::rpc.mark_safe(category_name + ".max_alloc"); - } - rpc::rpc.mark_safe("directory.default"); rpc::rpc.mark_safe("session.path"); rpc::rpc.mark_safe("session.path.realpath.or_empty"); diff --git a/src/command_system.cc b/src/command_system.cc new file mode 100644 index 00000000..4615acd2 --- /dev/null +++ b/src/command_system.cc @@ -0,0 +1,195 @@ +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "command_helpers.h" +#include "control.h" +#include "globals.h" +#include "core/manager.h" +#include "session/session_manager.h" +#include "utils/file_status_cache.h" + + +torrent::Object +system_env(const torrent::Object::string_type& arg) { + if (arg.empty()) + throw torrent::input_error("system.env: Missing variable name."); + + char* val = getenv(arg.c_str()); + return std::string(val ? val : ""); +} + +torrent::Object +system_hostname() { + char buffer[1024]; + + if (gethostname(buffer, 1023) == -1) + throw torrent::input_error("Unable to read hostname."); + +// if (shorten) +// *std::find(buffer, buffer + 1023, '.') = '\0'; + + return std::string(buffer); +} + +torrent::Object +system_get_cwd() { + char* buffer = getcwd(NULL, 0); + + if (buffer == NULL) + throw torrent::input_error("Unable to read cwd."); + + torrent::Object result = torrent::Object(std::string(buffer)); + free(buffer); + + return result; +} + +torrent::Object +system_set_cwd(const torrent::Object::string_type& rawArgs) { + if (::chdir(rawArgs.c_str()) != 0) + throw torrent::input_error("Could not change current working directory."); + + return torrent::Object(); +} + +uint32_t +checked_socket_value(int64_t value, const char* label) { + if (value < 0 || value > std::numeric_limits::max()) + throw torrent::input_error(std::string("Invalid ") + label + " value."); + + return static_cast(value); +} + +void +initialize_command_system() { + CMD_ANY ("system.hostname", std::bind(&system_hostname)); + CMD_ANY ("system.pid", std::bind(&getpid)); + + CMD_VAR_C_STRING("system.api_version", (int64_t)API_VERSION); + CMD_VAR_C_STRING("system.client_version", PACKAGE_VERSION); + CMD_VAR_C_STRING("system.library_version", torrent::runtime::version()); + + CMD_ANY ("system.torrent_name.use_sanitized", [](auto, auto) { return torrent::runtime::client_config()->torrent_name_use_sanitized(); }); + CMD_ANY_VALUE_V ("system.torrent_name.use_sanitized.set", [](auto, auto& value) { return torrent::runtime::client_config()->set_torrent_name_use_sanitized(value); }); + + CMD_VAR_VALUE ("system.file.allocate", 0); + CMD_VAR_VALUE ("system.file.max_size", (int64_t)512 << 30); + CMD_VAR_VALUE ("system.file.split_size", -1); + CMD_VAR_STRING ("system.file.split_suffix", ".part"); + + CMD_ANY ("system.file_name.replace_slash", [](auto, auto) { return torrent::runtime::client_config()->file_name_replace_slash(); }); + CMD_ANY_STRING_V("system.file_name.replace_slash.set", [](auto, auto& str) { return torrent::runtime::client_config()->set_file_name_replace_slash(str); }); + + CMD_ANY ("system.file_status_cache.size", [](auto, auto) { return control->core()->file_status_cache()->size(); }); + CMD_ANY_V ("system.file_status_cache.prune", [](auto, auto) { return control->core()->file_status_cache()->prune(); }); + + CMD_VAR_BOOL ("file.prioritize_toc", 0); + CMD_VAR_LIST ("file.prioritize_toc.first"); + CMD_VAR_LIST ("file.prioritize_toc.last"); + + CMD_ANY ("system.files.advise_random", [](auto, auto) { return torrent::file_manager()->advise_random(); }); + CMD_ANY_VALUE_V ("system.files.advise_random.set", [](auto, auto& value) { return torrent::file_manager()->set_advise_random(value); }); + CMD_ANY ("system.files.advise_random.hashing", [](auto, auto) { return torrent::file_manager()->advise_random_hashing(); }); + CMD_ANY_VALUE_V ("system.files.advise_random.hashing.set", [](auto, auto& value) { return torrent::file_manager()->set_advise_random_hashing(value); }); + CMD_ANY ("system.files.session.fdatasync", [](auto, auto) { return session_thread::manager()->use_fsyncdisk(); }); + CMD_ANY_VALUE_V ("system.files.session.fdatasync.set", [](auto, auto& value) { return session_thread::manager()->set_use_fsyncdisk(value); }); + + CMD_ANY ("system.files.close_idle", [](auto, auto) { return torrent::file_manager()->close_idle_timeout().count(); }); + CMD_ANY_VALUE_V ("system.files.close_idle.set", [](auto, auto& value) { return torrent::file_manager()->set_close_idle_timeout(value * 1s); }); + + CMD_ANY ("system.files.opened_counter", [](auto, auto) { return torrent::file_manager()->files_opened_counter(); }); + CMD_ANY ("system.files.closed_counter", [](auto, auto) { return torrent::file_manager()->files_closed_counter(); }); + CMD_ANY ("system.files.failed_counter", [](auto, auto) { return torrent::file_manager()->files_failed_counter(); }); + + CMD_ANY_STRING ("system.env", [](auto, auto& str) { return system_env(str); }); + + CMD_ANY ("system.time", [](auto, auto) { return torrent::this_thread::cached_seconds().count(); }); + CMD_ANY ("system.time_seconds", [](auto, auto) { return torrent::utils::cast_seconds(torrent::utils::time_since_epoch()).count(); }); + CMD_ANY ("system.time_usec", [](auto, auto) { return torrent::utils::time_since_epoch().count(); }); + + CMD_ANY_VALUE_V ("system.umask.set", [](auto, auto& value) { return ::umask(value); }); + + CMD_VAR_BOOL ("system.daemon", false); + + CMD_ANY_V ("system.shutdown.normal", [](auto, auto) { control->receive_normal_shutdown(); }); + CMD_ANY_V ("system.shutdown.quick", [](auto, auto) { control->receive_quick_shutdown(); }); + + CMD_REDIRECT_NO_EXPORT("system.shutdown", "system.shutdown.normal"); + + CMD_ANY ("system.cwd", [](auto, auto) { return system_get_cwd(); }); + CMD_ANY_STRING ("system.cwd.set", [](auto, auto& str) { return system_set_cwd(str); }); + + CMD_ANY ("system.sockets.size", [](auto, auto) { return torrent::runtime::socket_manager()->size(); }); + CMD_ANY ("system.sockets.max_size", [](auto, auto) { return torrent::runtime::socket_manager()->max_size(); }); + CMD_ANY_VALUE_V ("system.sockets.max_size.set", [](auto, auto& value) { + return torrent::runtime::socket_manager()->set_max_size_and_adjust(checked_socket_value(value, "socket max size")); + }); + CMD_ANY_V ("system.sockets.adjust_alloc", [](auto, auto) { torrent::runtime::socket_manager()->adjust_allocation(); }); + CMD_ANY ("system.sockets.reserved_alloc", [](auto, auto) { return torrent::runtime::socket_manager()->reserved_allocation(); }); + CMD_ANY ("system.sockets.available_alloc", [](auto, auto) { return torrent::runtime::socket_manager()->available_allocation(); }); + + for (uint32_t i = 0; i < torrent::runtime::SocketManager::category_count; ++i) { + auto category = static_cast(i); + auto category_name = "system.sockets." + torrent::option_to_str_or_throw(torrent::OPTION_SOCKET_CATEGORY, i); + + CMD_ANY (category_name + ".size", [category](auto, auto) { return torrent::runtime::socket_manager()->category_managed_size(category); }); + CMD_ANY (category_name + ".max_size", [category](auto, auto) { return torrent::runtime::socket_manager()->category_max_size(category); }); + + if (i == 0) { + CMD_ANY (category_name + ".min_alloc", [](auto, auto) { return torrent::runtime::socket_manager()->generic_min_allocation(); }); + continue; + } + + CMD_ANY (category_name + ".max_alloc.limit", [category](auto, auto) { return torrent::runtime::socket_manager()->category_alloc_limit(category); }); + CMD_ANY (category_name + ".min_alloc.limit", [category](auto, auto) { return torrent::runtime::socket_manager()->category_alloc_minimum(category); }); + CMD_ANY (category_name + ".min_alloc", [category](auto, auto) { return torrent::runtime::socket_manager()->category_min_allocation(category); }); + CMD_ANY (category_name + ".max_alloc", [category](auto, auto) { return torrent::runtime::socket_manager()->category_max_allocation(category); }); + CMD_ANY_VALUE_V(category_name + ".min_alloc.set", [category](auto, auto& value) { + torrent::runtime::socket_manager()->set_category_min_allocation(category, checked_socket_value(value, "socket min alloc")); + }); + CMD_ANY_VALUE_V(category_name + ".max_alloc.set", [category](auto, auto& value) { + torrent::runtime::socket_manager()->set_category_max_allocation(category, checked_socket_value(value, "socket max alloc")); + }); + } + + rpc::rpc.mark_safe("system.api_version"); + rpc::rpc.mark_safe("system.client_version"); + rpc::rpc.mark_safe("system.library_version"); + rpc::rpc.mark_safe("system.time"); + rpc::rpc.mark_safe("system.time_seconds"); + rpc::rpc.mark_safe("system.time_usec"); + rpc::rpc.mark_safe("system.file.max_size"); + rpc::rpc.mark_safe("system.file.split_size"); + rpc::rpc.mark_safe("system.file.split_suffix"); + + rpc::rpc.mark_safe("system.sockets.size"); + rpc::rpc.mark_safe("system.sockets.max_size"); + rpc::rpc.mark_safe("system.sockets.reserved_alloc"); + rpc::rpc.mark_safe("system.sockets.available_alloc"); + + for (uint32_t i = 0; i < torrent::runtime::SocketManager::category_count; ++i) { + auto category_name = "system.sockets." + torrent::option_to_str_or_throw(torrent::OPTION_SOCKET_CATEGORY, i); + + rpc::rpc.mark_safe(category_name + ".size"); + rpc::rpc.mark_safe(category_name + ".max_size"); + + if (i == 0) { + rpc::rpc.mark_safe(category_name + ".min_alloc"); + continue; + } + + rpc::rpc.mark_safe(category_name + ".max_alloc.limit"); + rpc::rpc.mark_safe(category_name + ".min_alloc.limit"); + rpc::rpc.mark_safe(category_name + ".min_alloc"); + rpc::rpc.mark_safe(category_name + ".max_alloc"); + } +} diff --git a/src/control.h b/src/control.h index dfc287d4..ceb3656e 100644 --- a/src/control.h +++ b/src/control.h @@ -118,4 +118,6 @@ private: std::atomic m_shutdown_quick{}; }; +extern Control* control; + #endif diff --git a/src/core/download.cc b/src/core/download.cc index 98772344..4167cbaa 100644 --- a/src/core/download.cc +++ b/src/core/download.cc @@ -101,7 +101,18 @@ Download::set_throttle_name(const std::string& name) { } void -Download::set_root_directory(const std::string& path) { +Download::set_directory(const std::string& path) { + if (!m_download.file_list()->is_multi_file()) + return set_base_directory(path); + + if (path.empty() || *path.rbegin() == '/') + return set_base_directory(path + m_download.info()->name_sanitized()); + + set_base_directory(path + "/" + m_download.info()->name_sanitized()); +} + +void +Download::set_base_directory(const std::string& path) { // If the download is open, hashed and has completed chunks make // sure to verify that the download files are still present. // diff --git a/src/core/download.h b/src/core/download.h index 61c34867..046034d8 100644 --- a/src/core/download.h +++ b/src/core/download.h @@ -79,7 +79,8 @@ public: uint32_t resume_flags() { return m_resumeFlags; } void set_resume_flags(uint32_t flags) { m_resumeFlags = flags; } - void set_root_directory(const std::string& path); + void set_directory(const std::string& path); + void set_base_directory(const std::string& path); void set_throttle_name(const std::string& name); diff --git a/src/main.cc b/src/main.cc index 87683667..e0630786 100644 --- a/src/main.cc +++ b/src/main.cc @@ -379,6 +379,12 @@ main(int argc, char** argv) { lt_log_print(torrent::LOG_WARN, "network.max_open_files.set is deprecated, use system.sockets.files.min_alloc.set instead."); }); + // TODO: Keep d.directory_base for a while as it is widely used. + CMD_REDIRECT("d.directory_base", "d.directory"); + CMD_REDIRECT("d.directory_base.set", "d.directory.base.set"); + + rpc::rpc.mark_safe("d.directory_base"); + // if (rpc::call_command_value("method.use_intermediate") == 1) { // } else if (rpc::call_command_value("method.use_intermediate") == 2) { diff --git a/test/Makefile.am b/test/Makefile.am index 550051f8..f8f546c6 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -51,8 +51,8 @@ rtorrent_Test_Rpc_SOURCES = $(rtorrent_Test_Common) \ rtorrent_Test_Src_SOURCES = $(rtorrent_Test_Common) \ src/test_command_dynamic.cc \ src/test_command_dynamic.h \ - src/test_command_local.cc \ - src/test_command_local.h \ + src/test_command_system.cc \ + src/test_command_system.h \ src/test_command_path.cc \ src/test_command_path.h \ src/test_command_string.cc \ diff --git a/test/src/test_command_local.cc b/test/src/test_command_system.cc similarity index 83% rename from test/src/test_command_local.cc rename to test/src/test_command_system.cc index bde49046..2ddf937f 100644 --- a/test/src/test_command_local.cc +++ b/test/src/test_command_system.cc @@ -1,6 +1,6 @@ #include "config.h" -#include "test/src/test_command_local.h" +#include "test/src/test_command_system.h" #include #include @@ -10,12 +10,12 @@ #include "globals.h" #include "rpc/parse_commands.h" -CPPUNIT_TEST_SUITE_REGISTRATION(TestCommandLocal); +CPPUNIT_TEST_SUITE_REGISTRATION(TestCommandSystem); -void initialize_command_local(); +void initialize_command_system(); void -TestCommandLocal::setUp() { +TestCommandSystem::setUp() { torrent::initialize_main_thread(); torrent::initialize(); @@ -23,16 +23,16 @@ TestCommandLocal::setUp() { control = new Control; if (!rpc::commands.has("system.sockets.size")) - initialize_command_local(); + initialize_command_system(); } void -TestCommandLocal::tearDown() { +TestCommandSystem::tearDown() { torrent::cleanup(); } void -TestCommandLocal::test_socket_category_commands() { +TestCommandSystem::test_socket_category_commands() { for (uint32_t i = 0; i < torrent::runtime::SocketManager::category_count; ++i) { auto category = static_cast(i); auto name = "system.sockets." + torrent::option_to_str_or_throw(torrent::OPTION_SOCKET_CATEGORY, i); diff --git a/test/src/test_command_local.h b/test/src/test_command_system.h similarity index 69% rename from test/src/test_command_local.h rename to test/src/test_command_system.h index 652febfc..7dfd7d2b 100644 --- a/test/src/test_command_local.h +++ b/test/src/test_command_system.h @@ -1,7 +1,7 @@ #include "test/helpers/test_fixture.h" -class TestCommandLocal : public test_fixture { - CPPUNIT_TEST_SUITE(TestCommandLocal); +class TestCommandSystem : public test_fixture { + CPPUNIT_TEST_SUITE(TestCommandSystem); CPPUNIT_TEST(test_socket_category_commands); From f327523a0700d528d2952279827144aa3caea7ad Mon Sep 17 00:00:00 2001 From: aeiouaeiouaeiouaeiouaeiouaeiou Date: Sun, 30 Aug 2026 03:48:07 +0300 Subject: [PATCH 24/34] gzip.h: fix build with Clang <17 Signed-off-by: aeiouaeiouaeiouaeiouaeiouaeiou --- src/utils/gzip.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/gzip.h b/src/utils/gzip.h index 03c0891e..a035f2bf 100644 --- a/src/utils/gzip.h +++ b/src/utils/gzip.h @@ -2,6 +2,7 @@ #define RTORRENT_UTILS_GZIP_H #include +#include namespace utils { From a8be7e443a5bdb5e34d5c0e877d712b59fb2bab3 Mon Sep 17 00:00:00 2001 From: rakshasa Date: Wed, 2 Sep 2026 09:39:57 +0200 Subject: [PATCH 25/34] Tagged release 0.16.22. --- configure.ac | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index cc459306..f2e3cde5 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ m4_pattern_allow([PKG_CHECK_EXISTS]) -AC_INIT([rtorrent],[0.16.21],[sundell.software@gmail.com]) +AC_INIT([rtorrent],[0.16.22],[sundell.software@gmail.com]) AC_CONFIG_HEADERS([config.h]) AC_CONFIG_MACRO_DIRS([scripts]) @@ -49,7 +49,7 @@ fi PKG_CHECK_MODULES([CPPUNIT], [cppunit],, [no_cppunit="yes"]) PKG_CHECK_MODULES([ZLIB], [zlib]) -PKG_CHECK_MODULES([DEPENDENCIES], [libtorrent >= 0.16.21]) +PKG_CHECK_MODULES([DEPENDENCIES], [libtorrent >= 0.16.22]) AC_LANG_PUSH(C++) TORRENT_WITH_XMLRPC_C From 0373d227c17173d903f14ad2e16a6f61d5279175 Mon Sep 17 00:00:00 2001 From: Nicolas PARLANT Date: Wed, 2 Sep 2026 14:08:37 +0200 Subject: [PATCH 26/34] add missing headers for libcxx-23 Signed-off-by: Nicolas PARLANT --- src/setup.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/setup.h b/src/setup.h index fe105c3b..e8cec98b 100644 --- a/src/setup.h +++ b/src/setup.h @@ -3,6 +3,7 @@ #include #include +#include int parse_main_options(int argc, char** argv); void parse_config_file(int argc, char** argv, std::function parse_fn); From 3caa31113ddacfc08ad41fe5f079a41286d4fb32 Mon Sep 17 00:00:00 2001 From: rakshasa Date: Sun, 6 Sep 2026 14:29:50 +0200 Subject: [PATCH 27/34] Bumped scgi max content length to 64mb. --- src/rpc/scgi_task.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc/scgi_task.h b/src/rpc/scgi_task.h index 33b7c181..22f93787 100644 --- a/src/rpc/scgi_task.h +++ b/src/rpc/scgi_task.h @@ -16,7 +16,7 @@ class SCgiTask : public torrent::system::Event { public: static constexpr int default_buffer_size = 8191; static constexpr int max_header_size = 2000; - static constexpr int max_content_size = (2 << 23); + static constexpr int max_content_size = (1 << 26); static constexpr auto timeout_request = std::chrono::seconds(60); From 20002ce937851f65fb7aeb5ae044fa6e2fb38821 Mon Sep 17 00:00:00 2001 From: Jakob Breivik Grimstveit Date: Fri, 11 Sep 2026 01:06:46 +0200 Subject: [PATCH 28/34] Fix false invalid session error on empty URI Avoid triggering 'Session data is invalid' when loading downloads with an empty URI. --- src/core/download_factory.cc | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/core/download_factory.cc b/src/core/download_factory.cc index a243066c..74c548dc 100644 --- a/src/core/download_factory.cc +++ b/src/core/download_factory.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,11 @@ static constexpr const char* session_invalid_message = "Session data is invalid, static std::unique_ptr download_factory_load_stream(const char* filename, bool* is_invalid) { + struct stat sb; + + if (stat(filename, &sb) != 0 || !S_ISREG(sb.st_mode)) + return std::unique_ptr(); + std::fstream stream(filename, std::ios::in | std::ios::binary); if (!stream.is_open()) @@ -167,11 +173,19 @@ void DownloadFactory::receive_success() { bool session_invalid = false; - auto rtorrent_object = download_factory_load_stream((expand_path(m_uri) + ".rtorrent").c_str(), &session_invalid); - auto libtorrent_resume_object = download_factory_load_stream((expand_path(m_uri) + ".libtorrent_resume").c_str(), &session_invalid); + std::unique_ptr rtorrent_object; + std::unique_ptr libtorrent_resume_object; - if (session_invalid) - lt_log_print(torrent::LOG_ERROR, "%s: %s", session_invalid_message, m_uri.c_str()); + if (m_session) { + if (m_uri.empty()) + throw torrent::input_error("Session torrent URI is empty."); + + rtorrent_object = download_factory_load_stream((expand_path(m_uri) + ".rtorrent").c_str(), &session_invalid); + libtorrent_resume_object = download_factory_load_stream((expand_path(m_uri) + ".libtorrent_resume").c_str(), &session_invalid); + + if (session_invalid) + lt_log_print(torrent::LOG_ERROR, "%s: %s", session_invalid_message, m_uri.c_str()); + } uint32_t tracker_key; From c984a69fd1331a4ccc74c845ab22146b635640ae Mon Sep 17 00:00:00 2001 From: noctuum <25441068+noctuum@users.noreply.github.com> Date: Sun, 13 Sep 2026 07:33:01 +0500 Subject: [PATCH 29/34] Fix cidr /32 ip filter entries covering everything above the address. Shifting end_mask by the full type width is undefined behavior. --- src/command_ip.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/command_ip.cc b/src/command_ip.cc index c4194eaf..185ff37d 100644 --- a/src/command_ip.cc +++ b/src/command_ip.cc @@ -206,6 +206,9 @@ ipv4_range_parse(const char* address, uint32_t* address_start, uint32_t* address if (mask_bits == 0) { mask = 0; end_mask = ~(uint32_t)0; + } else if (mask_bits == 32) { + mask = ~(uint32_t)0; + end_mask = 0; } else { mask = (~mask) << (32-mask_bits); end_mask = (~end_mask) >> mask_bits; From af0b600b5409ca803628f767c6d82943ec9acefe Mon Sep 17 00:00:00 2001 From: noctuum <25441068+noctuum@users.noreply.github.com> Date: Sun, 13 Sep 2026 07:33:01 +0500 Subject: [PATCH 30/34] Add unit tests for ipv4_range_parse. Covers single addresses, explicit ranges and the cidr mask boundaries. --- test/Makefile.am | 2 ++ test/src/test_command_ip.cc | 55 +++++++++++++++++++++++++++++++++++++ test/src/test_command_ip.h | 25 +++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 test/src/test_command_ip.cc create mode 100644 test/src/test_command_ip.h diff --git a/test/Makefile.am b/test/Makefile.am index f8f546c6..5432dcb5 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -51,6 +51,8 @@ rtorrent_Test_Rpc_SOURCES = $(rtorrent_Test_Common) \ rtorrent_Test_Src_SOURCES = $(rtorrent_Test_Common) \ src/test_command_dynamic.cc \ src/test_command_dynamic.h \ + src/test_command_ip.cc \ + src/test_command_ip.h \ src/test_command_system.cc \ src/test_command_system.h \ src/test_command_path.cc \ diff --git a/test/src/test_command_ip.cc b/test/src/test_command_ip.cc new file mode 100644 index 00000000..9b34f05f --- /dev/null +++ b/test/src/test_command_ip.cc @@ -0,0 +1,55 @@ +#include "config.h" + +#include "test/src/test_command_ip.h" + +#include + +CPPUNIT_TEST_SUITE_REGISTRATION(TestCommandIp); + +bool ipv4_range_parse(const char* address, uint32_t* address_start, uint32_t* address_end); + +static uint32_t +ipv4(uint32_t a, uint32_t b, uint32_t c, uint32_t d) { + return (a << 24) | (b << 16) | (c << 8) | d; +} + +#define RANGE_ASSERT(address, expected_start, expected_end) \ + { \ + uint32_t start = 0; \ + uint32_t end = 0; \ + \ + CPPUNIT_ASSERT(ipv4_range_parse(address, &start, &end)); \ + CPPUNIT_ASSERT_EQUAL(expected_start, start); \ + CPPUNIT_ASSERT_EQUAL(expected_end, end); \ + } + +void +TestCommandIp::test_single_address() { + RANGE_ASSERT("10.1.2.3", ipv4(10, 1, 2, 3), ipv4(10, 1, 2, 3)); +} + +void +TestCommandIp::test_explicit_range() { + RANGE_ASSERT("10.1.2.3-10.1.2.9", ipv4(10, 1, 2, 3), ipv4(10, 1, 2, 9)); +} + +void +TestCommandIp::test_cidr() { + RANGE_ASSERT("10.0.0.0/8", ipv4(10, 0, 0, 0), ipv4(10, 255, 255, 255)); + RANGE_ASSERT("10.1.2.0/24", ipv4(10, 1, 2, 0), ipv4(10, 1, 2, 255)); + RANGE_ASSERT("10.1.2.128/25", ipv4(10, 1, 2, 128), ipv4(10, 1, 2, 255)); + RANGE_ASSERT("10.1.2.3/31", ipv4(10, 1, 2, 2), ipv4(10, 1, 2, 3)); +} + +void +TestCommandIp::test_cidr_zero_mask() { + RANGE_ASSERT("0.0.0.0/0", ipv4(0, 0, 0, 0), ipv4(255, 255, 255, 255)); + RANGE_ASSERT("10.1.2.3/0", ipv4(0, 0, 0, 0), ipv4(255, 255, 255, 255)); +} + +void +TestCommandIp::test_cidr_full_mask() { + RANGE_ASSERT("10.1.2.3/32", ipv4(10, 1, 2, 3), ipv4(10, 1, 2, 3)); + RANGE_ASSERT("0.0.0.0/32", ipv4(0, 0, 0, 0), ipv4(0, 0, 0, 0)); + RANGE_ASSERT("255.255.255.255/32", ipv4(255, 255, 255, 255), ipv4(255, 255, 255, 255)); +} diff --git a/test/src/test_command_ip.h b/test/src/test_command_ip.h new file mode 100644 index 00000000..9d45a71b --- /dev/null +++ b/test/src/test_command_ip.h @@ -0,0 +1,25 @@ +#include +#include + +// ipv4_range_parse is a pure function, so this does not use test_fixture and +// the mock and logging setup that comes with it. +class TestCommandIp : public CppUnit::TestFixture { + CPPUNIT_TEST_SUITE(TestCommandIp); + + CPPUNIT_TEST(test_single_address); + CPPUNIT_TEST(test_explicit_range); + + CPPUNIT_TEST(test_cidr); + CPPUNIT_TEST(test_cidr_zero_mask); + CPPUNIT_TEST(test_cidr_full_mask); + + CPPUNIT_TEST_SUITE_END(); + +public: + void test_single_address(); + void test_explicit_range(); + + void test_cidr(); + void test_cidr_zero_mask(); + void test_cidr_full_mask(); +}; From 0c507c581f7d0c556dd078d3f943700fbb6bb16d Mon Sep 17 00:00:00 2001 From: noctuum <25441068+noctuum@users.noreply.github.com> Date: Sun, 13 Sep 2026 08:12:53 +0500 Subject: [PATCH 31/34] Release the zlib deflate state in gzip_compress_to_vector. deflateEnd was never called, leaking 168 kB per compressed response. --- src/utils/gzip.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/gzip.cc b/src/utils/gzip.cc index b0a416f0..3c01ad2b 100644 --- a/src/utils/gzip.cc +++ b/src/utils/gzip.cc @@ -5,6 +5,8 @@ #include #include +#include "utils/functional.h" + namespace utils { void @@ -21,6 +23,8 @@ gzip_compress_to_vector(const char* buffer, unsigned int length, std::vector Date: Sun, 13 Sep 2026 08:26:40 +0500 Subject: [PATCH 32/34] Fix a one byte write past address_copy The index bound was the buffer size, not the last writable index. --- src/command_ip.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command_ip.cc b/src/command_ip.cc index 185ff37d..6988a37c 100644 --- a/src/command_ip.cc +++ b/src/command_ip.cc @@ -131,7 +131,7 @@ ipv4_range_parse(const char* address, uint32_t* address_start, uint32_t* address // copy everything up to '#' to address_copy and work from there while(address[address_start_index] != '#' && address[address_start_index] != '\r' && address[address_start_index] != '\n' && address[address_start_index] != '\0' && - address_start_index < 4096 ) { + address_start_index < 4095 ) { address_copy[address_start_index] = address[address_start_index]; address_start_index++; From c41e63248a80994cef85d5c8e7ea3ef9cf991060 Mon Sep 17 00:00:00 2001 From: noctuum <25441068+noctuum@users.noreply.github.com> Date: Sun, 13 Sep 2026 08:44:32 +0500 Subject: [PATCH 33/34] Check the flush and sync on session file writes good() was checked before close(), where the data is actually flushed. --- src/session/download_storer.cc | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/session/download_storer.cc b/src/session/download_storer.cc index 1af2f1cd..1464a906 100644 --- a/src/session/download_storer.cc +++ b/src/session/download_storer.cc @@ -127,8 +127,12 @@ save_stream(const std::string& path, bool use_fsyncdisk, const std::stringstream if (!output.good()) throw torrent::storage_error("failed to write stream to file : " + path); + // The data only reaches the kernel here, so this is where a full disk is seen. output.close(); + if (!output.good()) + throw torrent::storage_error("failed to flush stream to file : " + path); + // Ensure that the new file is actually written to the disk int fd = ::open(path.c_str(), O_WRONLY); @@ -137,13 +141,19 @@ save_stream(const std::string& path, bool use_fsyncdisk, const std::stringstream if (use_fsyncdisk) { #ifdef __APPLE__ - ::fsync(fd); + int sync_result = ::fsync(fd); #else - ::fdatasync(fd); + int sync_result = ::fdatasync(fd); #endif + + if (sync_result == -1) { + ::close(fd); + throw torrent::storage_error("failed to sync file to disk : " + path); + } } - ::close(fd); + if (::close(fd) == -1) + throw torrent::storage_error("failed to close file descriptor : " + path); } } // namespace anonymous From 282c86597a4929e94e242672379b40dbfc5a49e5 Mon Sep 17 00:00:00 2001 From: noctuum <25441068+noctuum@users.noreply.github.com> Date: Sun, 13 Sep 2026 08:46:51 +0500 Subject: [PATCH 34/34] Fix a use after free in the xmlrpc-c index path The string was freed before it was tested, and tmp was never released. --- src/rpc/xmlrpc_c.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rpc/xmlrpc_c.cc b/src/rpc/xmlrpc_c.cc index 08544b03..48b05ad9 100644 --- a/src/rpc/xmlrpc_c.cc +++ b/src/rpc/xmlrpc_c.cc @@ -77,16 +77,18 @@ xmlrpc_list_entry_to_value(xmlrpc_env* env, xmlrpc_value* src, int index) { { const char* str; xmlrpc_read_string(env, tmp, &str); + xmlrpc_DECREF(tmp); if (env->fault_occurred) throw xmlrpc_error_c(env); const char* end = str; int64_t v3 = ::strtoll(str, (char**)&end, 0); + bool invalid = *str == '\0' || *end != '\0'; ::free((void*)str); - if (*str == '\0' || *end != '\0') + if (invalid) throw xmlrpc_error_c(XMLRPC_TYPE_ERROR, "Invalid index."); return v3;