diff --git a/ChangeLog b/ChangeLog index 5ff6756e3..57b982b12 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,4 @@ + * update symlinks to conform to BEP 47 * fix python bindings for peer_info * support creating symlinks, for torrents with symlinks in them * fix error in seed_mode flag diff --git a/include/libtorrent/aux_/path.hpp b/include/libtorrent/aux_/path.hpp index fef71c68b..67326b2a2 100644 --- a/include/libtorrent/aux_/path.hpp +++ b/include/libtorrent/aux_/path.hpp @@ -156,6 +156,8 @@ namespace libtorrent { , string_view rhs); TORRENT_EXTRA_EXPORT void append_path(std::string& branch , string_view leaf); + TORRENT_EXTRA_EXPORT std::string lexically_relative(string_view base + , string_view target); // internal used by create_torrent.hpp TORRENT_EXTRA_EXPORT std::string complete(string_view f); diff --git a/include/libtorrent/file_storage.hpp b/include/libtorrent/file_storage.hpp index 75097db76..d3d87e89a 100644 --- a/include/libtorrent/file_storage.hpp +++ b/include/libtorrent/file_storage.hpp @@ -37,6 +37,7 @@ POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include #include @@ -527,6 +528,8 @@ namespace libtorrent { // offset to add to any pointers to make them point into the new buffer void apply_pointer_offset(std::ptrdiff_t off); + void sanitize_symlinks(); + private: file_index_t last_file() const noexcept; @@ -566,7 +569,7 @@ namespace libtorrent { // for files that are symlinks, the symlink // path_index in the internal_file_entry indexes // this vector of strings - aux::vector m_symlinks; + std::vector m_symlinks; // the modification times of each file. This vector // is empty if no file have a modification time. diff --git a/src/create_torrent.cpp b/src/create_torrent.cpp index 334f1c166..c20fdb72a 100644 --- a/src/create_torrent.cpp +++ b/src/create_torrent.cpp @@ -120,7 +120,7 @@ namespace { if ((file_flags & file_storage::flag_symlink) && (flags & create_torrent::symlinks)) { - std::string sym_path = aux::get_symlink_path(f); + std::string const sym_path = aux::get_symlink_path(f); fs.add_file(l, 0, file_flags, std::time_t(s.mtime), sym_path); } else diff --git a/src/file_storage.cpp b/src/file_storage.cpp index 514f512c7..82f4aa193 100644 --- a/src/file_storage.cpp +++ b/src/file_storage.cpp @@ -667,7 +667,7 @@ namespace { TORRENT_ASSERT_PRECOND(index >= file_index_t(0) && index < end_file()); internal_file_entry const& fe = m_files[index]; TORRENT_ASSERT(fe.symlink_index < int(m_symlinks.size())); - return m_symlinks[file_index_t(fe.symlink_index)]; + return m_symlinks[fe.symlink_index]; } std::time_t file_storage::mtime(file_index_t const index) const @@ -884,7 +884,7 @@ namespace { std::string const& file_storage::symlink(internal_file_entry const& fe) const { TORRENT_ASSERT_PRECOND(fe.symlink_index < int(m_symlinks.size())); - return m_symlinks[file_index_t(fe.symlink_index)]; + return m_symlinks[fe.symlink_index]; } std::time_t file_storage::mtime(internal_file_entry const& fe) const @@ -1116,6 +1116,83 @@ namespace { if (index != cur_index) reorder_file(index, cur_index); } + void file_storage::sanitize_symlinks() + { + // symlinks are unusual, this function is optimized assuming there are no + // symbolic links in the torrent. If we find one symbolic link, we'll + // build the hash table of files it's allowed to refer to, but don't pay + // that price up-front. + std::unordered_map file_map; + bool file_map_initialized = false; + + for (auto const i : file_range()) + { + if (!(file_flags(i) & file_storage::flag_symlink)) continue; + + if (!file_map_initialized) + { + for (auto const j : file_range()) file_map[file_path(j)] = j; + file_map_initialized = true; + } + + internal_file_entry const& fe = m_files[i]; + TORRENT_ASSERT(fe.symlink_index < int(m_symlinks.size())); + + // symlink targets are only allowed to point to files or directories in + // this torrent. + { + std::string target = symlink(i); + + // if it points to a directory, that's OK + auto it = std::find(m_paths.begin(), m_paths.end(), target); + if (it != m_paths.end()) + { + m_symlinks[fe.symlink_index] = combine_path(name(), *it); + continue; + } + + target = combine_path(name(), target); + + auto const idx = file_map.find(target); + if (idx != file_map.end()) + { + m_symlinks[fe.symlink_index] = target; + continue; + } + } + + // this symlink target points to a file that's not part of this torrent + // file structure. That's not allowed by the spec. + + // for backwards compatibility, allow paths relative to the link as + // well + if (fe.path_index >= 0) + { + std::string target = m_paths[fe.path_index]; + append_path(target, symlink(i)); + // if it points to a directory, that's OK + auto it = std::find(m_paths.begin(), m_paths.end(), target); + if (it != m_paths.end()) + { + m_symlinks[fe.symlink_index] = combine_path(name(), *it); + continue; + } + + target = combine_path(name(), target); + auto const idx = file_map.find(target); + if (idx != file_map.end()) + { + m_symlinks[fe.symlink_index] = target; + continue; + } + } + + // this symlink is invalid, make it point to itself + m_symlinks[fe.symlink_index] = file_path(i); + } + } + + namespace aux { std::tuple diff --git a/src/path.cpp b/src/path.cpp index 6f1f45e85..b450bab02 100644 --- a/src/path.cpp +++ b/src/path.cpp @@ -737,6 +737,47 @@ namespace { return ret; } + std::string lexically_relative(string_view base, string_view target) + { + // first, strip trailing directory separators + if (!base.empty() && base.back() == TORRENT_SEPARATOR_CHAR) + base.remove_suffix(1); + if (!target.empty() && target.back() == TORRENT_SEPARATOR_CHAR) + target.remove_suffix(1); + + // strip common path elements + for (;;) + { + if (base.empty()) break; + string_view prev_base = base; + string_view prev_target = target; + + string_view base_element; + string_view target_element; + std::tie(base_element, base) = split_string(base, TORRENT_SEPARATOR_CHAR); + std::tie(target_element, target) = split_string(target, TORRENT_SEPARATOR_CHAR); + if (base_element == target_element) continue; + + base = prev_base; + target = prev_target; + break; + } + + // count number of path elements left in base, and prepend that number of + // "../" to target + + // base alwaus points to a directory. There's an implied directory + // separator at the end of it + int const num_steps = static_cast(std::count( + base.begin(), base.end(), TORRENT_SEPARATOR_CHAR)) + (base.empty() ? 0 : 1); + std::string ret; + for (int i = 0; i < num_steps; ++i) + ret += ".." TORRENT_SEPARATOR; + + ret += target.to_string(); + return ret; + } + std::string current_working_directory() { #if defined TORRENT_WINDOWS diff --git a/src/stat_cache.cpp b/src/stat_cache.cpp index 88b2a5b12..cbdccb19d 100644 --- a/src/stat_cache.cpp +++ b/src/stat_cache.cpp @@ -78,6 +78,16 @@ namespace libtorrent { std::int64_t stat_cache::get_filesize(file_index_t const i, file_storage const& fs , std::string const& save_path, error_code& ec) { + // always pretend symlinks don't exist, to trigger special logic for + // creating and possibly validating them. There's a risk we'll and up in a + // cycle of references here otherwise. + // Should stat_file() be changed to use lstat()? + if (fs.file_flags(i) & file_storage::flag_symlink) + { + ec.assign(boost::system::errc::no_such_file_or_directory, boost::system::system_category()); + return 0; + } + std::lock_guard l(m_mutex); TORRENT_ASSERT(i < fs.end_file()); if (i >= m_stat_cache.end_index()) m_stat_cache.resize(static_cast(i) + 1 diff --git a/src/storage.cpp b/src/storage.cpp index 578982372..c733993b3 100644 --- a/src/storage.cpp +++ b/src/storage.cpp @@ -298,7 +298,6 @@ namespace libtorrent { break; } - // if the file is empty and doesn't already exist, create it // deliberately don't truncate files that already exist // if a file is supposed to have size 0, but already exists, we will @@ -326,13 +325,35 @@ namespace libtorrent { // create symlinks if (fs.file_flags(file_index) & file_storage::flag_symlink) { - if (::symlink(fs.symlink(file_index).c_str() - , fs.file_path(file_index, m_save_path).c_str()) != 0) + // we make the symlink target relative to the link itself + std::string const target = lexically_relative( + parent_path(fs.file_path(file_index)), fs.symlink(file_index)); + std::string const link = fs.file_path(file_index, m_save_path); + if (::symlink(target.c_str(), link.c_str()) != 0) { - ec.ec = error_code(errno, generic_category()); - ec.file(file_index); - ec.operation = operation_t::symlink; - break; + int const error = errno; + if (error == EEXIST) + { + // if the file exist, it may be a symlink already. if so, + // just verify the link target is what it's supposed to be + // note that readlink() does not null terminate the buffer + char buffer[512]; + auto const ret = ::readlink(link.c_str(), buffer, sizeof(buffer)); + if (ret <= 0 || target != string_view(buffer, std::size_t(ret))) + { + ec.ec = error_code(error, generic_category()); + ec.file(file_index); + ec.operation = operation_t::symlink; + return; + } + } + else + { + ec.ec = error_code(error, generic_category()); + ec.file(file_index); + ec.operation = operation_t::symlink; + return; + } } } else diff --git a/src/torrent_info.cpp b/src/torrent_info.cpp index 0ca2e0fc6..53f2983bc 100644 --- a/src/torrent_info.cpp +++ b/src/torrent_info.cpp @@ -488,9 +488,12 @@ namespace { sanitize_append_path_element(symlink_path, pe); } } + // symlink targets are validated later, as it may point to a file or + // directory we haven't parsed yet } else { + // technically this is an invalid torrent. "symlink path" must exist file_flags &= ~file_storage::flag_symlink; } @@ -526,6 +529,8 @@ namespace { , info_ptr_diff, false, pad_file_cnt, ec)) return false; } + // this rewrites invalid symlinks to point to themselves + target.sanitize_symlinks(); return true; } @@ -1006,6 +1011,7 @@ namespace { return false; } + files.sanitize_symlinks(); m_flags &= ~multifile; } else diff --git a/test/test_file.cpp b/test/test_file.cpp index 9eaebf12d..c6716d8a4 100644 --- a/test/test_file.cpp +++ b/test/test_file.cpp @@ -415,6 +415,46 @@ TORRENT_TEST(stat_file) TEST_EQUAL(ec, boost::system::errc::no_such_file_or_directory); } +TORRENT_TEST(relative_path) +{ +#ifdef TORRENT_WINDOWS +#define S "\\" +#else +#define S "/" +#endif + TEST_EQUAL(lexically_relative("A" S "B" S "C", "A" S "C" S "B") + , ".." S ".." S "C" S "B"); + + TEST_EQUAL(lexically_relative("A" S "B" S "C" S, "A" S "C" S "B") + , ".." S ".." S "C" S "B"); + + TEST_EQUAL(lexically_relative("A" S "B" S "C" S, "A" S "C" S "B" S) + , ".." S ".." S "C" S "B"); + + TEST_EQUAL(lexically_relative("A" S "B" S "C", "A" S "B" S "B") + , ".." S "B"); + + TEST_EQUAL(lexically_relative("A" S "B" S "C", "A" S "B" S "C") + , ""); + + TEST_EQUAL(lexically_relative("A" S "B", "A" S "B") + , ""); + + TEST_EQUAL(lexically_relative("A" S "B", "A" S "B" S "C") + , "C"); + + TEST_EQUAL(lexically_relative("A" S, "A" S) + , ""); + + TEST_EQUAL(lexically_relative("", "A" S "B" S "C") + , "A" S "B" S "C"); + + TEST_EQUAL(lexically_relative("A" S "B" S "C", "") + , ".." S ".." S ".." S); + + TEST_EQUAL(lexically_relative("", ""), ""); +} + // UNC tests #if TORRENT_USE_UNC_PATHS @@ -570,4 +610,5 @@ TORRENT_TEST(unc_paths) remove(reserved_name, ec); TEST_CHECK(!ec); } + #endif diff --git a/test/test_torrent_info.cpp b/test/test_torrent_info.cpp index c5c2da5d4..c0c6863b7 100644 --- a/test/test_torrent_info.cpp +++ b/test/test_torrent_info.cpp @@ -126,6 +126,7 @@ static test_torrent_t test_torrents[] = { "invalid_name2.torrent" }, { "invalid_name3.torrent" }, { "symlink1.torrent" }, + { "symlink2.torrent" }, { "unordered.torrent" }, { "symlink_zero_size.torrent" }, { "pad_file_no_path.torrent" }, @@ -795,6 +796,17 @@ TORRENT_TEST(parse_torrents) TEST_EQUAL(ti->name(), "foobar "); #endif } + else if (t.file == "symlink1.torrent"_sv) + { + TEST_EQUAL(ti->num_files(), 2); + TEST_EQUAL(ti->files().symlink(file_index_t{1}), "temp" SEPARATOR "a" SEPARATOR "b" SEPARATOR "bar"); + } + else if (t.file == "symlink2.torrent"_sv) + { + TEST_EQUAL(ti->num_files(), 5); + TEST_EQUAL(ti->files().symlink(file_index_t{0}), "Some.framework" SEPARATOR "Versions" SEPARATOR "A" SEPARATOR "SDL2"); + TEST_EQUAL(ti->files().symlink(file_index_t{4}), "Some.framework" SEPARATOR "Versions" SEPARATOR "A"); + } else if (t.file == "slash_path.torrent"_sv) { TEST_EQUAL(ti->num_files(), 1); @@ -813,7 +825,7 @@ TORRENT_TEST(parse_torrents) else if (t.file == "symlink_zero_size.torrent"_sv) { TEST_EQUAL(ti->num_files(), 2); - TEST_EQUAL(ti->files().symlink(file_index_t(1)), combine_path("foo", "bar")); + TEST_EQUAL(ti->files().symlink(file_index_t(1)), "temp" SEPARATOR "a" SEPARATOR "b" SEPARATOR "bar"); } else if (t.file == "pad_file_no_path.torrent"_sv) { diff --git a/test/test_torrents/symlink1.torrent b/test/test_torrents/symlink1.torrent index e41e6c2d9..ed6163210 100644 --- a/test/test_torrents/symlink1.torrent +++ b/test/test_torrents/symlink1.torrent @@ -1 +1 @@ -d10:created by10:libtorrent13:creation datei1359599503e4:infod5:filesld6:lengthi425e4:pathl1:a1:b3:bareed4:attr1:l6:lengthi425e4:pathl1:a1:b3:fooe12:symlink pathl3:foo3:bareee4:name4:temp12:piece lengthi16384e6:pieces20:‚ž¼Œ&¾ÇJW›}ÜA4u,·¼‘‡ee +d10:created by10:libtorrent13:creation datei1359599503e4:infod5:filesld6:lengthi425e4:pathl1:a1:b3:bareed4:attr1:l6:lengthi425e4:pathl1:a1:b3:fooe12:symlink pathl1:a1:b3:bareee4:name4:temp12:piece lengthi16384e6:pieces20:‚ž¼Œ&¾ÇJW›}ÜA4u,·¼‘‡ee diff --git a/test/test_torrents/symlink_zero_size.torrent b/test/test_torrents/symlink_zero_size.torrent index a7f225a0d..2b7935bd4 100644 --- a/test/test_torrents/symlink_zero_size.torrent +++ b/test/test_torrents/symlink_zero_size.torrent @@ -1 +1 @@ -d10:created by10:libtorrent13:creation datei1359599503e4:infod5:filesld6:lengthi425e4:pathl1:a1:b3:bareed4:attr1:l4:pathl1:a1:b3:fooe12:symlink pathl3:foo3:bareee4:name4:temp12:piece lengthi16384e6:pieces20:aaaaaaaaaaaaaaaaaaaaee +d10:created by10:libtorrent13:creation datei1359599503e4:infod5:filesld6:lengthi425e4:pathl1:a1:b3:bareed4:attr1:l4:pathl1:a1:b3:fooe12:symlink pathl1:a1:b3:bareee4:name4:temp12:piece lengthi16384e6:pieces20:aaaaaaaaaaaaaaaaaaaaee