Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package libtorrent for openSUSE:Factory checked in at 2026-09-04 12:41:21 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/libtorrent (Old) and /work/SRC/openSUSE:Factory/.libtorrent.new.1265 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "libtorrent" Fri Sep 4 12:41:21 2026 rev:42 rq:1375601 version:0.16.22 Changes: -------- --- /work/SRC/openSUSE:Factory/libtorrent/libtorrent.changes 2026-08-27 18:55:16.368742303 +0200 +++ /work/SRC/openSUSE:Factory/.libtorrent.new.1265/libtorrent.changes 2026-09-04 12:42:14.149431445 +0200 @@ -1,0 +2,9 @@ +Thu Sep 3 09:37:37 UTC 2026 - Jan Engelhardt <[email protected]> + +- Update to release 0.16.22 + * Reject leading zeros in a bencode integer regardless of sign. + * Add DHT peer nodes learned from the peer protocol PORT message. + * Optionally sanitize download and file names by replacing slash + with underscore. + +------------------------------------------------------------------- Old: ---- libtorrent-0.16.21.tar.gz New: ---- libtorrent-0.16.22.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ libtorrent.spec ++++++ --- /var/tmp/diff_new_pack.h2t0E6/_old 2026-09-04 12:42:14.837455607 +0200 +++ /var/tmp/diff_new_pack.h2t0E6/_new 2026-09-04 12:42:14.838455643 +0200 @@ -16,9 +16,9 @@ # -%define lname libtorrent49 +%define lname libtorrent50 Name: libtorrent -Version: 0.16.21 +Version: 0.16.22 Release: 0 Summary: A BitTorrent library written in C++ License: SUSE-GPL-2.0+-with-openssl-exception ++++++ _scmsync.obsinfo ++++++ --- /var/tmp/diff_new_pack.h2t0E6/_old 2026-09-04 12:42:14.873456872 +0200 +++ /var/tmp/diff_new_pack.h2t0E6/_new 2026-09-04 12:42:14.876456977 +0200 @@ -1,5 +1,5 @@ -mtime: 1787772701 -commit: ea673a9befb1a9b3bcdfbab83956c209cb936f34e8b830e90f7cd564ecc8c5db +mtime: 1788430355 +commit: 4dd2e1cfd0c270494887aebc127d3e49f88ae84f0ba5f3fc63cae5cb3b8687d4 url: https://src.opensuse.org/jengelh/libtorrent revision: master ++++++ build.specials.obscpio ++++++ ++++++ build.specials.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/.gitignore new/.gitignore --- old/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ new/.gitignore 2026-09-03 12:12:35.000000000 +0200 @@ -0,0 +1 @@ +.osc ++++++ libtorrent-0.16.21.tar.gz -> libtorrent-0.16.22.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/configure new/libtorrent-0.16.22/configure --- old/libtorrent-0.16.21/configure 2026-08-26 09:17:56.000000000 +0200 +++ new/libtorrent-0.16.22/configure 2026-09-02 10:22:33.000000000 +0200 @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.72 for libtorrent 0.16.21. +# Generated by GNU Autoconf 2.72 for libtorrent 0.16.22. # # Report bugs to <[email protected]>. # @@ -614,8 +614,8 @@ # Identity of this package. PACKAGE_NAME='libtorrent' PACKAGE_TARNAME='libtorrent' -PACKAGE_VERSION='0.16.21' -PACKAGE_STRING='libtorrent 0.16.21' +PACKAGE_VERSION='0.16.22' +PACKAGE_STRING='libtorrent 0.16.22' PACKAGE_BUGREPORT='[email protected]' PACKAGE_URL='' @@ -1408,7 +1408,7 @@ # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -'configure' configures libtorrent 0.16.21 to adapt to many kinds of systems. +'configure' configures libtorrent 0.16.22 to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1479,7 +1479,7 @@ if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of libtorrent 0.16.21:";; + short | recursive ) echo "Configuration of libtorrent 0.16.22:";; esac cat <<\_ACEOF @@ -1634,7 +1634,7 @@ test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -libtorrent configure 0.16.21 +libtorrent configure 0.16.22 generated by GNU Autoconf 2.72 Copyright (C) 2023 Free Software Foundation, Inc. @@ -2352,7 +2352,7 @@ This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by libtorrent $as_me 0.16.21, which was +It was created by libtorrent $as_me 0.16.22, which was generated by GNU Autoconf 2.72. Invocation command line was $ $0$ac_configure_args_raw @@ -4045,7 +4045,7 @@ # Define the identity of the package. PACKAGE='libtorrent' - VERSION='0.16.21' + VERSION='0.16.22' printf "%s\n" "#define PACKAGE \"$PACKAGE\"" >>confdefs.h @@ -13691,13 +13691,13 @@ # When releasing the first 1.x.y version, we need to start with 1.1.0 (or higher) as we've already # used 0.16.x. -printf "%s\n" "#define PEER_NAME \"-lt1014-\"" >>confdefs.h +printf "%s\n" "#define PEER_NAME \"-lt1016-\"" >>confdefs.h -printf "%s\n" "#define PEER_VERSION \"lt\\x10\\x14\"" >>confdefs.h +printf "%s\n" "#define PEER_VERSION \"lt\\x10\\x16\"" >>confdefs.h -LIBTORRENT_CURRENT=49 +LIBTORRENT_CURRENT=50 LIBTORRENT_REVISION=0 LIBTORRENT_AGE=0 @@ -23348,7 +23348,7 @@ # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" -This file was extended by libtorrent $as_me 0.16.21, which was +This file was extended by libtorrent $as_me 0.16.22, which was generated by GNU Autoconf 2.72. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -23416,7 +23416,7 @@ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config='$ac_cs_config_escaped' ac_cs_version="\\ -libtorrent config.status 0.16.21 +libtorrent config.status 0.16.22 configured by $0, generated by GNU Autoconf 2.72, with options \\"\$ac_cs_config\\" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/configure.ac new/libtorrent-0.16.22/configure.ac --- old/libtorrent-0.16.21/configure.ac 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/configure.ac 2026-09-02 10:22:22.000000000 +0200 @@ -1,4 +1,4 @@ -AC_INIT([[libtorrent]],[[0.16.21]],[[[email protected]]]) +AC_INIT([[libtorrent]],[[0.16.22]],[[[email protected]]]) AC_CONFIG_HEADERS([config.h]) AC_CONFIG_MACRO_DIRS([scripts]) @@ -8,10 +8,10 @@ # When releasing the first 1.x.y version, we need to start with 1.1.0 (or higher) as we've already # used 0.16.x. -AC_DEFINE([[PEER_NAME]], [["-lt1014-"]], [[Identifier that is part of the default peer id.]]) -AC_DEFINE([[PEER_VERSION]], [["lt\x10\x14"]], [[4 byte client and version identifier for DHT.]]) +AC_DEFINE([[PEER_NAME]], [["-lt1016-"]], [[Identifier that is part of the default peer id.]]) +AC_DEFINE([[PEER_VERSION]], [["lt\x10\x16"]], [[4 byte client and version identifier for DHT.]]) -LIBTORRENT_CURRENT=49 +LIBTORRENT_CURRENT=50 LIBTORRENT_REVISION=0 LIBTORRENT_AGE=0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/data/chunk.cc new/libtorrent-0.16.22/src/data/chunk.cc --- old/libtorrent-0.16.21/src/data/chunk.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/data/chunk.cc 2026-09-02 10:22:22.000000000 +0200 @@ -221,7 +221,7 @@ } else { for (char* first = static_cast<char*>(data.first), *last = static_cast<char*>(data.first) + data.second; first < last; first += 4096) - [[maybe_unused]] volatile char touchChunk = *static_cast<char*>(data.first); + [[maybe_unused]] volatile char touchChunk = *first; // Make sure we touch the last page in the range. [[maybe_unused]] volatile char ouchChunk = *(static_cast<char*>(data.first) + data.second - 1); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/dht/dht_router.cc new/libtorrent-0.16.22/src/dht/dht_router.cc --- old/libtorrent-0.16.21/src/dht/dht_router.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/dht/dht_router.cc 2026-09-02 10:22:22.000000000 +0200 @@ -22,8 +22,9 @@ HashString DhtRouter::zero_id; -DhtRouter::DhtRouter(const Object& cache) +DhtRouter::DhtRouter(tracker::DhtController* controller, const Object& cache) : DhtNode(zero_id, sa_make_inet_any().get()), // actual ID is set later + m_controller(controller), m_server(this), m_curToken(random()), m_prevToken(random()), @@ -107,6 +108,8 @@ m_server.start(port); + m_controller->set_nodes_populated(check_nodes_populated()); + // Set timeout slot and schedule it to be called immediately for initial bootstrapping if // necessary. m_task_timeout.slot() = [this] { receive_timeout_bootstrap(); }; @@ -403,7 +406,9 @@ // we have enough nodes in our routing table. After we have 32 nodes, we switch // to a less aggressive non-bootstrap mode of collecting nodes that contact us // and through doing normal torrent announces. - if (m_nodes.size() < num_bootstrap_complete) { + m_controller->set_nodes_populated(check_nodes_populated()); + + if (!check_nodes_populated()) { if (!m_contacts.has_value()) throw internal_error("DhtRouter::receive_timeout_bootstrap called without contact list."); @@ -481,6 +486,8 @@ ++itr; } + m_controller->set_nodes_populated(check_nodes_populated()); + m_server.update(); m_numRefresh++; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/dht/dht_router.h new/libtorrent-0.16.22/src/dht/dht_router.h --- old/libtorrent-0.16.21/src/dht/dht_router.h 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/dht/dht_router.h 2026-09-02 10:22:22.000000000 +0200 @@ -35,7 +35,7 @@ // A node ID of all zero. static HashString zero_id; - DhtRouter(const Object& cache); + DhtRouter(tracker::DhtController* controller, const Object& cache); ~DhtRouter(); void start(int port); @@ -100,6 +100,8 @@ // Maximum number of potential contacts to keep until bootstrap complete. static constexpr unsigned int num_bootstrap_contacts = 64; + bool check_nodes_populated() const { return m_nodes.size() >= num_bootstrap_complete; } + using DhtBucketList = std::map<const HashString, DhtBucket*>; DhtBucketList::iterator find_bucket(const HashString& id); @@ -120,6 +122,8 @@ // buffer needs to hold an SHA1 hash (20 bytes), not just the token (8 bytes) static char* generate_token(const sockaddr* sa, int token, char buffer[20]); + tracker::DhtController* m_controller; + system::SchedulerEntry m_task_timeout; DhtServer m_server{nullptr}; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/download/download_constructor.cc new/libtorrent-0.16.22/src/download/download_constructor.cc --- old/libtorrent-0.16.21/src/download/download_constructor.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/download/download_constructor.cc 2026-09-02 10:22:22.000000000 +0200 @@ -12,11 +12,54 @@ #include "torrent/exceptions.h" #include "torrent/data/file.h" #include "torrent/data/file_list.h" +#include "torrent/runtime/client_config.h" #include "torrent/utils/string_manip.h" #include "tracker/tracker_list.h" namespace torrent { +namespace { + +std::string +sanitize_file_name(std::string name, const char* context) { + auto count = std::count(name.begin(), name.end(), '/'); + + if (count == 0) + return name; + + auto replace_slash = runtime::client_config()->file_name_replace_slash(); + + if (replace_slash.empty()) + throw input_error("Invalid torrent file, " + std::string(context) + " contains '/' but 'system.file_name.replace_slash' is empty."); + + std::string result; + result.reserve(name.size() + count * (replace_slash.size() - 1)); + + for (char c : name) { + if (c == '/') + result += replace_slash; + else + result += c; + } + + return result; +} + +bool +any_of_shared_prefix(const std::vector<FileList::split_type>& split_list) { + std::vector<const Path*> sorted_paths; + sorted_paths.reserve(split_list.size()); + + for (const auto& split : split_list) + sorted_paths.push_back(&std::get<1>(split)); + + std::sort(sorted_paths.begin(), sorted_paths.end(), &Path::compare_less); + + return std::adjacent_find(sorted_paths.begin(), sorted_paths.end(), &Path::is_prefix) != sorted_paths.end(); +} + +} // namespace + void DownloadConstructor::initialize(Object& b) { if (!b.has_key_map("info") && b.has_key_string("magnet-uri")) @@ -35,10 +78,18 @@ void DownloadConstructor::parse_name(const Object& b) { - if (is_invalid_path_element(b.get_key("name"))) - throw input_error("Bad torrent file, \"name\" is an invalid path name."); + auto original_name = b.get_key_string("name"); + auto sanitized_name = sanitize_file_name(original_name, "dict-key \"name\""); + + if (!Path::is_valid_component(sanitized_name)) + throw input_error("Bad torrent file, dict-key \"name\" contains invalid characters or is empty."); + + if (runtime::client_config()->torrent_name_use_sanitized()) + m_download->info()->set_name(sanitized_name); + else + m_download->info()->set_name(original_name); - m_download->info()->set_name(b.get_key_string("name")); + m_download->info()->set_name_sanitized(sanitized_name); } void @@ -81,7 +132,7 @@ } else if (b.has_key("files")) { parse_multi_files(b.get_key("files"), chunkSize); - fileList->set_root_dir("./" + m_download->info()->name().str()); + fileList->set_root_dir("./" + sanitize_file_name(m_download->info()->name().str(), "dict-key \"name\"")); } else if (!m_download->info()->is_meta_download()) { throw input_error("Torrent must have either length or files entry."); @@ -148,22 +199,8 @@ m_download->main()->tracker_list()->insert_url(group, utils::trim_spaces_str(b.as_string())); } -bool -DownloadConstructor::is_valid_path_element(const Object& b) { - return - b.is_string() && - !b.as_string().empty() && - b.as_string() != "." && - b.as_string() != ".." && - std::find(b.as_string().begin(), b.as_string().end(), '/') == b.as_string().end() && - std::find(b.as_string().begin(), b.as_string().end(), '\0') == b.as_string().end(); -} - void DownloadConstructor::parse_single_file(const Object& b, uint32_t chunkSize) { - if (is_invalid_path_element(b.get_key("name"))) - throw input_error("Bad torrent file, \"name\" is an invalid path name."); - int64_t length = chunkSize == 1 ? 1 : b.get_key_value("length"); if (length < 0) @@ -174,10 +211,7 @@ fileList->set_multi_file(false); Path path; - path.push_back(b.get_key_string("name")); - - if (path.empty()) - throw input_error("Bad torrent file, an entry has no valid filename."); + path.push_back_component(sanitize_file_name(b.get_key_string("name"), "dict-key \"name\"")); *fileList->front()->mutable_path() = path; fileList->update_paths(fileList->begin(), fileList->end()); @@ -192,6 +226,7 @@ throw input_error("Bad torrent file, entry has no files."); int64_t torrent_size = 0; + std::vector<FileList::split_type> split_list; split_list.reserve(object_list.size()); @@ -221,15 +256,7 @@ split_list.emplace_back(length, path, attr_flags); } - std::vector<const Path*> sorted_paths; - sorted_paths.reserve(split_list.size()); - - for (const auto& split : split_list) - sorted_paths.push_back(&std::get<1>(split)); - - std::sort(sorted_paths.begin(), sorted_paths.end(), &Path::compare_less); - - if (std::adjacent_find(sorted_paths.begin(), sorted_paths.end(), &Path::is_prefix) != sorted_paths.end()) + if (any_of_shared_prefix(split_list)) throw input_error("Bad torrent file, a file path is a duplicate or the prefix of another."); FileList* file_list = m_download->main()->file_list(); @@ -240,19 +267,21 @@ file_list->update_paths(file_list->begin(), file_list->end()); } -inline Path +Path DownloadConstructor::create_path(const Object::list_type& plist) { - // Make sure we are given a proper file path. if (plist.empty()) throw input_error("Bad torrent file, \"path\" has zero entries."); - if (std::any_of(plist.begin(), plist.end(), &DownloadConstructor::is_invalid_path_element)) - throw input_error("Bad torrent file, \"path\" has zero entries or a zero length entry."); - Path p; - for (const auto& path : plist) - p.push_back(path.as_string()); + for (const auto& path : plist) { + auto sanitized_name = sanitize_file_name(path.as_string(), "a value in dict-key \"path\""); + + if (!Path::is_valid_component(sanitized_name)) + throw input_error("Bad torrent file, a value in dict-key \"path\" contains invalid characters or is empty."); + + p.push_back(sanitized_name); + } return p; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/download/download_constructor.h new/libtorrent-0.16.22/src/download/download_constructor.h --- old/libtorrent-0.16.21/src/download/download_constructor.h 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/download/download_constructor.h 2026-09-02 10:22:22.000000000 +0200 @@ -27,9 +27,6 @@ void add_tracker_group(const Object& b); void add_tracker_single(const Object& b, int group); - static bool is_valid_path_element(const Object& b); - static bool is_invalid_path_element(const Object& b) { return !is_valid_path_element(b); } - void parse_single_file(const Object& b, uint32_t chunkSize); void parse_multi_files(const Object& b, uint32_t chunkSize); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/manager.cc new/libtorrent-0.16.22/src/manager.cc --- old/libtorrent-0.16.21/src/manager.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/manager.cc 2026-09-02 10:22:22.000000000 +0200 @@ -110,6 +110,7 @@ m_resource_manager->receive_tick(); m_chunk_manager->periodic_sync(); + m_file_manager->periodic_close_idle(); // To ensure the downloads get equal chance over time at using // various limited resources, like sockets for handshakes, cycle the diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/net/curl_socket.cc new/libtorrent-0.16.22/src/net/curl_socket.cc --- old/libtorrent-0.16.21/src/net/curl_socket.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/net/curl_socket.cc 2026-09-02 10:22:22.000000000 +0200 @@ -349,6 +349,8 @@ // while (stack->process_done_handle()) ; // Do nothing. + + stack->ensure_timeout_scheduled(); } void diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/net/curl_stack.cc new/libtorrent-0.16.22/src/net/curl_stack.cc --- old/libtorrent-0.16.21/src/net/curl_stack.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/net/curl_stack.cc 2026-09-02 10:22:22.000000000 +0200 @@ -164,6 +164,8 @@ curl_get->activate_unsafe(); } + + ensure_timeout_scheduled(); } void @@ -236,16 +238,29 @@ return; } - if (!m_task_timeout.is_scheduled()) { - // Sometimes libcurl forgets to reset the timeout. Try to poll the value in that case, or use 10 - // seconds max. - long timeout_ms; - curl_multi_timeout(m_handle, &timeout_ms); + ensure_timeout_scheduled(); +} - auto timeout = std::max<std::chrono::microseconds>(std::chrono::milliseconds(timeout_ms), 10s); +// The multi-socket API only enforces CURLOPT_TIMEOUT when libcurl is driven by socket activity or +// the timer task. If the timer is not scheduled while transfers are active (e.g. libcurl cleared +// it, or a connection through a dead proxy never produces socket events), a hung transfer would +// never time out. Guarantee the stack is polled at least every 10 seconds while non-empty. +void +CurlStack::ensure_timeout_scheduled() { + assert(std::this_thread::get_id() == m_thread->thread_id()); - torrent::this_thread::scheduler()->wait_for_ceil_seconds(&m_task_timeout, timeout); - } + if (base_type::empty() || m_task_timeout.is_scheduled()) + return; + + long timeout_ms{}; + curl_multi_timeout(m_handle, &timeout_ms); + + std::chrono::microseconds timeout = 10s; + + if (timeout_ms >= 0) + timeout = std::min<std::chrono::microseconds>(std::chrono::milliseconds(timeout_ms), timeout); + + torrent::this_thread::scheduler()->wait_for_ceil_seconds(&m_task_timeout, timeout); } // TODO: Check if curl_get is still active. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/net/curl_stack.h new/libtorrent-0.16.22/src/net/curl_stack.h --- old/libtorrent-0.16.21/src/net/curl_stack.h 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/net/curl_stack.h 2026-09-02 10:22:22.000000000 +0200 @@ -63,6 +63,8 @@ bool process_done_handle(); + void ensure_timeout_scheduled(); + system::Thread* thread() const { return m_thread; } protected: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/protocol/handshake_manager.cc new/libtorrent-0.16.22/src/protocol/handshake_manager.cc --- old/libtorrent-0.16.21/src/protocol/handshake_manager.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/protocol/handshake_manager.cc 2026-09-02 10:22:22.000000000 +0200 @@ -220,8 +220,13 @@ handshake->destroy_connection(); }; - if (!download->info()->is_active()) + if (!download->info()->is_active()) { + // Fix race: inactive drop must not arm connect_filter_recent for 600s. + if (handshake->peer_info() != nullptr) + handshake->peer_info()->set_last_handshake(0); + return error_func(Handshake::e_handshake_inactive_download); + } if (!download->connection_list()->want_connection(handshake->peer_info(), handshake->bitfield())) return error_func(Handshake::e_handshake_unwanted_connection); @@ -273,6 +278,10 @@ auto handshake = find_and_erase(ptr); auto sa = handshake->socket_address(); + // Fix race: same as receive_succeeded. + if (error == Handshake::e_handshake_inactive_download && handshake->peer_info() != nullptr) + handshake->peer_info()->set_last_handshake(0); + handshake->destroy_connection(); LT_LOG_SA(sa, "received error: message:%x %s.", message, handshake_strerror(error)); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/data/file.h new/libtorrent-0.16.22/src/torrent/data/file.h --- old/libtorrent-0.16.21/src/torrent/data/file.h 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/data/file.h 2026-09-02 10:22:22.000000000 +0200 @@ -82,6 +82,7 @@ void set_protection(int prot) { m_protection = prot; } uint64_t last_touched() const { return m_last_touched; } + auto last_touched_usec() const { return std::chrono::microseconds(m_last_touched); } void set_last_touched(uint64_t t) { m_last_touched = t; } protected: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/data/file_manager.cc new/libtorrent-0.16.22/src/torrent/data/file_manager.cc --- old/libtorrent-0.16.21/src/torrent/data/file_manager.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/data/file_manager.cc 2026-09-02 10:22:22.000000000 +0200 @@ -12,6 +12,7 @@ #include "data/socket_file.h" #include "torrent/exceptions.h" #include "torrent/data/file.h" +#include "torrent/utils/chrono.h" #include "utils/fd_close_queue.h" namespace torrent { @@ -81,6 +82,9 @@ void FileManager::close_files(const std::vector<File*>& files) { + if (files.empty()) + return; + std::vector<int> closed_fds; closed_fds.reserve(files.size()); @@ -96,6 +100,9 @@ void FileManager::close_files(const std::vector<std::unique_ptr<File>>& files) { + if (files.empty()) + return; + std::vector<int> closed_fds; closed_fds.reserve(files.size()); @@ -248,4 +255,31 @@ return count; } +void +FileManager::periodic_close_idle() { + if (m_close_idle_timeout == 0s || empty()) + return; + + auto cached_seconds = this_thread::cached_seconds(); + + if (cached_seconds - m_close_idle_last_check < m_close_idle_timeout / 4) + return; + + m_close_idle_last_check = cached_seconds; + + std::vector<File*> files; + + for (auto* file : *this) { + if (!file->is_open()) + continue; + + auto last_touched = utils::cast_seconds(file->last_touched_usec()); + + if (last_touched < cached_seconds && cached_seconds - last_touched > m_close_idle_timeout) + files.push_back(file); + } + + close_files(files); +} + } // namespace torrent diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/data/file_manager.h new/libtorrent-0.16.22/src/torrent/data/file_manager.h --- old/libtorrent-0.16.21/src/torrent/data/file_manager.h 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/data/file_manager.h 2026-09-02 10:22:22.000000000 +0200 @@ -42,6 +42,9 @@ bool advise_random_hashing() const { return m_advise_random_hashing; } void set_advise_random_hashing(bool state) { m_advise_random_hashing = state; } + auto close_idle_timeout() const; + void set_close_idle_timeout(std::chrono::seconds timeout); + bool open(File* file, bool hashing, int prot, int flags); void close(File* file); @@ -53,6 +56,8 @@ // void close_least_active(); + void periodic_close_idle(); + // Statistics: uint64_t files_opened_counter() const { return m_files_opened_counter; } uint64_t files_closed_counter() const { return m_files_closed_counter; } @@ -74,7 +79,11 @@ void evict_least_active(unsigned int count); unsigned int evict_least_active_from_cache(unsigned int count); - size_type m_max_open_files{}; + size_type m_max_open_files{}; + + std::chrono::seconds m_close_idle_timeout{10min}; + std::chrono::seconds m_close_idle_last_check{}; + bool m_advise_random{}; bool m_advise_random_hashing{}; @@ -87,6 +96,9 @@ std::unique_ptr<utils::FdCloseQueue> m_fd_close_queue; }; +inline auto FileManager::close_idle_timeout() const { return m_close_idle_timeout; } +inline void FileManager::set_close_idle_timeout(std::chrono::seconds timeout) { m_close_idle_timeout = timeout; } + } // namespace torrent #endif diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/download_info.h new/libtorrent-0.16.22/src/torrent/download_info.h --- old/libtorrent-0.16.21/src/torrent/download_info.h 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/download_info.h 2026-09-02 10:22:22.000000000 +0200 @@ -50,6 +50,9 @@ const string_utf8& name() const { return m_name; } void set_name(const std::string& s) { m_name.reset(s); } + const std::string& name_sanitized() const { return m_name_sanitized; } + void set_name_sanitized(const std::string& s) { m_name_sanitized = s; } + const HashString& hash() const { return m_hash; } const HashString& info_hash() const { return m_hash; } HashString& mutable_hash() { return m_hash; } @@ -140,6 +143,7 @@ private: string_utf8 m_name; + std::string m_name_sanitized; HashString m_hash{HashString::new_zero()}; HashString m_hash_obfuscated{HashString::new_zero()}; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/hash_string.h new/libtorrent-0.16.22/src/torrent/hash_string.h --- old/libtorrent-0.16.21/src/torrent/hash_string.h 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/hash_string.h 2026-09-02 10:22:22.000000000 +0200 @@ -42,6 +42,8 @@ reference operator [] (size_type n) { return *(m_data + n); } const_reference operator [] (size_type n) const { return *(m_data + n); } + unsigned char at_uchar(size_type n) const { return static_cast<unsigned char>(*(m_data + n)); } + value_type* data() { return m_data; } const value_type* data() const { return m_data; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/object_stream.cc new/libtorrent-0.16.22/src/torrent/object_stream.cc --- old/libtorrent-0.16.21/src/torrent/object_stream.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/object_stream.cc 2026-09-02 10:22:22.000000000 +0200 @@ -59,11 +59,17 @@ if (errc.ptr >= last || *errc.ptr != 'e') throw torrent::bencode_error("Invalid bencode data: missing 'e' terminator."); - if (value != 0 && *first == '0') - throw torrent::bencode_error("Invalid bencode data: leading zeros are not allowed."); + if (*first == '-') { + if (value == 0) + throw torrent::bencode_error("Invalid bencode data: negative zero is not allowed."); - if (value == 0 && *first == '-') - throw torrent::bencode_error("Invalid bencode data: negative zero is not allowed."); + if (*(first + 1) == '0' && errc.ptr != first + 2) + throw torrent::bencode_error("Invalid bencode data: leading zeros are not allowed."); + + } else { + if (*first == '0' && errc.ptr != first + 1) + throw torrent::bencode_error("Invalid bencode data: leading zeros are not allowed."); + } return errc.ptr + 1; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/path.cc new/libtorrent-0.16.22/src/torrent/path.cc --- old/libtorrent-0.16.21/src/torrent/path.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/path.cc 2026-09-02 10:22:22.000000000 +0200 @@ -1,41 +1,67 @@ #include "config.h" +#include "path.h" + #include <algorithm> -#include "path.h" +#include "torrent/exceptions.h" namespace torrent { +bool +Path::is_prefix(const Path* prefix, const Path* path) { + return prefix->size() <= path->size() && + std::equal(prefix->begin(), prefix->end(), path->begin(), + [](const auto& l, const auto& r) { return l.str() == r.str(); }); +} + +bool +Path::is_valid_component(const std::string& name) { + if (name.empty()) + return false; + + if (name == "." || name == "..") + return false; + + if (std::any_of(name.begin(), name.end(), [](char c) { return c == '/' || c == '\0'; })) + return false; + + return true; +} + void Path::insert_path(iterator pos, const std::string& path) { - std::string::const_iterator first = path.begin(); - std::string::const_iterator last; + if (std::find(path.begin(), path.end(), '\0') != path.end()) + throw input_error("Invalid path, contains null character."); + + auto first = path.begin(); while (first != path.end()) { - last = std::find(first, path.end(), '/'); - pos = insert(pos, string_utf8::from_string(std::string(first, last))); + auto last = std::find(first, path.end(), '/'); + + pos = insert(pos, string_utf8::from_string(std::string(first, last))); if (last == path.end()) return; - first = last; - first++; + first = last + 1; } } +void +Path::insert_component(iterator pos, const std::string& name) { + if (!is_valid_component(name)) + throw input_error("Invalid path component name."); + + insert(pos, string_utf8::from_string(name)); +} + bool Path::compare_less(const Path* left, const Path* right) { return std::lexicographical_compare(left->begin(), left->end(), right->begin(), right->end(), [](const auto& l, const auto& r) { return l.str() < r.str(); }); } -bool -Path::is_prefix(const Path* prefix, const Path* path) { - return prefix->size() <= path->size() && - std::equal(prefix->begin(), prefix->end(), path->begin(), - [](const auto& l, const auto& r) { return l.str() == r.str(); }); -} - std::string Path::as_string() const { if (empty()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/path.h new/libtorrent-0.16.22/src/torrent/path.h --- old/libtorrent-0.16.21/src/torrent/path.h 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/path.h 2026-09-02 10:22:22.000000000 +0200 @@ -27,8 +27,14 @@ using base_type::at; using base_type::operator[]; + static bool is_prefix(const Path* prefix, const Path* path); + static bool is_valid_component(const std::string& name); + void insert_path(iterator pos, const std::string& path); + void insert_component(iterator pos, const std::string& name); + void push_back(const std::string& path); + void push_back_component(const std::string& name); // Return the path as a string with '/' deliminator. The deliminator // is only inserted between path elements. @@ -38,10 +44,10 @@ const base_type* base() const { return this; } static bool compare_less(const Path* left, const Path* right); - static bool is_prefix(const Path* prefix, const Path* path); }; -inline void Path::push_back(const std::string& path) { insert_path(end(), path); } +inline void Path::push_back(const std::string& path) { insert_path(end(), path); } +inline void Path::push_back_component(const std::string& name) { insert_component(end(), name); } } // namespace torrent diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/peer/client_list.cc new/libtorrent-0.16.22/src/torrent/peer/client_list.cc --- old/libtorrent-0.16.21/src/torrent/peer/client_list.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/peer/client_list.cc 2026-09-02 10:22:22.000000000 +0200 @@ -198,8 +198,8 @@ bool ClientList::retrieve_id(ClientInfo* dest, const HashString& id) const { if (id[0] == '-' && id[7] == '-' && - std::isalpha(id[1]) && std::isalpha(id[2]) && - std::isxdigit(id[3]) && std::isxdigit(id[4]) && std::isxdigit(id[5]) && std::isxdigit(id[6])) { + std::isalpha(id.at_uchar(1)) && std::isalpha(id.at_uchar(2)) && + std::isxdigit(id.at_uchar(3)) && std::isxdigit(id.at_uchar(4)) && std::isxdigit(id.at_uchar(5)) && std::isxdigit(id.at_uchar(6))) { dest->set_type(ClientInfo::TYPE_AZUREUS); dest->mutable_key()[0] = id[1]; @@ -208,8 +208,8 @@ for (int i = 0; i < 4; i++) dest->mutable_version()[i] = dest->mutable_upper_version()[i] = utils::hex_to_value_or_zero(id[3 + i]); - } else if (std::isalpha(id[0]) && id[4] == '-' && - std::isxdigit(id[1]) && std::isxdigit(id[2]) && std::isxdigit(id[3])) { + } else if (std::isalpha(id.at_uchar(0)) && id[4] == '-' && + std::isxdigit(id.at_uchar(1)) && std::isxdigit(id.at_uchar(2)) && std::isxdigit(id.at_uchar(3))) { dest->set_type(ClientInfo::TYPE_COMPACT); dest->mutable_key()[0] = id[0]; @@ -220,8 +220,8 @@ dest->mutable_version()[2] = dest->mutable_upper_version()[2] = utils::hex_to_value_or_zero(id[3]); dest->mutable_version()[3] = dest->mutable_upper_version()[3] = '\0'; - } else if (std::isalpha(id[0]) && std::isdigit(id[1]) && id[2] == '-' && - std::isdigit(id[3]) && (id[6] == '-' || id[7] == '-')) { + } else if (std::isalpha(id.at_uchar(0)) && std::isdigit(id.at_uchar(1)) && id[2] == '-' && + std::isdigit(id.at_uchar(3)) && (id[6] == '-' || id[7] == '-')) { dest->set_type(ClientInfo::TYPE_MAINLINE); @@ -230,12 +230,12 @@ dest->mutable_version()[0] = dest->mutable_upper_version()[0] = utils::hex_to_value_or_zero(id[1]); - if (id[4] == '-' && std::isdigit(id[5]) && id[6] == '-') { + if (id[4] == '-' && std::isdigit(id.at_uchar(5)) && id[6] == '-') { dest->mutable_version()[1] = dest->mutable_upper_version()[1] = utils::hex_to_value_or_zero(id[3]); dest->mutable_version()[2] = dest->mutable_upper_version()[2] = utils::hex_to_value_or_zero(id[5]); dest->mutable_version()[3] = dest->mutable_upper_version()[3] = '\0'; - } else if (std::isdigit(id[4]) && id[5] == '-' && std::isdigit(id[6]) && id[7] == '-') { + } else if (std::isdigit(id.at_uchar(4)) && id[5] == '-' && std::isdigit(id.at_uchar(6)) && id[7] == '-') { dest->mutable_version()[1] = dest->mutable_upper_version()[1] = utils::hex_to_value_or_zero(id[3]) * 10 + utils::hex_to_value_or_zero(id[4]); dest->mutable_version()[2] = dest->mutable_upper_version()[2] = utils::hex_to_value_or_zero(id[6]); dest->mutable_version()[3] = dest->mutable_upper_version()[3] = '\0'; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/runtime/client_config.cc new/libtorrent-0.16.22/src/torrent/runtime/client_config.cc --- old/libtorrent-0.16.21/src/torrent/runtime/client_config.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/runtime/client_config.cc 2026-09-02 10:22:22.000000000 +0200 @@ -24,4 +24,14 @@ m_listen_port_last = last; } +void +ClientConfig::set_file_name_replace_slash(const std::string& str) { + if (str.find('/') != std::string::npos || str.find('\0') != std::string::npos) + throw input_error("Invalid string for file_name.replace_slash, contains '/' or null character."); + + auto guard = lock_guard(); + + m_file_name_replace_slash = str; +} + } // namespace torrent::runtime diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/runtime/client_config.h new/libtorrent-0.16.22/src/torrent/runtime/client_config.h --- old/libtorrent-0.16.21/src/torrent/runtime/client_config.h 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/runtime/client_config.h 2026-09-02 10:22:22.000000000 +0200 @@ -21,6 +21,12 @@ bool is_pex_enabled() const; void set_pex_enabled(bool v); + bool torrent_name_use_sanitized() const; + void set_torrent_name_use_sanitized(bool v); + + std::string file_name_replace_slash() const; + void set_file_name_replace_slash(const std::string& str); + protected: friend class torrent::RuntimeManager; @@ -35,19 +41,27 @@ uint16_t m_listen_port_first{6881}; uint16_t m_listen_port_last{6999}; + std::string m_file_name_replace_slash{"_"}; + align_cacheline std::atomic<bool> m_listen_port_random{true}; std::atomic<bool> m_pex_enabled{true}; + std::atomic<bool> m_torrent_name_use_sanitized{true}; }; -inline auto ClientConfig::lock_guard() const { return std::lock_guard(m_mutex); } +inline auto ClientConfig::lock_guard() const { return std::lock_guard(m_mutex); } + +inline bool ClientConfig::listen_port_random() const { return m_listen_port_random; } +inline void ClientConfig::set_listen_port_random(bool v) { m_listen_port_random = v; } + +inline bool ClientConfig::is_pex_enabled() const { return m_pex_enabled; } +inline void ClientConfig::set_pex_enabled(bool v) { m_pex_enabled = v; } -inline bool ClientConfig::listen_port_random() const { return m_listen_port_random; } -inline void ClientConfig::set_listen_port_random(bool v) { m_listen_port_random = v; } +inline bool ClientConfig::torrent_name_use_sanitized() const { return m_torrent_name_use_sanitized; } +inline void ClientConfig::set_torrent_name_use_sanitized(bool v) { m_torrent_name_use_sanitized = v; } -inline bool ClientConfig::is_pex_enabled() const { return m_pex_enabled; } -inline void ClientConfig::set_pex_enabled(bool v) { m_pex_enabled = v; } +inline std::string ClientConfig::file_name_replace_slash() const { auto guard = lock_guard(); return m_file_name_replace_slash; } } // namespace torrent::runtime diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/runtime/network_manager.cc new/libtorrent-0.16.22/src/torrent/runtime/network_manager.cc --- old/libtorrent-0.16.21/src/torrent/runtime/network_manager.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/runtime/network_manager.cc 2026-09-02 10:22:22.000000000 +0200 @@ -169,10 +169,11 @@ } void -NetworkManager::dht_add_peer_node([[maybe_unused]] const sockaddr* sa, [[maybe_unused]] int port) { - // Ignore peer nodes as we shouldn't need them(?) - // - // Re-enable this if it causes issues. +NetworkManager::dht_add_peer_node(const sockaddr* sa, int port) { + if (m_dht_controller->is_nodes_populated()) + return; + + m_dht_controller->add_node(sa, port); } bool diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/system/scheduler.h new/libtorrent-0.16.22/src/torrent/system/scheduler.h --- old/libtorrent-0.16.21/src/torrent/system/scheduler.h 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/system/scheduler.h 2026-09-02 10:22:22.000000000 +0200 @@ -3,6 +3,7 @@ #include <functional> #include <thread> +#include <vector> #include <torrent/system/common.h> namespace torrent::system { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/tracker/dht_controller.cc new/libtorrent-0.16.22/src/torrent/tracker/dht_controller.cc --- old/libtorrent-0.16.21/src/torrent/tracker/dht_controller.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/tracker/dht_controller.cc 2026-09-02 10:22:22.000000000 +0200 @@ -56,7 +56,7 @@ LT_LOG("initializing", 0); try { - m_router = std::make_unique<DhtRouter>(dht_cache); + m_router = std::make_unique<DhtRouter>(this, dht_cache); } catch (const torrent::local_error& e) { LT_LOG("initialization failed : %s", e.what()); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/tracker/dht_controller.h new/libtorrent-0.16.22/src/torrent/tracker/dht_controller.h --- old/libtorrent-0.16.21/src/torrent/tracker/dht_controller.h 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/tracker/dht_controller.h 2026-09-02 10:22:22.000000000 +0200 @@ -1,6 +1,7 @@ #ifndef LIBTORRENT_TRACKER_DHT_CONTROLLER_H #define LIBTORRENT_TRACKER_DHT_CONTROLLER_H +#include <atomic> #include <memory> #include <mutex> #include <torrent/common.h> @@ -44,6 +45,7 @@ bool is_valid(); bool is_active(); bool is_receiving_requests(); + bool is_nodes_populated(); void set_receive_requests(bool state); @@ -69,13 +71,18 @@ void reset_statistics(); protected: + friend class torrent::DhtRouter; friend class torrent::TrackerDht; + void set_nodes_populated(bool state); + // Called from tracker_thread. void announce(const HashString& info_hash, std::weak_ptr<TrackerDht> weak_tracker); void cancel_announce(const HashString& info_hash, std::weak_ptr<TrackerDht> weak_tracker); private: + std::atomic<bool> m_nodes_populated{false}; + std::mutex m_lock; uint16_t m_port{0}; bool m_receive_requests{true}; @@ -83,6 +90,16 @@ std::unique_ptr<DhtRouter> m_router; }; +inline bool +DhtController::is_nodes_populated() { + return m_nodes_populated.load(std::memory_order_relaxed); +} + +inline void +DhtController::set_nodes_populated(bool state) { + m_nodes_populated.store(state, std::memory_order_relaxed); +} + } // namespace torrent::tracker #endif // LIBTORRENT_TRACKER_DHT_CONTROLLER_H diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/torrent/utils/log.cc new/libtorrent-0.16.22/src/torrent/utils/log.cc --- old/libtorrent-0.16.21/src/torrent/utils/log.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/torrent/utils/log.cc 2026-09-02 10:22:22.000000000 +0200 @@ -280,8 +280,20 @@ auto itr = log_find_output_name(name); - if (itr != log_outputs.end()) - log_outputs.erase(itr); + if (itr == log_outputs.end()) + return; + + size_t index = std::distance(log_outputs.begin(), itr); + log_outputs.erase(itr); + + for (auto& group : log_groups) { + auto outputs = group.outputs(); + auto lower = outputs & log_group::outputs_type((uint64_t{1} << index) - 1); + + group.set_outputs(lower | ((outputs >> (index + 1)) << index)); + } + + log_rebuild_cache(); } void diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/tracker/tracker_controller.cc new/libtorrent-0.16.22/src/tracker/tracker_controller.cc --- old/libtorrent-0.16.21/src/tracker/tracker_controller.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/tracker/tracker_controller.cc 2026-09-02 10:22:22.000000000 +0200 @@ -548,7 +548,7 @@ if ((m_flags & flag_requesting)) update_timeout(30); - else if (!m_tracker_list->has_active()) { + else if (!m_tracker_list->has_active_not_scrape()) { std::chrono::seconds normal_interval; tracker.lock_and_call_state([&](const tracker::TrackerState& state) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/tracker/tracker_http.cc new/libtorrent-0.16.22/src/tracker/tracker_http.cc --- old/libtorrent-0.16.21/src/tracker/tracker_http.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/tracker/tracker_http.cc 2026-09-02 10:22:22.000000000 +0200 @@ -498,13 +498,11 @@ LT_LOG("received failure : previous family succeeded : url:%s : %s", info().url.c_str(), msg.c_str()); m_slot_success(AddressList()); - } else if (!m_last_error_message.empty()) { - LT_LOG("received failure : previous family also failed : url:%s : %s /// %s", info().url.c_str(), msg.c_str(), m_last_error_message.c_str()); - m_slot_failure(msg + " /// " + m_last_error_message); - } else { - LT_LOG("received failure : url:%s : %s", info().url.c_str(), msg.c_str()); - m_slot_failure(msg); + auto error_msg = generate_error_message(m_current_family, msg, m_last_error_message); + + LT_LOG("received failure : url:%s : %s", info().url.c_str(), error_msg.c_str()); + m_slot_failure(error_msg); } if (m_requested_scrape && m_data == nullptr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/tracker/tracker_http.h new/libtorrent-0.16.22/src/tracker/tracker_http.h --- old/libtorrent-0.16.21/src/tracker/tracker_http.h 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/tracker/tracker_http.h 2026-09-02 10:22:22.000000000 +0200 @@ -64,6 +64,7 @@ bool m_last_success{}; std::string m_last_error_message; + bool m_requested_scrape{}; system::SchedulerEntry m_delay_scrape; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/tracker/tracker_udp.cc new/libtorrent-0.16.22/src/tracker/tracker_udp.cc --- old/libtorrent-0.16.21/src/tracker/tracker_udp.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/tracker/tracker_udp.cc 2026-09-02 10:22:22.000000000 +0200 @@ -111,6 +111,8 @@ // Don't clear packet_sent to ensure disownable flag remains set. switch (family) { case AF_INET: + m_inet_state.error_message = msg; + if (m_inet_state.transaction_id == 0) return; // TODO: Should we throw? @@ -118,6 +120,8 @@ m_inet_state.connection_id = 0; break; case AF_INET6: + m_inet6_state.error_message = msg; + if (m_inet6_state.transaction_id == 0) return; // TODO: Should we throw? @@ -129,14 +133,16 @@ } if (m_inet_state.transaction_id != 0 || m_inet6_state.transaction_id != 0) - return; // TODO: Save message. + return; LT_LOG("closing with error : hostname:%s port:%u : %s", m_hostname.c_str(), m_port, msg.c_str()); remove_events(); update_requesting_state(); - m_slot_failure(msg); + auto& last_msg = family == AF_INET ? m_inet6_state.error_message : m_inet_state.error_message; + + m_slot_failure(generate_error_message(family, msg, last_msg)); } void diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/tracker/tracker_udp.h new/libtorrent-0.16.22/src/tracker/tracker_udp.h --- old/libtorrent-0.16.21/src/tracker/tracker_udp.h 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/tracker/tracker_udp.h 2026-09-02 10:22:22.000000000 +0200 @@ -25,9 +25,10 @@ using buffer_type = ProtocolBuffer<512>; struct family_state { - uint32_t transaction_id{}; - uint64_t connection_id{}; - bool packet_sent{}; + uint32_t transaction_id{}; + uint64_t connection_id{}; + bool packet_sent{}; + std::string error_message; }; void close_directly(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/tracker/tracker_worker.cc new/libtorrent-0.16.22/src/tracker/tracker_worker.cc --- old/libtorrent-0.16.21/src/tracker/tracker_worker.cc 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/tracker/tracker_worker.cc 2026-09-02 10:22:22.000000000 +0200 @@ -2,6 +2,8 @@ #include "tracker_worker.h" +#include <netinet/in.h> + #include "torrent/exceptions.h" #include "torrent/system/callbacks.h" @@ -33,4 +35,23 @@ system::cancel_callback_and_wait(m_callback_id, main_thread::thread(), tracker_thread::thread()); } +std::string +TrackerWorker::generate_error_message(int current_family, const std::string& current_msg, const std::string& last_msg) { + constexpr auto not_resolved_msg = "Could not resolve hostname"; + + std::string current_family_str = current_family == AF_INET ? "v4 : " : "v6 : "; + std::string last_family_str = current_family == AF_INET ? "v6 : " : "v4 : "; + + if (last_msg.empty()) + return current_family_str + current_msg; + + if (current_msg == not_resolved_msg && last_msg != not_resolved_msg) + return current_family_str + current_msg; + + if (last_msg == not_resolved_msg && current_msg != not_resolved_msg) + return last_family_str + last_msg; + + return current_family_str + current_msg + " | " + last_family_str + last_msg; +} + } // namespace torrent diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/src/tracker/tracker_worker.h new/libtorrent-0.16.22/src/tracker/tracker_worker.h --- old/libtorrent-0.16.21/src/tracker/tracker_worker.h 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/src/tracker/tracker_worker.h 2026-09-02 10:22:22.000000000 +0200 @@ -74,8 +74,10 @@ auto& callback_id() { return m_callback_id; } void remove_events(); - tracker::TrackerState& state() { return m_state; } - const tracker::TrackerState& state() const { return m_state; } + auto& state(); + const auto& state() const; + + std::string generate_error_message(int current_family, const std::string& current_msg, const std::string& last_msg); // Do not lock when calling these functions/slots: // @@ -143,6 +145,9 @@ m_tracker_id = id; } +inline auto& TrackerWorker::state() { return m_state; } +inline const auto& TrackerWorker::state() const { return m_state; } + } // namespace torrent #endif // LIBTORRENT_TRACKER_TRACKER_WORKER_H diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/test/Makefile.am new/libtorrent-0.16.22/test/Makefile.am --- old/libtorrent-0.16.21/test/Makefile.am 2026-08-26 09:17:41.000000000 +0200 +++ new/libtorrent-0.16.22/test/Makefile.am 2026-09-02 10:22:22.000000000 +0200 @@ -83,6 +83,8 @@ torrent/utils/test_uri_parser.h LibTorrent_Test_Torrent_SOURCES = $(LibTorrent_Test_Common) \ + torrent/test_dht_controller.cc \ + torrent/test_dht_controller.h \ torrent/object_test.cc \ torrent/object_test.h \ torrent/object_test_utils.cc \ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/test/Makefile.in new/libtorrent-0.16.22/test/Makefile.in --- old/libtorrent-0.16.21/test/Makefile.in 2026-08-26 09:17:58.000000000 +0200 +++ new/libtorrent-0.16.22/test/Makefile.in 2026-09-02 10:22:35.000000000 +0200 @@ -195,6 +195,7 @@ helpers/LibTorrent_Test_Torrent-test_thread.$(OBJEXT) \ helpers/LibTorrent_Test_Torrent-tracker_test.$(OBJEXT) am_LibTorrent_Test_Torrent_OBJECTS = $(am__objects_4) \ + torrent/LibTorrent_Test_Torrent-test_dht_controller.$(OBJEXT) \ torrent/LibTorrent_Test_Torrent-object_test.$(OBJEXT) \ torrent/LibTorrent_Test_Torrent-object_test_utils.$(OBJEXT) \ torrent/LibTorrent_Test_Torrent-object_static_map_test.$(OBJEXT) \ @@ -391,6 +392,7 @@ torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_stream_test.Po \ torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_test.Po \ torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_test_utils.Po \ + torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_dht_controller.Po \ torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_tracker_controller.Po \ torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_tracker_controller_features.Po \ torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_tracker_controller_requesting.Po \ @@ -731,6 +733,8 @@ torrent/utils/test_uri_parser.h LibTorrent_Test_Torrent_SOURCES = $(LibTorrent_Test_Common) \ + torrent/test_dht_controller.cc \ + torrent/test_dht_controller.h \ torrent/object_test.cc \ torrent/object_test.h \ torrent/object_test_utils.cc \ @@ -954,6 +958,8 @@ torrent/$(DEPDIR)/$(am__dirstamp): @$(MKDIR_P) torrent/$(DEPDIR) @: >>torrent/$(DEPDIR)/$(am__dirstamp) +torrent/LibTorrent_Test_Torrent-test_dht_controller.$(OBJEXT): \ + torrent/$(am__dirstamp) torrent/$(DEPDIR)/$(am__dirstamp) torrent/LibTorrent_Test_Torrent-object_test.$(OBJEXT): \ torrent/$(am__dirstamp) torrent/$(DEPDIR)/$(am__dirstamp) torrent/LibTorrent_Test_Torrent-object_test_utils.$(OBJEXT): \ @@ -1219,6 +1225,7 @@ @AMDEP_TRUE@@am__include@ @am__quote@torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_stream_test.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_test.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_test_utils.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_dht_controller.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_tracker_controller.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_tracker_controller_features.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_tracker_controller_requesting.Po@am__quote@ # am--include-marker @@ -1857,6 +1864,20 @@ @AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ @am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(LibTorrent_Test_Torrent_CXXFLAGS) $(CXXFLAGS) -c -o helpers/LibTorrent_Test_Torrent-tracker_test.obj `if test -f 'helpers/tracker_test.cc'; then $(CYGPATH_W) 'helpers/tracker_test.cc'; else $(CYGPATH_W) '$(srcdir)/helpers/tracker_test.cc'; fi` +torrent/LibTorrent_Test_Torrent-test_dht_controller.o: torrent/test_dht_controller.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(LibTorrent_Test_Torrent_CXXFLAGS) $(CXXFLAGS) -MT torrent/LibTorrent_Test_Torrent-test_dht_controller.o -MD -MP -MF torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_dht_controller.Tpo -c -o torrent/LibTorrent_Test_Torrent-test_dht_controller.o `test -f 'torrent/test_dht_controller.cc' || echo '$(srcdir)/'`torrent/test_dht_controller.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_dht_controller.Tpo torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_dht_controller.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='torrent/test_dht_controller.cc' object='torrent/LibTorrent_Test_Torrent-test_dht_controller.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(LibTorrent_Test_Torrent_CXXFLAGS) $(CXXFLAGS) -c -o torrent/LibTorrent_Test_Torrent-test_dht_controller.o `test -f 'torrent/test_dht_controller.cc' || echo '$(srcdir)/'`torrent/test_dht_controller.cc + +torrent/LibTorrent_Test_Torrent-test_dht_controller.obj: torrent/test_dht_controller.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(LibTorrent_Test_Torrent_CXXFLAGS) $(CXXFLAGS) -MT torrent/LibTorrent_Test_Torrent-test_dht_controller.obj -MD -MP -MF torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_dht_controller.Tpo -c -o torrent/LibTorrent_Test_Torrent-test_dht_controller.obj `if test -f 'torrent/test_dht_controller.cc'; then $(CYGPATH_W) 'torrent/test_dht_controller.cc'; else $(CYGPATH_W) '$(srcdir)/torrent/test_dht_controller.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_dht_controller.Tpo torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_dht_controller.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='torrent/test_dht_controller.cc' object='torrent/LibTorrent_Test_Torrent-test_dht_controller.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(LibTorrent_Test_Torrent_CXXFLAGS) $(CXXFLAGS) -c -o torrent/LibTorrent_Test_Torrent-test_dht_controller.obj `if test -f 'torrent/test_dht_controller.cc'; then $(CYGPATH_W) 'torrent/test_dht_controller.cc'; else $(CYGPATH_W) '$(srcdir)/torrent/test_dht_controller.cc'; fi` + torrent/LibTorrent_Test_Torrent-object_test.o: torrent/object_test.cc @am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(LibTorrent_Test_Torrent_CXXFLAGS) $(CXXFLAGS) -MT torrent/LibTorrent_Test_Torrent-object_test.o -MD -MP -MF torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_test.Tpo -c -o torrent/LibTorrent_Test_Torrent-object_test.o `test -f 'torrent/object_test.cc' || echo '$(srcdir)/'`torrent/object_test.cc @am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_test.Tpo torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_test.Po @@ -3010,6 +3031,7 @@ -rm -f torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_stream_test.Po -rm -f torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_test.Po -rm -f torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_test_utils.Po + -rm -f torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_dht_controller.Po -rm -f torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_tracker_controller.Po -rm -f torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_tracker_controller_features.Po -rm -f torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_tracker_controller_requesting.Po @@ -3156,6 +3178,7 @@ -rm -f torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_stream_test.Po -rm -f torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_test.Po -rm -f torrent/$(DEPDIR)/LibTorrent_Test_Torrent-object_test_utils.Po + -rm -f torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_dht_controller.Po -rm -f torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_tracker_controller.Po -rm -f torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_tracker_controller_features.Po -rm -f torrent/$(DEPDIR)/LibTorrent_Test_Torrent-test_tracker_controller_requesting.Po diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/test/torrent/object_stream_test.cc new/libtorrent-0.16.22/test/torrent/object_stream_test.cc --- old/libtorrent-0.16.21/test/torrent/object_stream_test.cc 2026-08-26 09:17:42.000000000 +0200 +++ new/libtorrent-0.16.22/test/torrent/object_stream_test.cc 2026-09-02 10:22:22.000000000 +0200 @@ -273,4 +273,15 @@ CPPUNIT_ASSERT(read_value_rejected("i-9223372036854775809e")); CPPUNIT_ASSERT(read_value_rejected("i18446744073709551615e")); CPPUNIT_ASSERT(read_value_rejected("i99999999999999999999e")); + + CPPUNIT_ASSERT(read_value_ok("i0e", 0)); + CPPUNIT_ASSERT(read_value_ok("i-1e", -1)); + + CPPUNIT_ASSERT(read_value_rejected("i01e")); + CPPUNIT_ASSERT(read_value_rejected("i00e")); + CPPUNIT_ASSERT(read_value_rejected("i000e")); + CPPUNIT_ASSERT(read_value_rejected("i-0e")); + CPPUNIT_ASSERT(read_value_rejected("i-00e")); + CPPUNIT_ASSERT(read_value_rejected("i-01e")); + CPPUNIT_ASSERT(read_value_rejected("i-000123e")); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/test/torrent/test_dht_controller.cc new/libtorrent-0.16.22/test/torrent/test_dht_controller.cc --- old/libtorrent-0.16.21/test/torrent/test_dht_controller.cc 1970-01-01 01:00:00.000000000 +0100 +++ new/libtorrent-0.16.22/test/torrent/test_dht_controller.cc 2026-09-02 10:22:22.000000000 +0200 @@ -0,0 +1,99 @@ +#include "config.h" + +#include "test_dht_controller.h" + +#include "torrent/hash_string.h" +#include "torrent/net/socket_address.h" +#include "torrent/object.h" +#include "torrent/runtime/network_config.h" +#include "torrent/runtime/network_manager.h" +#include "torrent/runtime/socket_manager.h" +#include "torrent/tracker/dht_controller.h" + +CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(test_dht_controller, "torrent"); + +namespace { + +constexpr uint16_t dht_port = 43821; +constexpr unsigned int bootstrap_complete_nodes = 32; + +torrent::Object +create_dht_cache(unsigned int node_count) { + torrent::HashString self_id; + self_id.clear(0x55); + + auto cache = torrent::Object::create_map(); + cache.insert_key("self_id", self_id.str()); + + auto& nodes = cache.insert_key("nodes", torrent::Object::create_map()); + + for (unsigned int i = 0; i < node_count; i++) { + torrent::HashString node_id = self_id; + node_id.data()[i / 8] ^= 0x80 >> (i % 8); + + auto& node = nodes.insert_key(node_id.str(), torrent::Object::create_map()); + + node.insert_key("i", int64_t{0x7f000001}); + node.insert_key("p", int64_t{10000 + i}); + node.insert_key("t", int64_t{0}); + } + + return cache; +} + +unsigned int +add_peer_node_and_count_queries() { + auto dht = torrent::runtime::network_manager()->dht_controller(); + + auto queries_sent = dht->get_statistics().queries_sent; + auto sa = torrent::sa_make_inet_h(0x7f000002, 0); + + torrent::runtime::network_manager()->dht_add_peer_node(sa.get(), 6881); + + return dht->get_statistics().queries_sent - queries_sent; +} + +} // namespace + +void +test_dht_controller::setUp() { + TestFixtureWithMainNetTrackerThread::setUp(); + + torrent::runtime::socket_manager()->set_max_size_and_adjust(1024); + torrent::runtime::network_config()->set_override_dht_port(dht_port); +} + +void +test_dht_controller::tearDown() { + torrent::runtime::network_manager()->dht_controller()->stop(); + + TestFixtureWithMainNetTrackerThread::tearDown(); +} + +void +test_dht_controller::test_add_peer_node_while_bootstrapping() { + auto dht = torrent::runtime::network_manager()->dht_controller(); + + CPPUNIT_ASSERT(!dht->is_nodes_populated()); + + dht->initialize(create_dht_cache(0)); + + CPPUNIT_ASSERT(dht->start()); + CPPUNIT_ASSERT_EQUAL(0u, dht->get_statistics().num_nodes); + CPPUNIT_ASSERT(!dht->is_nodes_populated()); + + CPPUNIT_ASSERT_EQUAL(1u, add_peer_node_and_count_queries()); +} + +void +test_dht_controller::test_add_peer_node_after_bootstrap() { + auto dht = torrent::runtime::network_manager()->dht_controller(); + + dht->initialize(create_dht_cache(bootstrap_complete_nodes)); + + CPPUNIT_ASSERT(dht->start()); + CPPUNIT_ASSERT_EQUAL(bootstrap_complete_nodes, dht->get_statistics().num_nodes); + CPPUNIT_ASSERT(dht->is_nodes_populated()); + + CPPUNIT_ASSERT_EQUAL(0u, add_peer_node_and_count_queries()); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/test/torrent/test_dht_controller.h new/libtorrent-0.16.22/test/torrent/test_dht_controller.h --- old/libtorrent-0.16.21/test/torrent/test_dht_controller.h 1970-01-01 01:00:00.000000000 +0100 +++ new/libtorrent-0.16.22/test/torrent/test_dht_controller.h 2026-09-02 10:22:22.000000000 +0200 @@ -0,0 +1,15 @@ +#include "helpers/test_main_thread.h" + +class test_dht_controller : public TestFixtureWithMainNetTrackerThread { + CPPUNIT_TEST_SUITE(test_dht_controller); + CPPUNIT_TEST(test_add_peer_node_while_bootstrapping); + CPPUNIT_TEST(test_add_peer_node_after_bootstrap); + CPPUNIT_TEST_SUITE_END(); + +public: + void setUp() override; + void tearDown() override; + + void test_add_peer_node_while_bootstrapping(); + void test_add_peer_node_after_bootstrap(); +}; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/test/torrent/test_tracker_controller.cc new/libtorrent-0.16.22/test/torrent/test_tracker_controller.cc --- old/libtorrent-0.16.21/test/torrent/test_tracker_controller.cc 2026-08-26 09:17:42.000000000 +0200 +++ new/libtorrent-0.16.22/test/torrent/test_tracker_controller.cc 2026-09-02 10:22:22.000000000 +0200 @@ -708,6 +708,49 @@ CPPUNIT_ASSERT(tracker_0_0.state().latest_new_peers() == 20); } +// Regression test for the stale-announce bug: +// If an announce succeeds while a scrape is still active on another tracker, the +// controller must still schedule the next announce timeout. Before the fix it +// used has_active() which counted scrapes as active, so it skipped scheduling +// and the next announce was never started once the scrape finished. +void +TestTrackerController::test_success_with_active_scrape_reschedules_timeout() { + TEST_MULTI3_BEGIN(); + + auto tracker_0_0_worker = TrackerTest::test_worker(tracker_0_0); + auto tracker_1_0_worker = TrackerTest::test_worker(tracker_1_0); + + // Send an announce on tracker_0_0 and a scrape on tracker_1_0. Use the + // worker directly for the scrape so the test does not depend on cached time + // or scrape intervals. + tracker_controller.send_update_event(); + + tracker_1_0_worker->set_scrapable(); + tracker_1_0_worker->send_scrape(torrent::tracker::TrackerParams{}); + + std::this_thread::sleep_for(100ms); + + CPPUNIT_ASSERT(tracker_0_0.is_requesting()); + CPPUNIT_ASSERT(tracker_1_0.is_requesting()); + CPPUNIT_ASSERT(!tracker_1_0.is_requesting_not_scrape()); + + // Announce succeeds while the scrape is still in progress. + CPPUNIT_ASSERT(tracker_0_0_worker->trigger_success()); + + // The next announce timeout must be queued despite the active scrape. + // trigger_success() resets the tracker's normal interval to the default, + // so just verify the timeout is set and non-zero. + CPPUNIT_ASSERT(tracker_controller.is_timeout_queued()); + CPPUNIT_ASSERT(tracker_controller.seconds_to_next_timeout() > 0); + CPPUNIT_ASSERT(tracker_controller.seconds_to_next_timeout() <= + tracker_0_0.state().normal_interval().count() + 1); + + // Let the scrape finish. + CPPUNIT_ASSERT(tracker_1_0_worker->trigger_scrape()); + + TEST_MULTIPLE_END(1, 0); +} + // Add new function for finding the first tracker that will time out, // e.g. both with failure mode and normal rerequesting. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libtorrent-0.16.21/test/torrent/test_tracker_controller.h new/libtorrent-0.16.22/test/torrent/test_tracker_controller.h --- old/libtorrent-0.16.21/test/torrent/test_tracker_controller.h 2026-08-26 09:17:42.000000000 +0200 +++ new/libtorrent-0.16.22/test/torrent/test_tracker_controller.h 2026-09-02 10:22:22.000000000 +0200 @@ -31,6 +31,7 @@ CPPUNIT_TEST(test_timeout_lacking_usable); CPPUNIT_TEST(test_disable_tracker); CPPUNIT_TEST(test_new_peers); + CPPUNIT_TEST(test_success_with_active_scrape_reschedules_timeout); CPPUNIT_TEST_SUITE_END(); @@ -63,6 +64,12 @@ void test_timeout_lacking_usable(); void test_disable_tracker(); void test_new_peers(); + + // Regression: an announce success while another tracker is scraping must still + // schedule the next announce. Before the fix, the controller waited for *all* + // activity (including scrapes) to finish, then receive_scrape() never + // rescheduled the announce timeout. + void test_success_with_active_scrape_reschedules_timeout(); }; #define TRACKER_CONTROLLER_SETUP() \
