update symlinks to conform to BEP 47

This commit is contained in:
Arvid Norberg 2019-03-14 10:02:41 +01:00 committed by Arvid Norberg
parent 5f85e40193
commit 57cd2882d4
13 changed files with 228 additions and 14 deletions

View File

@ -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

View File

@ -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);

View File

@ -37,6 +37,7 @@ POSSIBILITY OF SUCH DAMAGE.
#include <string>
#include <vector>
#include <unordered_set>
#include <unordered_map>
#include <ctime>
#include <cstdint>
@ -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<std::string, file_index_t> m_symlinks;
std::vector<std::string> m_symlinks;
// the modification times of each file. This vector
// is empty if no file have a modification time.

View File

@ -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

View File

@ -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<std::string, file_index_t> 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<piece_index_t, piece_index_t>

View File

@ -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<int>(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

View File

@ -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<std::mutex> l(m_mutex);
TORRENT_ASSERT(i < fs.end_file());
if (i >= m_stat_cache.end_index()) m_stat_cache.resize(static_cast<int>(i) + 1

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
{

View File

@ -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

View File

@ -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