Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ set(libtorrent_include_files
mmap_disk_io.hpp
natpmp.hpp
operations.hpp
path_sanitize_flags.hpp
peer_class.hpp
peer_class_type_filter.hpp
peer_connection_handle.hpp
Expand Down
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
2.2.0 not released

* add versioned path-sanitization
* reject invalid controlURL from UPnP router
* deprecate file_storage::file_absolute_path()
* avoid copying file path strings out of the info-section
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ HEADERS = \
mmap_disk_io.hpp \
natpmp.hpp \
operations.hpp \
path_sanitize_flags.hpp \
peer_class.hpp \
peer_class_type_filter.hpp \
peer_connection_handle.hpp \
Expand Down Expand Up @@ -980,6 +981,7 @@ TEST_SOURCES = \
test_resolve_links.cpp \
test_resume.cpp \
test_rtc.cpp \
test_sanitizer.cpp \
test_session.cpp \
test_session_params.cpp \
test_settings_pack.cpp \
Expand Down Expand Up @@ -1110,6 +1112,7 @@ TEST_TORRENTS = \
pad_file_symlink.torrent \
parent_path.torrent \
sample.torrent \
sanitize_limits.torrent \
similar.torrent \
similar2.torrent \
single_multi_file.torrent \
Expand Down
16 changes: 16 additions & 0 deletions bindings/python/libtorrent/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ class add_torrent_params(metaclass=_BoostBaseClass):
renamed_files: dict[int, str]
resume_data: list[str]
root_certificate: str
sanitize_flags: int
save_path: str
seeding_time: int
storage_mode: int
Expand Down Expand Up @@ -2216,6 +2217,20 @@ class options_t(metaclass=_BoostBaseClass):

class oversized_file_alert(torrent_alert): ...

class path_sanitize_flags(metaclass=_BoostBaseClass):
__instance_size__: int
all: int
default_flags: int
filter_dos_reserved_names: int
filter_unicode_formatting_chars: int
libtorrent_2_0: int
libtorrent_2_1: int
libtorrent_2_2: int
limit_unicode_characters: int
sanitize_invalid_chars_android: int
sanitize_invalid_chars_win: int
trim_trailing_spaces_and_dots: int

class pause_flags_t(metaclass=_BoostBaseClass):
__instance_size__: int
graceful_pause: int
Expand Down Expand Up @@ -4756,6 +4771,7 @@ class load_torrent_limits(dict):
max_pieces: Literal[0x200000]
max_decode_depth: Literal[100]
max_decode_tokens: Literal[3000000]
sanitize_flags: int

class tracker_source(int):
source_client: int
Expand Down
3 changes: 3 additions & 0 deletions bindings/python/src/converters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "libtorrent/storage_defs.hpp"
#include "libtorrent/kademlia/announce_flags.hpp"
#include "libtorrent/write_resume_data.hpp"
#include "libtorrent/path_sanitize_flags.hpp"
#include <vector>
#include <map>

Expand Down Expand Up @@ -446,6 +447,7 @@ void bind_converters()
to_python_converter<lt::file_progress_flags_t, from_bitfield_flag<lt::file_progress_flags_t>>();
to_python_converter<lt::write_torrent_flags_t, from_bitfield_flag<lt::write_torrent_flags_t>>();
to_python_converter<lt::picker_flags_t, from_bitfield_flag<lt::picker_flags_t>>();
to_python_converter<lt::path_sanitize_flags_t, from_bitfield_flag<lt::path_sanitize_flags_t>>();
to_python_converter<lt::string_view, from_string_view>();

// work-around types
Expand Down Expand Up @@ -559,4 +561,5 @@ void bind_converters()
to_bitfield_flag<lt::file_progress_flags_t>();
to_bitfield_flag<lt::write_torrent_flags_t>();
to_bitfield_flag<lt::picker_flags_t>();
to_bitfield_flag<lt::path_sanitize_flags_t>();
}
23 changes: 22 additions & 1 deletion bindings/python/src/session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,8 @@ struct dummy17
{};
struct dummy_announce_flags
{};
struct dummy_path_sanitize_flags
{};

void bind_session()
{
Expand Down Expand Up @@ -1062,7 +1064,8 @@ void bind_session()
.add_property("comment", PROP(&add_torrent_params::comment))
.add_property("created_by", PROP(&add_torrent_params::created_by))
.add_property("creation_date", PROP(&add_torrent_params::creation_date))
.add_property("root_certificate", PROP(&add_torrent_params::root_certificate));
.add_property("root_certificate", PROP(&add_torrent_params::root_certificate))
.add_property("sanitize_flags", PROP(&add_torrent_params::sanitize_flags));

#ifndef TORRENT_DISABLE_DHT
class_<lt::dht::dht_state>("dht_state")
Expand Down Expand Up @@ -1168,6 +1171,24 @@ void bind_session()
#endif
;

{
scope s = class_<dummy_path_sanitize_flags>("path_sanitize_flags");
s.attr("limit_unicode_characters") = lt::path_sanitize_flags::limit_unicode_characters;
s.attr("trim_trailing_spaces_and_dots") =
lt::path_sanitize_flags::trim_trailing_spaces_and_dots;
s.attr("filter_dos_reserved_names") = lt::path_sanitize_flags::filter_dos_reserved_names;
s.attr("sanitize_invalid_chars_win") = lt::path_sanitize_flags::sanitize_invalid_chars_win;
s.attr("sanitize_invalid_chars_android") =
lt::path_sanitize_flags::sanitize_invalid_chars_android;
s.attr("filter_unicode_formatting_chars") =
lt::path_sanitize_flags::filter_unicode_formatting_chars;
s.attr("libtorrent_2_0") = lt::path_sanitize_flags::libtorrent_2_0;
s.attr("libtorrent_2_1") = lt::path_sanitize_flags::libtorrent_2_1;
s.attr("libtorrent_2_2") = lt::path_sanitize_flags::libtorrent_2_2;
s.attr("default_flags") = lt::path_sanitize_flags::default_flags;
s.attr("all") = lt::path_sanitize_flags::all;
}

enum_<lt::portmap_protocol>("portmap_protocol")
.value("none", lt::portmap_protocol::none)
.value("udp", lt::portmap_protocol::udp)
Expand Down
5 changes: 5 additions & 0 deletions bindings/python/src/torrent_info.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,11 @@ load_torrent_limits dict_to_limits(dict limits)
ret.max_decode_tokens = extract<int>(value);
continue;
}
else if (key == "sanitize_flags")
{
ret.sanitize_flags = extract<lt::path_sanitize_flags_t>(value);
continue;
}
}
return ret;
}
Expand Down
24 changes: 24 additions & 0 deletions bindings/python/tests/torrent_info_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,30 @@ def test_load_max_buffer_size_limit(self) -> None:
load_torrent_limits({"max_buffer_size": 1}),
)

def test_load_sanitize_flags_limit(self) -> None:
# name embeds a zero-width space (U+200B), which
# filter_unicode_formatting_chars strips out (it's part of
# default_flags), but an empty sanitize_flags value leaves alone.
name = "foo\u200bbar.txt"
entry = TorrentFileDict(
{
b"info": {
b"name": name.encode(),
b"piece length": 16 * 1024,
b"pieces": lib.get_random_bytes(20),
b"length": 1024,
}
}
)

ti_default = lt.torrent_info(entry)
self.assertEqual(ti_default.name(), "foobar.txt")

ti_unfiltered = lt.torrent_info(
entry, load_torrent_limits({"sanitize_flags": 0})
)
self.assertEqual(ti_unfiltered.name(), name)


class FieldTest(unittest.TestCase):
def setUp(self) -> None:
Expand Down
11 changes: 11 additions & 0 deletions docs/hunspell/libtorrent.dic
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ Tribler
gzipped
processes'
versioning
versioned
cstdint
cloneable
inline
Expand Down Expand Up @@ -677,3 +678,13 @@ sl
hasher256
ffff
RFC1918
com1
lpt1
ZWSP
ZWNJ
ZWJ
bidi
LRM
RLM
sanitization
ruleset
43 changes: 43 additions & 0 deletions docs/upgrade_to_2.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,46 @@ and ``create_file_entry`` instead, see `Creating torrents`_ in the 2.1
upgrade guide.

.. _`Creating torrents`: upgrade_to_2.1-ref.html#creating-torrents

configurable filename sanitization
==================================

The rules for sanitizing filenames found in a torrent's info-dict are now
controlled by ``path_sanitize_flags_t``, exposed via
``add_torrent_params::sanitize_flags`` and
``load_torrent_limits::sanitize_flags``. It defaults to
``path_sanitize_flags::default_flags``, which now also filters DOS/Windows
reserved device names (e.g. ``con``, ``com1``) on Windows, via the new
``path_sanitize_flags::filter_dos_reserved_names`` bit. This also includes
``path_sanitize_flags::filter_unicode_formatting_chars``, which makes the
filtering of unicode formatting characters (introduced in libtorrent 2.1)
optional.

The ruleset that was actually in effect in earlier releases is preserved
under versioned names, ``path_sanitize_flags::libtorrent_2_0`` and
``path_sanitize_flags::libtorrent_2_1``, matching the naming of the new
``path_sanitize_flags::libtorrent_2_2`` (equivalent to ``default_flags``).
Resume data written before this feature existed is interpreted using
``libtorrent_2_0`` or ``libtorrent_2_1``, based on the ``libtorrent-version``
recorded in it, rather than ``default_flags``, so an upgrade never silently
re-sanitizes an existing torrent's files under different rules than the
ones used to create them on disk.

Changing this ruleset for a torrent that already has files on disk can make
libtorrent look for them under different names than the ones actually
there. The ordinary resume data round trip pins and restores it
automatically; only clients overriding ``sanitize_flags`` themselves, or
reconstructing ``add_torrent_params`` from a .torrent file rather than
resume data, need to take care.

The deprecated ``torrent_info`` constructors that take no
``load_torrent_limits`` default to ``libtorrent_2_1`` rather than
``default_flags``, since callers still using them cannot supply a ruleset
and are most likely dealing with pre-existing torrents.

Clients that cache a torrent's info-dict separately from its resume data
(e.g. a stored .torrent file re-parsed on startup, independently of the
fast-resume file) must propagate the ``sanitize_flags`` recovered by
``read_resume_data()`` into the ``load_torrent_limits`` used for that
separate re-parse themselves; libtorrent has no way to reconcile the two
once they come from different calls.
34 changes: 34 additions & 0 deletions fuzzers/src/path_sanitize_rulesets.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*

Copyright (c) 2026, Arvid Norberg
All rights reserved.

You may use, distribute and modify this code under the terms of the BSD license,
see LICENSE file.
*/

#ifndef TORRENT_FUZZERS_PATH_SANITIZE_RULESETS_HPP_INCLUDED
#define TORRENT_FUZZERS_PATH_SANITIZE_RULESETS_HPP_INCLUDED

#include "libtorrent/path_sanitize_flags.hpp"

#include <array>

namespace fuzzers {

// the rulesets a real caller can actually end up with: unpinned
// (default_flags, same as libtorrent_2_2), one of the historical
// per-version rulesets a resume-data upgrade might pin, or the two
// extremes. Not every bit combination, since the path itself, not the
// flag combination, is the interesting search space.
constexpr std::array<lt::path_sanitize_flags_t, 5> path_sanitize_rulesets{{
lt::path_sanitize_flags_t{},
lt::path_sanitize_flags::all,
lt::path_sanitize_flags::libtorrent_2_0,
lt::path_sanitize_flags::libtorrent_2_1,
lt::path_sanitize_flags::libtorrent_2_2,
}};

}

#endif
11 changes: 9 additions & 2 deletions fuzzers/src/sanitize_path.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ see LICENSE file.
*/

#include "libtorrent/torrent_info.hpp"
#include "libtorrent/path_sanitize_flags.hpp"
#include "path_sanitize_rulesets.hpp"

extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size)
{
std::string out;
lt::aux::sanitize_path_element(out, {reinterpret_cast<char const*>(data), size});
lt::string_view const element(reinterpret_cast<char const*>(data), size);

for (lt::path_sanitize_flags_t const flags : fuzzers::path_sanitize_rulesets)
{
std::string out;
lt::aux::sanitize_path_element(out, element, flags);
}
return 0;
}

17 changes: 11 additions & 6 deletions fuzzers/src/torrent_info.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@ see LICENSE file.
*/

#include "libtorrent/load_torrent.hpp"
#include "path_sanitize_rulesets.hpp"

extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size)
{
try
{
lt::add_torrent_params atp = lt::load_torrent_buffer({reinterpret_cast<char const*>(data), int(size)});
}
catch (lt::system_error const&)
lt::span<char const> const buf{reinterpret_cast<char const*>(data), int(size)};

// exercise the rulesets a real caller can actually end up with,
// against the same input in one pass, rather than relying on
// separate corpora to ever reach the non-default ones
for (auto const flags : fuzzers::path_sanitize_rulesets)
{
return 0;
lt::load_torrent_limits cfg;
cfg.sanitize_flags = flags;
lt::error_code ec;
lt::add_torrent_params atp = lt::load_torrent_buffer(buf, ec, cfg);
}
return 0;
}
Expand Down
15 changes: 15 additions & 0 deletions include/libtorrent/add_torrent_params.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ see LICENSE file.
#include "libtorrent/error_code.hpp"
#include "libtorrent/units.hpp"
#include "libtorrent/torrent_flags.hpp"
#include "libtorrent/path_sanitize_flags.hpp"
#include "libtorrent/info_hash.hpp"
#include "libtorrent/download_priority.hpp"
#include "libtorrent/client_data.hpp"
Expand Down Expand Up @@ -356,6 +357,20 @@ TORRENT_VERSION_NAMESPACE_3
// applied before the torrent is added.
aux::noexcept_movable<std::map<file_index_t, std::string>> renamed_files;

// the path-sanitization ruleset used to build this torrent's file
// layout on disk. This defaults to
// the newest ruleset known to this version of libtorrent
// (``path_sanitize_flags::default_flags``), but once a torrent has
// been added, its actual on-disk file layout depends on whichever
// ruleset was in effect at the time. Changing this value for a
// torrent that has already downloaded files can make libtorrent look
// for those files under different names than the ones actually on
// disk. ``load_torrent_*()`` and ``read_resume_data()`` both set this
// field to whatever ruleset they actually used, so round-tripping an
// ``add_torrent_params`` object through resume data preserves it
// automatically. It cannot be changed after the torrent is added.
path_sanitize_flags_t sanitize_flags = path_sanitize_flags::default_flags;

// the posix time of the last time payload was received or sent for this
// torrent, respectively. A value of 0 means we don't know when we last
// uploaded or downloaded, or we have never uploaded or downloaded any
Expand Down
8 changes: 8 additions & 0 deletions include/libtorrent/aux_/torrent.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,14 @@ namespace libtorrent::aux {
// the internal bits.
torrent_flags_t m_flags{};

// the path-sanitization ruleset pinned for this torrent at
// add-time. See
// add_torrent_params::sanitize_flags. Used to parse the info-dict
// into a file layout whenever that happens after add() (e.g. when
// metadata arrives for a magnet link), so a torrent keeps resolving
// its files the same way regardless of later library upgrades.
path_sanitize_flags_t m_sanitize_flags = path_sanitize_flags::default_flags;

// helper: set or clear `bit` in m_flags based on `value`.
void set_flag(torrent_flags_t const bit, bool const value)
{
Expand Down
1 change: 1 addition & 0 deletions include/libtorrent/libtorrent.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
#include "libtorrent/mmap_disk_io.hpp"
#include "libtorrent/natpmp.hpp"
#include "libtorrent/operations.hpp"
#include "libtorrent/path_sanitize_flags.hpp"
#include "libtorrent/peer_class.hpp"
#include "libtorrent/peer_class_type_filter.hpp"
#include "libtorrent/peer_connection_handle.hpp"
Expand Down
Loading
Loading