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()                                      \

Reply via email to