mirror of
https://github.com/transmission/transmission
synced 2025-03-04 02:28:03 +00:00
refactor: tr_torrent_metainfo (#2340)
* refactor: add tr_torrent_metainfo class Can be used for parsing bencoded .torrent data without instantiating a tr_torrent. This will be used in all the places where client code needs to test a .torrent file for validity / to add a preview window before adding the torrent.
This commit is contained in:
parent
0e321c2d21
commit
7c87cb36eb
11 changed files with 1138 additions and 41 deletions
|
@ -38,6 +38,8 @@
|
||||||
4D8017EB10BBC073008A4AF2 /* torrent-magnet.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D8017E910BBC073008A4AF2 /* torrent-magnet.h */; };
|
4D8017EB10BBC073008A4AF2 /* torrent-magnet.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D8017E910BBC073008A4AF2 /* torrent-magnet.h */; };
|
||||||
4D80185910BBC0B0008A4AF2 /* magnet-metainfo.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4D80185710BBC0B0008A4AF2 /* magnet-metainfo.cc */; };
|
4D80185910BBC0B0008A4AF2 /* magnet-metainfo.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4D80185710BBC0B0008A4AF2 /* magnet-metainfo.cc */; };
|
||||||
4D80185A10BBC0B0008A4AF2 /* magnet-metainfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D80185810BBC0B0008A4AF2 /* magnet-metainfo.h */; };
|
4D80185A10BBC0B0008A4AF2 /* magnet-metainfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D80185810BBC0B0008A4AF2 /* magnet-metainfo.h */; };
|
||||||
|
0A89346B736DBCF81F3A4850 /* torrent-metainfo.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0A89346B736DBCF81F3A4851 /* torrent-metainfo.cc */; };
|
||||||
|
0A89346B736DBCF81F3A4852 /* torrent-metainfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A89346B736DBCF81F3A4853 /* torrent-metainfo.h */; };
|
||||||
4D9A2BF009E16D21002D0FF9 /* libtransmission.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D18389709DEC0030047D688 /* libtransmission.a */; };
|
4D9A2BF009E16D21002D0FF9 /* libtransmission.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D18389709DEC0030047D688 /* libtransmission.a */; };
|
||||||
4DB74F080E8CD75100AEB1A8 /* wildmat.c in Sources */ = {isa = PBXBuildFile; fileRef = 4DB74F070E8CD75100AEB1A8 /* wildmat.c */; };
|
4DB74F080E8CD75100AEB1A8 /* wildmat.c in Sources */ = {isa = PBXBuildFile; fileRef = 4DB74F070E8CD75100AEB1A8 /* wildmat.c */; };
|
||||||
4DCCBB3E09C3D71100D3CABF /* TorrentCell.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4DCCBB3C09C3D71100D3CABF /* TorrentCell.mm */; };
|
4DCCBB3E09C3D71100D3CABF /* TorrentCell.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4DCCBB3C09C3D71100D3CABF /* TorrentCell.mm */; };
|
||||||
|
@ -530,6 +532,8 @@
|
||||||
4D8017E910BBC073008A4AF2 /* torrent-magnet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "torrent-magnet.h"; sourceTree = "<group>"; };
|
4D8017E910BBC073008A4AF2 /* torrent-magnet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "torrent-magnet.h"; sourceTree = "<group>"; };
|
||||||
4D80185710BBC0B0008A4AF2 /* magnet-metainfo.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "magnet-metainfo.cc"; sourceTree = "<group>"; };
|
4D80185710BBC0B0008A4AF2 /* magnet-metainfo.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "magnet-metainfo.cc"; sourceTree = "<group>"; };
|
||||||
4D80185810BBC0B0008A4AF2 /* magnet-metainfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "magnet-metainfo.h"; sourceTree = "<group>"; };
|
4D80185810BBC0B0008A4AF2 /* magnet-metainfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "magnet-metainfo.h"; sourceTree = "<group>"; };
|
||||||
|
0A89346B736DBCF81F3A4851 /* torrent-metainfo.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "torrent-metainfo.cc"; sourceTree = "<group>"; };
|
||||||
|
0A89346B736DBCF81F3A4853 /* torrent-metainfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "torrent-metainfo.h"; sourceTree = "<group>"; };
|
||||||
4DB74F070E8CD75100AEB1A8 /* wildmat.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = wildmat.c; sourceTree = "<group>"; };
|
4DB74F070E8CD75100AEB1A8 /* wildmat.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = wildmat.c; sourceTree = "<group>"; };
|
||||||
4DCCBB3C09C3D71100D3CABF /* TorrentCell.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = TorrentCell.mm; sourceTree = "<group>"; };
|
4DCCBB3C09C3D71100D3CABF /* TorrentCell.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = TorrentCell.mm; sourceTree = "<group>"; };
|
||||||
4DCCBB3D09C3D71100D3CABF /* TorrentCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TorrentCell.h; sourceTree = "<group>"; };
|
4DCCBB3D09C3D71100D3CABF /* TorrentCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TorrentCell.h; sourceTree = "<group>"; };
|
||||||
|
@ -1390,6 +1394,8 @@
|
||||||
C11DEA151FCD31C0009E22B9 /* subprocess.h */,
|
C11DEA151FCD31C0009E22B9 /* subprocess.h */,
|
||||||
4D80185710BBC0B0008A4AF2 /* magnet-metainfo.cc */,
|
4D80185710BBC0B0008A4AF2 /* magnet-metainfo.cc */,
|
||||||
4D80185810BBC0B0008A4AF2 /* magnet-metainfo.h */,
|
4D80185810BBC0B0008A4AF2 /* magnet-metainfo.h */,
|
||||||
|
0A89346B736DBCF81F3A4851 /* torrent-metainfo.cc */,
|
||||||
|
0A89346B736DBCF81F3A4853 /* torrent-metainfo.h */,
|
||||||
4D8017E810BBC073008A4AF2 /* torrent-magnet.cc */,
|
4D8017E810BBC073008A4AF2 /* torrent-magnet.cc */,
|
||||||
4D8017E910BBC073008A4AF2 /* torrent-magnet.h */,
|
4D8017E910BBC073008A4AF2 /* torrent-magnet.h */,
|
||||||
0A6169A50FE5C9A200C66CE6 /* bitfield.cc */,
|
0A6169A50FE5C9A200C66CE6 /* bitfield.cc */,
|
||||||
|
@ -1882,6 +1888,7 @@
|
||||||
ED8A16412735A8AA000D61F9 /* peer-mgr-wishlist.h in Headers */,
|
ED8A16412735A8AA000D61F9 /* peer-mgr-wishlist.h in Headers */,
|
||||||
4D8017EB10BBC073008A4AF2 /* torrent-magnet.h in Headers */,
|
4D8017EB10BBC073008A4AF2 /* torrent-magnet.h in Headers */,
|
||||||
4D80185A10BBC0B0008A4AF2 /* magnet-metainfo.h in Headers */,
|
4D80185A10BBC0B0008A4AF2 /* magnet-metainfo.h in Headers */,
|
||||||
|
0A89346B736DBCF81F3A4852 /* torrent-metainfo.h in Headers */,
|
||||||
A209EE5D1144B51E002B02D1 /* history.h in Headers */,
|
A209EE5D1144B51E002B02D1 /* history.h in Headers */,
|
||||||
A247A443114C701800547DFC /* InfoViewController.h in Headers */,
|
A247A443114C701800547DFC /* InfoViewController.h in Headers */,
|
||||||
A220EC5C118C8A060022B4BE /* tr-lpd.h in Headers */,
|
A220EC5C118C8A060022B4BE /* tr-lpd.h in Headers */,
|
||||||
|
@ -2479,6 +2486,7 @@
|
||||||
66F977825E65AD498C028BB0 /* announce-list.cc in Sources */,
|
66F977825E65AD498C028BB0 /* announce-list.cc in Sources */,
|
||||||
4D8017EA10BBC073008A4AF2 /* torrent-magnet.cc in Sources */,
|
4D8017EA10BBC073008A4AF2 /* torrent-magnet.cc in Sources */,
|
||||||
4D80185910BBC0B0008A4AF2 /* magnet-metainfo.cc in Sources */,
|
4D80185910BBC0B0008A4AF2 /* magnet-metainfo.cc in Sources */,
|
||||||
|
0A89346B736DBCF81F3A4850 /* torrent-metainfo.cc in Sources */,
|
||||||
A220EC5B118C8A060022B4BE /* tr-lpd.cc in Sources */,
|
A220EC5B118C8A060022B4BE /* tr-lpd.cc in Sources */,
|
||||||
C1FEE57A1C3223CC00D62832 /* watchdir.cc in Sources */,
|
C1FEE57A1C3223CC00D62832 /* watchdir.cc in Sources */,
|
||||||
A23547E211CD0B090046EAE6 /* cache.cc in Sources */,
|
A23547E211CD0B090046EAE6 /* cache.cc in Sources */,
|
||||||
|
|
|
@ -58,6 +58,7 @@ set(PROJECT_FILES
|
||||||
subprocess-win32.cc
|
subprocess-win32.cc
|
||||||
torrent-ctor.cc
|
torrent-ctor.cc
|
||||||
torrent-magnet.cc
|
torrent-magnet.cc
|
||||||
|
torrent-metainfo.cc
|
||||||
torrent.cc
|
torrent.cc
|
||||||
tr-assert.cc
|
tr-assert.cc
|
||||||
tr-dht.cc
|
tr-dht.cc
|
||||||
|
@ -183,6 +184,7 @@ set(${PROJECT_NAME}_PRIVATE_HEADERS
|
||||||
stats.h
|
stats.h
|
||||||
subprocess.h
|
subprocess.h
|
||||||
torrent-magnet.h
|
torrent-magnet.h
|
||||||
|
torrent-metainfo.h
|
||||||
torrent.h
|
torrent.h
|
||||||
tr-dht.h
|
tr-dht.h
|
||||||
tr-lpd.h
|
tr-lpd.h
|
||||||
|
|
|
@ -26,6 +26,8 @@ using namespace std::literals;
|
||||||
|
|
||||||
/* this base32 code converted from code by Robert Kaye and Gordon Mohr
|
/* this base32 code converted from code by Robert Kaye and Gordon Mohr
|
||||||
* and is public domain. see http://bitzi.com/publicdomain for more info */
|
* and is public domain. see http://bitzi.com/publicdomain for more info */
|
||||||
|
namespace
|
||||||
|
{
|
||||||
namespace bitzi
|
namespace bitzi
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -106,11 +108,17 @@ void base32_to_sha1(uint8_t* out, char const* in, size_t const inlen)
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace bitzi
|
} // namespace bitzi
|
||||||
|
} // namespace
|
||||||
|
|
||||||
/***
|
/***
|
||||||
****
|
****
|
||||||
***/
|
***/
|
||||||
|
|
||||||
|
void tr_magnet_metainfo::clear()
|
||||||
|
{
|
||||||
|
*this = tr_magnet_metainfo{};
|
||||||
|
}
|
||||||
|
|
||||||
std::string tr_magnet_metainfo::magnet() const
|
std::string tr_magnet_metainfo::magnet() const
|
||||||
{
|
{
|
||||||
auto s = std::string{};
|
auto s = std::string{};
|
||||||
|
@ -118,19 +126,19 @@ std::string tr_magnet_metainfo::magnet() const
|
||||||
s += "magnet:?xt=urn:btih:"sv;
|
s += "magnet:?xt=urn:btih:"sv;
|
||||||
s += infoHashString();
|
s += infoHashString();
|
||||||
|
|
||||||
if (!std::empty(name))
|
if (!std::empty(name_))
|
||||||
{
|
{
|
||||||
s += "&dn="sv;
|
s += "&dn="sv;
|
||||||
tr_http_escape(s, name, true);
|
tr_http_escape(s, name_, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto const& tracker : this->announce_list)
|
for (auto const& tracker : this->announceList())
|
||||||
{
|
{
|
||||||
s += "&tr="sv;
|
s += "&tr="sv;
|
||||||
tr_http_escape(s, tracker.announce.full, true);
|
tr_http_escape(s, tracker.announce.full, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto const& webseed : webseed_urls)
|
for (auto const& webseed : webseed_urls_)
|
||||||
{
|
{
|
||||||
s += "&ws="sv;
|
s += "&ws="sv;
|
||||||
tr_http_escape(s, webseed, true);
|
tr_http_escape(s, webseed, true);
|
||||||
|
@ -153,12 +161,12 @@ bool tr_magnet_metainfo::parseMagnet(std::string_view magnet_link, tr_error** er
|
||||||
{
|
{
|
||||||
if (key == "dn"sv)
|
if (key == "dn"sv)
|
||||||
{
|
{
|
||||||
this->name = tr_urlPercentDecode(value);
|
this->setName(tr_urlPercentDecode(value));
|
||||||
}
|
}
|
||||||
else if (key == "tr"sv || tr_strvStartsWith(key, "tr."sv))
|
else if (key == "tr"sv || tr_strvStartsWith(key, "tr."sv))
|
||||||
{
|
{
|
||||||
// "tr." explanation @ https://trac.transmissionbt.com/ticket/3341
|
// "tr." explanation @ https://trac.transmissionbt.com/ticket/3341
|
||||||
this->announce_list.add(this->announce_list.nextTier(), tr_urlPercentDecode(value));
|
this->announce_list_.add(this->announce_list_.nextTier(), tr_urlPercentDecode(value));
|
||||||
}
|
}
|
||||||
else if (key == "ws"sv)
|
else if (key == "ws"sv)
|
||||||
{
|
{
|
||||||
|
@ -166,7 +174,7 @@ bool tr_magnet_metainfo::parseMagnet(std::string_view magnet_link, tr_error** er
|
||||||
auto const url_sv = tr_strvStrip(url);
|
auto const url_sv = tr_strvStrip(url);
|
||||||
if (tr_urlIsValid(url_sv))
|
if (tr_urlIsValid(url_sv))
|
||||||
{
|
{
|
||||||
this->webseed_urls.emplace_back(url_sv);
|
this->webseed_urls_.emplace_back(url_sv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (key == "xt"sv)
|
else if (key == "xt"sv)
|
||||||
|
@ -178,13 +186,13 @@ bool tr_magnet_metainfo::parseMagnet(std::string_view magnet_link, tr_error** er
|
||||||
switch (std::size(hash))
|
switch (std::size(hash))
|
||||||
{
|
{
|
||||||
case TR_SHA1_DIGEST_STRLEN:
|
case TR_SHA1_DIGEST_STRLEN:
|
||||||
this->info_hash = tr_sha1_from_string(hash);
|
this->info_hash_ = tr_sha1_from_string(hash);
|
||||||
got_checksum = true;
|
got_checksum = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 32:
|
case 32:
|
||||||
bitzi::base32_to_sha1(
|
bitzi::base32_to_sha1(
|
||||||
reinterpret_cast<uint8_t*>(std::data(this->info_hash)),
|
reinterpret_cast<uint8_t*>(std::data(this->info_hash_)),
|
||||||
std::data(hash),
|
std::data(hash),
|
||||||
std::size(hash));
|
std::size(hash));
|
||||||
got_checksum = true;
|
got_checksum = true;
|
||||||
|
@ -197,6 +205,8 @@ bool tr_magnet_metainfo::parseMagnet(std::string_view magnet_link, tr_error** er
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info_hash_str_ = tr_sha1_to_string(this->infoHash());
|
||||||
|
|
||||||
return got_checksum;
|
return got_checksum;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,10 +215,10 @@ void tr_magnet_metainfo::toVariant(tr_variant* top) const
|
||||||
tr_variantInitDict(top, 4);
|
tr_variantInitDict(top, 4);
|
||||||
|
|
||||||
// announce list
|
// announce list
|
||||||
auto n = std::size(this->announce_list);
|
auto n = std::size(this->announceList());
|
||||||
if (n == 1)
|
if (n == 1)
|
||||||
{
|
{
|
||||||
tr_variantDictAddQuark(top, TR_KEY_announce, this->announce_list.at(0).announce_str.quark());
|
tr_variantDictAddQuark(top, TR_KEY_announce, this->announceList().at(0).announce_str.quark());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -216,7 +226,7 @@ void tr_magnet_metainfo::toVariant(tr_variant* top) const
|
||||||
tr_variant* tracker_list = nullptr;
|
tr_variant* tracker_list = nullptr;
|
||||||
|
|
||||||
auto* tier_list = tr_variantDictAddList(top, TR_KEY_announce_list, n);
|
auto* tier_list = tr_variantDictAddList(top, TR_KEY_announce_list, n);
|
||||||
for (auto const& tracker : this->announce_list)
|
for (auto const& tracker : this->announceList())
|
||||||
{
|
{
|
||||||
if (tracker_list == nullptr || current_tier != tracker.tier)
|
if (tracker_list == nullptr || current_tier != tracker.tier)
|
||||||
{
|
{
|
||||||
|
@ -229,12 +239,12 @@ void tr_magnet_metainfo::toVariant(tr_variant* top) const
|
||||||
}
|
}
|
||||||
|
|
||||||
// webseeds
|
// webseeds
|
||||||
n = std::size(this->webseed_urls);
|
n = std::size(this->webseeds());
|
||||||
if (n != 0)
|
if (n != 0)
|
||||||
{
|
{
|
||||||
tr_variant* list = tr_variantDictAddList(top, TR_KEY_url_list, n);
|
tr_variant* list = tr_variantDictAddList(top, TR_KEY_url_list, n);
|
||||||
|
|
||||||
for (auto& url : this->webseed_urls)
|
for (auto& url : this->webseeds())
|
||||||
{
|
{
|
||||||
tr_variantListAddStr(list, url);
|
tr_variantListAddStr(list, url);
|
||||||
}
|
}
|
||||||
|
@ -243,10 +253,10 @@ void tr_magnet_metainfo::toVariant(tr_variant* top) const
|
||||||
// nonstandard keys
|
// nonstandard keys
|
||||||
auto* const d = tr_variantDictAddDict(top, TR_KEY_magnet_info, 2);
|
auto* const d = tr_variantDictAddDict(top, TR_KEY_magnet_info, 2);
|
||||||
|
|
||||||
tr_variantDictAddRaw(d, TR_KEY_info_hash, std::data(this->info_hash), std::size(this->info_hash));
|
tr_variantDictAddRaw(d, TR_KEY_info_hash, std::data(this->infoHash()), std::size(this->infoHash()));
|
||||||
|
|
||||||
if (!std::empty(this->name))
|
if (!std::empty(this->name()))
|
||||||
{
|
{
|
||||||
tr_variantDictAddStr(d, TR_KEY_display_name, this->name);
|
tr_variantDictAddStr(d, TR_KEY_display_name, this->name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,33 +19,58 @@
|
||||||
#include "transmission.h"
|
#include "transmission.h"
|
||||||
|
|
||||||
#include "announce-list.h"
|
#include "announce-list.h"
|
||||||
#include "quark.h"
|
|
||||||
#include "tr-macros.h"
|
|
||||||
|
|
||||||
struct tr_error;
|
struct tr_error;
|
||||||
struct tr_variant;
|
struct tr_variant;
|
||||||
|
|
||||||
struct tr_magnet_metainfo
|
class tr_magnet_metainfo
|
||||||
{
|
{
|
||||||
|
public:
|
||||||
bool parseMagnet(std::string_view magnet_link, tr_error** error = nullptr);
|
bool parseMagnet(std::string_view magnet_link, tr_error** error = nullptr);
|
||||||
|
|
||||||
std::string magnet() const;
|
std::string magnet() const;
|
||||||
|
virtual ~tr_magnet_metainfo() = default;
|
||||||
|
|
||||||
void toVariant(tr_variant*) const;
|
auto const& infoHash() const
|
||||||
|
|
||||||
std::string_view infoHashString() const
|
|
||||||
{
|
{
|
||||||
// trim one byte off the end because of zero termination
|
return info_hash_;
|
||||||
return std::string_view{ std::data(info_hash_chars), std::size(info_hash_chars) - 1 };
|
}
|
||||||
|
auto const& name() const
|
||||||
|
{
|
||||||
|
return name_;
|
||||||
|
}
|
||||||
|
auto const& webseeds() const
|
||||||
|
{
|
||||||
|
return webseed_urls_;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr_announce_list announce_list;
|
auto& announceList()
|
||||||
|
{
|
||||||
|
return announce_list_;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> webseed_urls;
|
auto const& announceList() const
|
||||||
|
{
|
||||||
|
return announce_list_;
|
||||||
|
}
|
||||||
|
|
||||||
std::string name;
|
std::string const& infoHashString() const
|
||||||
|
{
|
||||||
|
return info_hash_str_;
|
||||||
|
}
|
||||||
|
|
||||||
tr_sha1_digest_string_t info_hash_chars;
|
virtual void clear();
|
||||||
|
|
||||||
tr_sha1_digest_t info_hash;
|
void setName(std::string_view name)
|
||||||
|
{
|
||||||
|
name_ = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void toVariant(tr_variant* top) const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
tr_announce_list announce_list_;
|
||||||
|
std::vector<std::string> webseed_urls_;
|
||||||
|
tr_sha1_digest_t info_hash_;
|
||||||
|
std::string info_hash_str_;
|
||||||
|
std::string name_;
|
||||||
};
|
};
|
||||||
|
|
699
libtransmission/torrent-metainfo.cc
Normal file
699
libtransmission/torrent-metainfo.cc
Normal file
|
@ -0,0 +1,699 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2007-2014 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <iterator>
|
||||||
|
#include <numeric>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <event2/util.h> // evutil_ascii_strncasecmp
|
||||||
|
|
||||||
|
#include "transmission.h"
|
||||||
|
|
||||||
|
#include "crypto-utils.h"
|
||||||
|
#include "error-types.h"
|
||||||
|
#include "error.h"
|
||||||
|
#include "torrent-metainfo.h"
|
||||||
|
#include "tr-assert.h"
|
||||||
|
#include "utils.h"
|
||||||
|
#include "variant.h"
|
||||||
|
#include "web-utils.h"
|
||||||
|
|
||||||
|
using namespace std::literals;
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
tr_piece_index_t getBytePiece(tr_torrent_metainfo const& tm, uint64_t byte_offset)
|
||||||
|
{
|
||||||
|
// handle 0-byte files at the end of a torrent
|
||||||
|
return byte_offset == tm.total_size ? tm.n_pieces - 1 : byte_offset / tm.piece_size;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
std::string tr_new_magnet_metainfo::makeFilename(std::string_view dirname, FilenameFormat format, std::string_view suffix) const
|
||||||
|
{
|
||||||
|
// `${dirname}/${name}.${info_hash}${suffix}`
|
||||||
|
// `${dirname}/${info_hash}${suffix}`
|
||||||
|
return format == FilenameFormat::NameAndParitalHash ?
|
||||||
|
tr_strvJoin(dirname, "/"sv, this->name, "."sv, this->infoHashString().substr(0, 16), suffix) :
|
||||||
|
tr_strvJoin(dirname, "/"sv, this->infoHashString(), suffix);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// tr_torrent_metainfo
|
||||||
|
|
||||||
|
//// C BINDINGS
|
||||||
|
|
||||||
|
/// Lifecycle
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
tr_torrent_metainfo* tr_torrentMetainfoNewFromData(char const* data, size_t data_len, struct tr_error** error)
|
||||||
|
{
|
||||||
|
auto* tm = new tr_torrent_metainfo{};
|
||||||
|
if (!tm->parseBenc(std::string_view{ data, data_len }, error))
|
||||||
|
{
|
||||||
|
delete tm;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tm;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
tr_torrent_metainfo* tr_torrentMetainfoNewFromFile(char const* filename, struct tr_error** error)
|
||||||
|
{
|
||||||
|
auto* tm = new tr_torrent_metainfo{};
|
||||||
|
if (!tm->parseBencFromFile(filename ? filename : "", nullptr, error))
|
||||||
|
{
|
||||||
|
delete tm;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tm;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
void tr_torrentMetainfoFree(tr_torrent_metainfo* tm)
|
||||||
|
{
|
||||||
|
delete tm;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//// Accessors
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
char* tr_torrentMetainfoMagnet(struct tr_torrent_metainfo const* tm)
|
||||||
|
{
|
||||||
|
return tr_strvDup(tm->magnet());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// Info
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
tr_torrent_metainfo_info* tr_torrentMetainfoGet(tr_torrent_metainfo const* tm, tr_torrent_metainfo_info* setme)
|
||||||
|
{
|
||||||
|
setme->comment = tm->comment.c_str();
|
||||||
|
setme->creator = tm->creator.c_str();
|
||||||
|
setme->info_hash = tm->info_hash;
|
||||||
|
setme->info_hash_string = std::data(tm->info_hash_chars);
|
||||||
|
setme->is_private = tm->is_private;
|
||||||
|
setme->n_pieces = tm->n_pieces;
|
||||||
|
setme->name = tm->name.c_str();
|
||||||
|
setme->source = tm->source.c_str();
|
||||||
|
setme->time_created = tm->time_created;
|
||||||
|
setme->total_size = tm->total_size;
|
||||||
|
return setme;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// Files
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
size_t tr_torrentMetainfoFileCount(tr_torrent_metainfo const* tm)
|
||||||
|
{
|
||||||
|
return std::size(tm->files);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
tr_torrent_metainfo_file_info* tr_torrentMetainfoFile(
|
||||||
|
tr_torrent_metainfo const* tm,
|
||||||
|
size_t n,
|
||||||
|
tr_torrent_metainfo_file_info* setme)
|
||||||
|
{
|
||||||
|
auto& file = tm->files[n];
|
||||||
|
setme->path = file.path.c_str();
|
||||||
|
setme->size = file.size;
|
||||||
|
return setme;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// Trackers
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
size_t tr_torrentMetainfoTrackerCount(tr_torrent_metainfo const* tm)
|
||||||
|
{
|
||||||
|
return std::size(tm->trackers);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
tr_torrent_metainfo_tracker_info* tr_torrentMetainfoTracker(
|
||||||
|
tr_torrent_metainfo const* tm,
|
||||||
|
size_t n,
|
||||||
|
tr_torrent_metainfo_tracker_info* setme)
|
||||||
|
{
|
||||||
|
auto it = std::begin(tm->trackers);
|
||||||
|
std::advance(it, n);
|
||||||
|
auto const& tracker = it->second;
|
||||||
|
setme->announce_url = tr_quark_get_string(tracker.announce_url);
|
||||||
|
setme->scrape_url = tr_quark_get_string(tracker.scrape_url);
|
||||||
|
setme->tier = tracker.tier;
|
||||||
|
return setme;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
void tr_metainfoDestruct(tr_info* inf)
|
||||||
|
{
|
||||||
|
for (unsigned int i = 0; i < inf->webseedCount; i++)
|
||||||
|
{
|
||||||
|
tr_free(inf->webseeds[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (tr_file_index_t ff = 0; ff < inf->fileCount; ff++)
|
||||||
|
{
|
||||||
|
tr_free(inf->files[ff].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr_free(inf->webseeds);
|
||||||
|
tr_free(inf->files);
|
||||||
|
tr_free(inf->comment);
|
||||||
|
tr_free(inf->creator);
|
||||||
|
tr_free(inf->source);
|
||||||
|
tr_free(inf->torrent);
|
||||||
|
tr_free(inf->originalName);
|
||||||
|
tr_free(inf->name);
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i < inf->trackerCount; i++)
|
||||||
|
{
|
||||||
|
tr_free(inf->trackers[i].announce);
|
||||||
|
tr_free(inf->trackers[i].scrape);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr_free(inf->trackers);
|
||||||
|
|
||||||
|
memset(inf, '\0', sizeof(tr_info));
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string getTorrentFilename(tr_session const* session, tr_info const* inf, enum tr_metainfo_basename_format format)
|
||||||
|
{
|
||||||
|
return tr_buildTorrentFilename(tr_getTorrentDir(session), inf, format, ".torrent"sv);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tr_metainfoRemoveSaved(tr_session const* session, tr_torrent_metainfo const& metainfo)
|
||||||
|
{
|
||||||
|
auto filename = getTorrentFilename(session, inf, tr_torrent_metainfo::FilenameFormat::FullHash);
|
||||||
|
tr_sys_path_remove(filename.c_str(), nullptr);
|
||||||
|
|
||||||
|
filename = getTorrentFilename(session, inf, tr_torrent_metainfo::FilenameFormat::NameAndParitalHash);
|
||||||
|
tr_sys_path_remove(filename.c_str(), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tr_metainfoMigrateFile(
|
||||||
|
tr_session const* session,
|
||||||
|
tr_info const* info,
|
||||||
|
enum tr_metainfo_basename_format old_format,
|
||||||
|
enum tr_metainfo_basename_format new_format)
|
||||||
|
{
|
||||||
|
auto const old_filename = getTorrentFilename(session, info, old_format);
|
||||||
|
auto const new_filename = getTorrentFilename(session, info, new_format);
|
||||||
|
|
||||||
|
if (tr_sys_path_rename(old_filename.c_str(), new_filename.c_str(), nullptr))
|
||||||
|
{
|
||||||
|
tr_logAddNamedError(
|
||||||
|
info->name,
|
||||||
|
"Migrated torrent file from \"%s\" to \"%s\"",
|
||||||
|
old_filename.c_str(),
|
||||||
|
new_filename.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/***
|
||||||
|
****
|
||||||
|
***/
|
||||||
|
|
||||||
|
void tr_torrent_metainfo::clear()
|
||||||
|
{
|
||||||
|
*this = tr_torrent_metainfo{};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Ensure that the URLs for multfile torrents end in a slash.
|
||||||
|
*
|
||||||
|
* See http://bittorrent.org/beps/bep_0019.html#metadata-extension
|
||||||
|
* for background on how the trailing slash is used for "url-list"
|
||||||
|
* fields.
|
||||||
|
*
|
||||||
|
* This function is to workaround some .torrent generators, such as
|
||||||
|
* mktorrent and very old versions of utorrent, that don't add the
|
||||||
|
* trailing slash for multifile torrents if omitted by the end user.
|
||||||
|
*/
|
||||||
|
std::string tr_torrent_metainfo::fixWebseedUrl(tr_torrent_metainfo const& tm, std::string_view url)
|
||||||
|
{
|
||||||
|
url = tr_strvStrip(url);
|
||||||
|
|
||||||
|
if (std::size(tm.files_) > 1 && !std::empty(url) && url.back() != '/')
|
||||||
|
{
|
||||||
|
return std::string{ url } + '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::string{ url };
|
||||||
|
}
|
||||||
|
|
||||||
|
void tr_torrent_metainfo::parseWebseeds(tr_torrent_metainfo& setme, tr_variant* meta)
|
||||||
|
{
|
||||||
|
setme.webseed_urls_.clear();
|
||||||
|
|
||||||
|
auto url = std::string_view{};
|
||||||
|
tr_variant* urls = nullptr;
|
||||||
|
if (tr_variantDictFindList(meta, TR_KEY_url_list, &urls))
|
||||||
|
{
|
||||||
|
size_t const n = tr_variantListSize(urls);
|
||||||
|
setme.webseed_urls_.reserve(n);
|
||||||
|
for (size_t i = 0; i < n; ++i)
|
||||||
|
{
|
||||||
|
if (tr_variantGetStrView(tr_variantListChild(urls, i), &url) && tr_urlIsValid(url))
|
||||||
|
{
|
||||||
|
setme.webseed_urls_.push_back(fixWebseedUrl(setme, url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (tr_variantDictFindStrView(meta, TR_KEY_url_list, &url) && tr_urlIsValid(url)) // handle single items in webseeds
|
||||||
|
{
|
||||||
|
setme.webseed_urls_.push_back(fixWebseedUrl(setme, url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool appendSanitizedComponent(std::string& out, std::string_view in, bool* setme_is_adjusted)
|
||||||
|
{
|
||||||
|
auto const original_out_len = std::size(out);
|
||||||
|
auto const original_in = in;
|
||||||
|
*setme_is_adjusted = false;
|
||||||
|
|
||||||
|
// remove leading spaces
|
||||||
|
auto constexpr leading_test = [](auto ch)
|
||||||
|
{
|
||||||
|
return isspace(ch);
|
||||||
|
};
|
||||||
|
auto const it = std::find_if_not(std::begin(in), std::end(in), leading_test);
|
||||||
|
in.remove_prefix(std::distance(std::begin(in), it));
|
||||||
|
|
||||||
|
// remove trailing spaces and '.'
|
||||||
|
auto constexpr trailing_test = [](auto ch)
|
||||||
|
{
|
||||||
|
return isspace(ch) || ch == '.';
|
||||||
|
};
|
||||||
|
auto const rit = std::find_if_not(std::rbegin(in), std::rend(in), trailing_test);
|
||||||
|
in.remove_suffix(std::distance(std::rbegin(in), rit));
|
||||||
|
|
||||||
|
// munge banned characters
|
||||||
|
// https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
|
||||||
|
auto constexpr ensure_legal_char = [](auto ch)
|
||||||
|
{
|
||||||
|
auto constexpr Banned = std::string_view{ "<>:\"/\\|?*" };
|
||||||
|
auto const banned = Banned.find(ch) != Banned.npos || (unsigned char)ch < 0x20;
|
||||||
|
return banned ? '_' : ch;
|
||||||
|
};
|
||||||
|
auto const old_out_len = std::size(out);
|
||||||
|
std::transform(std::begin(in), std::end(in), std::back_inserter(out), ensure_legal_char);
|
||||||
|
|
||||||
|
// munge banned filenames
|
||||||
|
// https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
|
||||||
|
auto constexpr ReservedNames = std::array<std::string_view, 22>{
|
||||||
|
"CON"sv, "PRN"sv, "AUX"sv, "NUL"sv, "COM1"sv, "COM2"sv, "COM3"sv, "COM4"sv, "COM5"sv, "COM6"sv, "COM7"sv,
|
||||||
|
"COM8"sv, "COM9"sv, "LPT1"sv, "LPT2"sv, "LPT3"sv, "LPT4"sv, "LPT5"sv, "LPT6"sv, "LPT7"sv, "LPT8"sv, "LPT9"sv,
|
||||||
|
};
|
||||||
|
for (auto const& name : ReservedNames)
|
||||||
|
{
|
||||||
|
size_t const name_len = std::size(name);
|
||||||
|
if (evutil_ascii_strncasecmp(out.c_str() + old_out_len, std::data(name), name_len) != 0 ||
|
||||||
|
(out[old_out_len + name_len] != '\0' && out[old_out_len + name_len] != '.'))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.insert(std::begin(out) + old_out_len + name_len, '_');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
*setme_is_adjusted = original_in != std::string_view{ out.c_str() + original_out_len };
|
||||||
|
return std::size(out) > original_out_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string tr_torrent_metainfo::parsePath(std::string_view root, tr_variant* path, std::string& buf)
|
||||||
|
{
|
||||||
|
if (!tr_variantIsList(path))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = root;
|
||||||
|
for (int i = 0, n = tr_variantListSize(path); i < n; i++)
|
||||||
|
{
|
||||||
|
auto raw = std::string_view{};
|
||||||
|
if (!tr_variantGetStrView(tr_variantListChild(path, i), &raw))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto is_component_adjusted = bool{};
|
||||||
|
auto const pos = std::size(buf);
|
||||||
|
if (!appendSanitizedComponent(buf, raw, &is_component_adjusted))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.insert(std::begin(buf) + pos, TR_PATH_DELIMITER);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::size(buf) <= std::size(root))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return tr_strvUtf8Clean(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view tr_torrent_metainfo::parseFiles(tr_torrent_metainfo& setme, tr_variant* info_dict, uint64_t* setme_total_size)
|
||||||
|
{
|
||||||
|
auto is_root_adjusted = bool{ false };
|
||||||
|
auto root_name = std::string{};
|
||||||
|
auto total_size = uint64_t{ 0 };
|
||||||
|
|
||||||
|
setme.files_.clear();
|
||||||
|
|
||||||
|
if (!appendSanitizedComponent(root_name, setme.name_, &is_root_adjusted))
|
||||||
|
{
|
||||||
|
return "invalid name"sv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bittorrent 1.0 spec
|
||||||
|
// http://bittorrent.org/beps/bep_0003.html
|
||||||
|
//
|
||||||
|
// "There is also a key length or a key files, but not both or neither.
|
||||||
|
//
|
||||||
|
// "If length is present then the download represents a single file,
|
||||||
|
// otherwise it represents a set of files which go in a directory structure.
|
||||||
|
// In the single file case, length maps to the length of the file in bytes.
|
||||||
|
auto len = int64_t{};
|
||||||
|
tr_variant* files_entry = nullptr;
|
||||||
|
if (tr_variantDictFindInt(info_dict, TR_KEY_length, &len))
|
||||||
|
{
|
||||||
|
total_size = len;
|
||||||
|
setme.files_.emplace_back(root_name, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
// "For the purposes of the other keys, the multi-file case is treated as
|
||||||
|
// only having a single file by concatenating the files in the order they
|
||||||
|
// appear in the files list. The files list is the value files maps to,
|
||||||
|
// and is a list of dictionaries containing the following keys:
|
||||||
|
// length - The length of the file, in bytes.
|
||||||
|
// path - A list of UTF-8 encoded strings corresponding to subdirectory
|
||||||
|
// names, the last of which is the actual file name (a zero length list
|
||||||
|
// is an error case).
|
||||||
|
// In the multifile case, the name key is the name of a directory.
|
||||||
|
else if (tr_variantDictFindList(info_dict, TR_KEY_files, &files_entry))
|
||||||
|
{
|
||||||
|
|
||||||
|
auto buf = std::string{};
|
||||||
|
auto const n_files = size_t{ tr_variantListSize(files_entry) };
|
||||||
|
for (size_t i = 0; i < n_files; ++i)
|
||||||
|
{
|
||||||
|
auto* const file_entry = tr_variantListChild(files_entry, i);
|
||||||
|
if (!tr_variantIsDict(file_entry))
|
||||||
|
{
|
||||||
|
return "'files' is not a dictionary";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tr_variantDictFindInt(file_entry, TR_KEY_length, &len))
|
||||||
|
{
|
||||||
|
return "length";
|
||||||
|
}
|
||||||
|
|
||||||
|
tr_variant* path_variant = nullptr;
|
||||||
|
if (!tr_variantDictFindList(file_entry, TR_KEY_path_utf_8, &path_variant) &&
|
||||||
|
!tr_variantDictFindList(file_entry, TR_KEY_path, &path_variant))
|
||||||
|
{
|
||||||
|
return "path";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const path = parsePath(root_name, path_variant, buf);
|
||||||
|
if (std::empty(path))
|
||||||
|
{
|
||||||
|
return "path";
|
||||||
|
}
|
||||||
|
|
||||||
|
setme.files_.emplace_back(path, len);
|
||||||
|
total_size += len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: add support for 'file tree' BitTorrent 2 torrents / hybrid torrents.
|
||||||
|
// Patches welcomed!
|
||||||
|
// https://www.bittorrent.org/beps/bep_0052.html#info-dictionary
|
||||||
|
return "'info' dict has neither 'files' nor 'length' key";
|
||||||
|
}
|
||||||
|
|
||||||
|
*setme_total_size = total_size;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.bittorrent.org/beps/bep_0012.html
|
||||||
|
std::string_view tr_torrent_metainfo::parseAnnounce(tr_torrent_metainfo& setme, tr_variant* meta)
|
||||||
|
{
|
||||||
|
setme.announce_list_.clear();
|
||||||
|
|
||||||
|
auto url = std::string_view{};
|
||||||
|
|
||||||
|
// announce-list
|
||||||
|
// example: d['announce-list'] = [ [tracker1], [backup1], [backup2] ]
|
||||||
|
tr_variant* tiers = nullptr;
|
||||||
|
if (tr_variantDictFindList(meta, TR_KEY_announce_list, &tiers))
|
||||||
|
{
|
||||||
|
for (size_t i = 0, n_tiers = tr_variantListSize(tiers); i < n_tiers; ++i)
|
||||||
|
{
|
||||||
|
tr_variant* tier_list = tr_variantListChild(tiers, i);
|
||||||
|
if (tier_list == nullptr)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t j = 0, jn = tr_variantListSize(tier_list); j < jn; ++j)
|
||||||
|
{
|
||||||
|
if (!tr_variantGetStrView(tr_variantListChild(tier_list, j), &url))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setme.announce_list_.add(i, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// single 'announce' url
|
||||||
|
if (std::empty(setme.announce_list_) && tr_variantDictFindStrView(meta, TR_KEY_announce, &url))
|
||||||
|
{
|
||||||
|
setme.announce_list_.add(0, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view tr_torrent_metainfo::parseImpl(tr_torrent_metainfo& setme, tr_variant* meta, std::string_view benc)
|
||||||
|
{
|
||||||
|
int64_t i = 0;
|
||||||
|
auto sv = std::string_view{};
|
||||||
|
|
||||||
|
// info_hash: urlencoded 20-byte SHA1 hash of the value of the info key
|
||||||
|
// from the Metainfo file. Note that the value will be a bencoded
|
||||||
|
// dictionary, given the definition of the info key above.
|
||||||
|
tr_variant* info_dict = nullptr;
|
||||||
|
if (tr_variantDictFindDict(meta, TR_KEY_info, &info_dict))
|
||||||
|
{
|
||||||
|
// Calculate the hash of the `info` dict.
|
||||||
|
// This is the torrent's unique ID and is central to everything.
|
||||||
|
size_t blen = 0;
|
||||||
|
char* const bstr = tr_variantToStr(info_dict, TR_VARIANT_FMT_BENC, &blen);
|
||||||
|
auto const hash = tr_sha1(std::string_view{ bstr, blen });
|
||||||
|
if (!hash)
|
||||||
|
{
|
||||||
|
tr_free(bstr);
|
||||||
|
return "bad info_dict checksum";
|
||||||
|
}
|
||||||
|
setme.info_hash_ = *hash;
|
||||||
|
setme.info_hash_str_ = tr_sha1_to_string(setme.info_hash_);
|
||||||
|
|
||||||
|
// Remember the offset and length of the bencoded info dict.
|
||||||
|
// This is important when providing metainfo to magnet peers
|
||||||
|
// (see http://bittorrent.org/beps/bep_0009.html for details).
|
||||||
|
//
|
||||||
|
// Calculating this later from scratch is kind of expensive,
|
||||||
|
// so do it here since we've already got the bencoded info dict.
|
||||||
|
auto const it = std::search(std::begin(benc), std::end(benc), bstr, bstr + blen);
|
||||||
|
setme.info_dict_offset_ = std::distance(std::begin(benc), it);
|
||||||
|
setme.info_dict_size_ = blen;
|
||||||
|
|
||||||
|
// In addition, remember the offset of the pieces dictionary entry.
|
||||||
|
// This will be useful when we load piece checksums on demand.
|
||||||
|
auto constexpr Key = "6:pieces"sv;
|
||||||
|
auto constexpr* BKey = std::data(Key);
|
||||||
|
auto const pit = std::search(bstr, bstr + blen, BKey, BKey + std::size(Key));
|
||||||
|
setme.pieces_offset_ = setme.info_dict_offset_ + (pit - bstr) + std::size(Key);
|
||||||
|
|
||||||
|
tr_free(bstr);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return "missing 'info' dictionary";
|
||||||
|
}
|
||||||
|
|
||||||
|
// name
|
||||||
|
if (tr_variantDictFindStrView(info_dict, TR_KEY_name_utf_8, &sv) || tr_variantDictFindStrView(info_dict, TR_KEY_name, &sv))
|
||||||
|
{
|
||||||
|
setme.setName(tr_strvUtf8Clean(sv));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return "'info' dictionary has neither 'name.utf-8' nor 'name'";
|
||||||
|
}
|
||||||
|
|
||||||
|
// comment (optional)
|
||||||
|
if (tr_variantDictFindStrView(meta, TR_KEY_comment_utf_8, &sv) || tr_variantDictFindStrView(meta, TR_KEY_comment, &sv))
|
||||||
|
{
|
||||||
|
setme.comment_ = tr_strvUtf8Clean(sv);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setme.comment_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// created by (optional)
|
||||||
|
if (tr_variantDictFindStrView(meta, TR_KEY_created_by_utf_8, &sv) ||
|
||||||
|
tr_variantDictFindStrView(meta, TR_KEY_created_by, &sv))
|
||||||
|
{
|
||||||
|
setme.creator_ = tr_strvUtf8Clean(sv);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setme.creator_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// creation date (optional)
|
||||||
|
if (tr_variantDictFindInt(meta, TR_KEY_creation_date, &i))
|
||||||
|
{
|
||||||
|
setme.date_created_ = i;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setme.date_created_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// private (optional)
|
||||||
|
if (tr_variantDictFindInt(info_dict, TR_KEY_private, &i) || tr_variantDictFindInt(meta, TR_KEY_private, &i))
|
||||||
|
{
|
||||||
|
setme.is_private_ = i != 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setme.is_private_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// source (optional)
|
||||||
|
if (tr_variantDictFindStrView(info_dict, TR_KEY_source, &sv) || tr_variantDictFindStrView(meta, TR_KEY_source, &sv))
|
||||||
|
{
|
||||||
|
setme.source_ = tr_strvUtf8Clean(sv);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setme.source_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// piece length
|
||||||
|
auto piece_size = uint64_t{};
|
||||||
|
if (tr_variantDictFindInt(info_dict, TR_KEY_piece_length, &i) && (i > 0))
|
||||||
|
{
|
||||||
|
piece_size = i;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return "'info' dict 'piece length' is missing or has an invalid value";
|
||||||
|
}
|
||||||
|
|
||||||
|
// pieces
|
||||||
|
if (tr_variantDictFindStrView(info_dict, TR_KEY_pieces, &sv) && (std::size(sv) % sizeof(tr_sha1_digest_t) == 0))
|
||||||
|
{
|
||||||
|
auto const n = std::size(sv) / sizeof(tr_sha1_digest_t);
|
||||||
|
setme.pieces_.resize(n);
|
||||||
|
std::copy_n(std::data(sv), std::size(sv), reinterpret_cast<char*>(std::data(setme.pieces_)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return "'info' dict 'pieces' is missing or has an invalid value";
|
||||||
|
}
|
||||||
|
|
||||||
|
// files
|
||||||
|
auto total_size = uint64_t{ 0 };
|
||||||
|
auto const errstr = parseFiles(setme, info_dict, &total_size);
|
||||||
|
if (!std::empty(errstr))
|
||||||
|
{
|
||||||
|
return errstr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::empty(setme.files_) || total_size == 0)
|
||||||
|
{
|
||||||
|
return "no files found"sv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the size and piece size match up?
|
||||||
|
setme.block_info_.initSizes(total_size, piece_size);
|
||||||
|
if (setme.block_info_.n_pieces != std::size(setme.pieces_))
|
||||||
|
{
|
||||||
|
return "piece count and file sizes do not match";
|
||||||
|
}
|
||||||
|
|
||||||
|
parseAnnounce(setme, meta);
|
||||||
|
parseWebseeds(setme, meta);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tr_torrent_metainfo::parseBenc(std::string_view benc, tr_error** error)
|
||||||
|
{
|
||||||
|
auto top = tr_variant{};
|
||||||
|
if (!tr_variantFromBuf(&top, TR_VARIANT_PARSE_BENC | TR_VARIANT_PARSE_INPLACE, benc, nullptr, error))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const errmsg = parseImpl(*this, &top, benc);
|
||||||
|
tr_variantFree(&top);
|
||||||
|
if (!std::empty(errmsg))
|
||||||
|
{
|
||||||
|
tr_error_set(error, TR_ERROR_EINVAL, "Error parsing metainfo: %" TR_PRIsv, TR_PRIsv_ARG(errmsg));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tr_torrent_metainfo::parseTorrentFile(std::string_view filename, std::vector<char>* contents, tr_error** error)
|
||||||
|
{
|
||||||
|
auto local_contents = std::vector<char>{};
|
||||||
|
|
||||||
|
if (contents == nullptr)
|
||||||
|
{
|
||||||
|
contents = &local_contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const sz_filename = std::string{ filename };
|
||||||
|
return tr_loadFile(*contents, sz_filename.c_str(), error) &&
|
||||||
|
parseBenc({ std::data(*contents), std::size(*contents) }, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr_sha1_digest_t const& tr_torrent_metainfo::pieceHash(tr_piece_index_t piece) const
|
||||||
|
{
|
||||||
|
return this->pieces_[piece];
|
||||||
|
}
|
167
libtransmission/torrent-metainfo.h
Normal file
167
libtransmission/torrent-metainfo.h
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2005-2014 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ctime>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "transmission.h"
|
||||||
|
|
||||||
|
#include "block-info.h"
|
||||||
|
#include "magnet-metainfo.h"
|
||||||
|
#include "quark.h"
|
||||||
|
|
||||||
|
struct tr_error;
|
||||||
|
struct tr_info;
|
||||||
|
|
||||||
|
struct tr_torrent_metainfo : public tr_magnet_metainfo
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
tr_torrent_metainfo() = default;
|
||||||
|
~tr_torrent_metainfo() override = default;
|
||||||
|
|
||||||
|
[[nodiscard]] auto empty() const
|
||||||
|
{
|
||||||
|
return std::empty(files_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parseBenc(std::string_view benc, tr_error** error = nullptr);
|
||||||
|
|
||||||
|
// Helper function wrapper around parseBenc().
|
||||||
|
// If you're looping through several files, passing in a non-nullptr
|
||||||
|
// `buffer` can reduce the number of memory allocations needed to
|
||||||
|
// load multiple files.
|
||||||
|
bool parseTorrentFile(std::string_view benc_filename, std::vector<char>* buffer = nullptr, tr_error** error = nullptr);
|
||||||
|
|
||||||
|
auto const& blockInfo() const
|
||||||
|
{
|
||||||
|
return block_info_;
|
||||||
|
}
|
||||||
|
auto pieceCount() const
|
||||||
|
{
|
||||||
|
return block_info_.n_pieces;
|
||||||
|
}
|
||||||
|
auto pieceSize() const
|
||||||
|
{
|
||||||
|
return block_info_.piece_size;
|
||||||
|
}
|
||||||
|
auto totalSize() const
|
||||||
|
{
|
||||||
|
return block_info_.total_size;
|
||||||
|
}
|
||||||
|
auto const& comment() const
|
||||||
|
{
|
||||||
|
return comment_;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const& creator() const
|
||||||
|
{
|
||||||
|
return creator_;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const& files() const
|
||||||
|
{
|
||||||
|
return files_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto const& isPrivate() const
|
||||||
|
{
|
||||||
|
return is_private_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto const& parsedTorrentFile() const
|
||||||
|
{
|
||||||
|
return torrent_file_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] tr_sha1_digest_t const& pieceHash(tr_piece_index_t piece) const;
|
||||||
|
|
||||||
|
[[nodiscard]] auto const& source() const
|
||||||
|
{
|
||||||
|
return source_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto const& dateCreated() const
|
||||||
|
{
|
||||||
|
return date_created_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() final;
|
||||||
|
|
||||||
|
[[nodiscard]] std::string benc() const;
|
||||||
|
|
||||||
|
[[nodiscard]] auto infoDictSize() const
|
||||||
|
{
|
||||||
|
return info_dict_size_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto infoDictOffset() const
|
||||||
|
{
|
||||||
|
return info_dict_offset_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::string parsePath(std::string_view root, tr_variant* path, std::string& buf);
|
||||||
|
static std::string fixWebseedUrl(tr_torrent_metainfo const& tm, std::string_view url);
|
||||||
|
static std::string_view parseFiles(tr_torrent_metainfo& setme, tr_variant* info_dict, uint64_t* setme_total_size);
|
||||||
|
static std::string_view parseImpl(tr_torrent_metainfo& setme, tr_variant* meta, std::string_view benc);
|
||||||
|
static std::string_view parseAnnounce(tr_torrent_metainfo& setme, tr_variant* meta);
|
||||||
|
static void parseWebseeds(tr_torrent_metainfo& setme, tr_variant* meta);
|
||||||
|
|
||||||
|
struct file_t
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::string const& path() const
|
||||||
|
{
|
||||||
|
return path_;
|
||||||
|
}
|
||||||
|
uint64_t length() const
|
||||||
|
{
|
||||||
|
return length_;
|
||||||
|
}
|
||||||
|
|
||||||
|
file_t(std::string_view path, uint64_t length)
|
||||||
|
: path_{ path }
|
||||||
|
, length_{ length }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string path_;
|
||||||
|
uint64_t length_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
tr_block_info block_info_ = tr_block_info{ 0, 0 };
|
||||||
|
|
||||||
|
std::vector<tr_sha1_digest_t> pieces_;
|
||||||
|
std::vector<file_t> files_;
|
||||||
|
|
||||||
|
std::string comment_;
|
||||||
|
std::string creator_;
|
||||||
|
std::string source_;
|
||||||
|
|
||||||
|
// empty unless `parseTorrentFile()` was used
|
||||||
|
std::string torrent_file_;
|
||||||
|
|
||||||
|
time_t date_created_ = 0;
|
||||||
|
|
||||||
|
// Offset + size of the bencoded info dict subset of the bencoded data.
|
||||||
|
// Used when loading pieces of it to sent to magnet peers.
|
||||||
|
// See http://bittorrent.org/beps/bep_0009.html
|
||||||
|
uint64_t info_dict_size_ = 0;
|
||||||
|
uint64_t info_dict_offset_ = 0;
|
||||||
|
|
||||||
|
// Offset of the bencoded 'pieces' checksums subset of the bencoded data.
|
||||||
|
// Used when loading piece checksums on demand.
|
||||||
|
uint64_t pieces_offset_ = 0;
|
||||||
|
|
||||||
|
bool is_private_ = false;
|
||||||
|
};
|
|
@ -106,7 +106,7 @@ tr_torrent* tr_torrentFindFromHash(tr_session* session, tr_sha1_digest_t const*
|
||||||
tr_torrent* tr_torrentFindFromMagnetLink(tr_session* session, char const* magnet_link)
|
tr_torrent* tr_torrentFindFromMagnetLink(tr_session* session, char const* magnet_link)
|
||||||
{
|
{
|
||||||
auto mm = tr_magnet_metainfo{};
|
auto mm = tr_magnet_metainfo{};
|
||||||
return mm.parseMagnet(magnet_link ? magnet_link : "") ? session->getTorrent(mm.info_hash) : nullptr;
|
return mm.parseMagnet(magnet_link ? magnet_link : "") ? session->getTorrent(mm.infoHash()) : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr_torrent* tr_torrentFindFromObfuscatedHash(tr_session* session, tr_sha1_digest_t const& obfuscated_hash)
|
tr_torrent* tr_torrentFindFromObfuscatedHash(tr_session* session, tr_sha1_digest_t const& obfuscated_hash)
|
||||||
|
|
|
@ -316,7 +316,7 @@ int tr_dhtInit(tr_session* ss)
|
||||||
|
|
||||||
auto const dat_file = tr_strvPath(ss->configDir, "dht.dat"sv);
|
auto const dat_file = tr_strvPath(ss->configDir, "dht.dat"sv);
|
||||||
auto benc = tr_variant{};
|
auto benc = tr_variant{};
|
||||||
auto const ok = tr_variantFromFile(&benc, TR_VARIANT_PARSE_BENC, dat_file.c_str());
|
auto const ok = tr_variantFromFile(&benc, TR_VARIANT_PARSE_BENC, dat_file);
|
||||||
|
|
||||||
bool have_id = false;
|
bool have_id = false;
|
||||||
uint8_t* nodes = nullptr;
|
uint8_t* nodes = nullptr;
|
||||||
|
@ -466,7 +466,7 @@ void tr_dhtUninit(tr_session* ss)
|
||||||
}
|
}
|
||||||
|
|
||||||
auto const dat_file = tr_strvPath(ss->configDir, "dht.dat");
|
auto const dat_file = tr_strvPath(ss->configDir, "dht.dat");
|
||||||
tr_variantToFile(&benc, TR_VARIANT_FMT_BENC, dat_file.c_str());
|
tr_variantToFile(&benc, TR_VARIANT_FMT_BENC, dat_file);
|
||||||
tr_variantFree(&benc);
|
tr_variantFree(&benc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ add_executable(libtransmission-test
|
||||||
crypto-test-ref.h
|
crypto-test-ref.h
|
||||||
crypto-test.cc
|
crypto-test.cc
|
||||||
error-test.cc
|
error-test.cc
|
||||||
file-test.cc
|
|
||||||
file-piece-map-test.cc
|
file-piece-map-test.cc
|
||||||
|
file-test.cc
|
||||||
getopt-test.cc
|
getopt-test.cc
|
||||||
history-test.cc
|
history-test.cc
|
||||||
json-test.cc
|
json-test.cc
|
||||||
|
@ -28,6 +28,7 @@ add_executable(libtransmission-test
|
||||||
subprocess-test-script.cmd
|
subprocess-test-script.cmd
|
||||||
subprocess-test.cc
|
subprocess-test.cc
|
||||||
test-fixtures.h
|
test-fixtures.h
|
||||||
|
torrent-metainfo-test.cc
|
||||||
utils-test.cc
|
utils-test.cc
|
||||||
variant-test.cc
|
variant-test.cc
|
||||||
watchdir-test.cc
|
watchdir-test.cc
|
||||||
|
|
|
@ -46,8 +46,8 @@ TEST(MagnetMetainfo, magnetParse)
|
||||||
auto mm = tr_magnet_metainfo{};
|
auto mm = tr_magnet_metainfo{};
|
||||||
|
|
||||||
EXPECT_TRUE(mm.parseMagnet(uri));
|
EXPECT_TRUE(mm.parseMagnet(uri));
|
||||||
EXPECT_EQ(2, std::size(mm.announce_list));
|
EXPECT_EQ(2, std::size(mm.announceList()));
|
||||||
auto it = std::begin(mm.announce_list);
|
auto it = std::begin(mm.announceList());
|
||||||
EXPECT_EQ(0, it->tier);
|
EXPECT_EQ(0, it->tier);
|
||||||
EXPECT_EQ("http://tracker.openbittorrent.com/announce"sv, it->announce.full);
|
EXPECT_EQ("http://tracker.openbittorrent.com/announce"sv, it->announce.full);
|
||||||
EXPECT_EQ("http://tracker.openbittorrent.com/scrape"sv, it->scrape.full);
|
EXPECT_EQ("http://tracker.openbittorrent.com/scrape"sv, it->scrape.full);
|
||||||
|
@ -55,9 +55,9 @@ TEST(MagnetMetainfo, magnetParse)
|
||||||
EXPECT_EQ(1, it->tier);
|
EXPECT_EQ(1, it->tier);
|
||||||
EXPECT_EQ("http://tracker.opentracker.org/announce", it->announce.full);
|
EXPECT_EQ("http://tracker.opentracker.org/announce", it->announce.full);
|
||||||
EXPECT_EQ("http://tracker.opentracker.org/scrape", it->scrape.full);
|
EXPECT_EQ("http://tracker.opentracker.org/scrape", it->scrape.full);
|
||||||
EXPECT_EQ(1, std::size(mm.webseed_urls));
|
EXPECT_EQ(1, std::size(mm.webseeds()));
|
||||||
EXPECT_EQ("http://server.webseed.org/path/to/file"sv, mm.webseed_urls.front());
|
EXPECT_EQ("http://server.webseed.org/path/to/file"sv, mm.webseeds().front());
|
||||||
EXPECT_EQ("Display Name"sv, mm.name);
|
EXPECT_EQ("Display Name"sv, mm.name());
|
||||||
EXPECT_EQ(ExpectedHash, mm.info_hash);
|
EXPECT_EQ(ExpectedHash, mm.infoHash());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
185
tests/libtransmission/torrent-metainfo-test.cc
Normal file
185
tests/libtransmission/torrent-metainfo-test.cc
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
/*
|
||||||
|
* This file Copyright (C) 2013-2014 Mnemosyne LLC
|
||||||
|
*
|
||||||
|
* It may be used under the GNU GPL versions 2 or 3
|
||||||
|
* or any future license endorsed by Mnemosyne LLC.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include "transmission.h"
|
||||||
|
|
||||||
|
#include "error.h"
|
||||||
|
#include "metainfo.h"
|
||||||
|
#include "torrent-metainfo.h"
|
||||||
|
#include "torrent.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
#include "gtest/gtest.h"
|
||||||
|
|
||||||
|
using namespace std::literals;
|
||||||
|
|
||||||
|
TEST(TorrentMetainfo, magnetLink)
|
||||||
|
{
|
||||||
|
// background info @ http://wiki.theory.org/BitTorrent_Magnet-URI_Webseeding
|
||||||
|
char const constexpr* const MagnetLink =
|
||||||
|
"magnet:?"
|
||||||
|
"xt=urn:btih:14ffe5dd23188fd5cb53a1d47f1289db70abf31e"
|
||||||
|
"&dn=ubuntu_12_04_1_desktop_32_bit"
|
||||||
|
"&tr=http%3A%2F%2Ftracker.publicbt.com%2Fannounce"
|
||||||
|
"&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80"
|
||||||
|
"&ws=http%3A%2F%2Ftransmissionbt.com";
|
||||||
|
|
||||||
|
auto metainfo = tr_torrent_metainfo{};
|
||||||
|
EXPECT_TRUE(metainfo.parseMagnet(MagnetLink));
|
||||||
|
EXPECT_EQ(0, std::size(metainfo.files())); // because it's a magnet link
|
||||||
|
EXPECT_EQ(2, std::size(metainfo.announceList()));
|
||||||
|
EXPECT_EQ(MagnetLink, metainfo.magnet());
|
||||||
|
}
|
||||||
|
|
||||||
|
#define BEFORE_PATH \
|
||||||
|
"d10:created by25:Transmission/2.82 (14160)13:creation datei1402280218e8:encoding5:UTF-84:infod5:filesld6:lengthi2e4:pathl"
|
||||||
|
#define AFTER_PATH \
|
||||||
|
"eed6:lengthi2e4:pathl5:b.txteee4:name3:foo12:piece lengthi32768e6:pieces20:ÞÉ`âMs¡Å;˺¬.åÂà7:privatei0eee"
|
||||||
|
|
||||||
|
// FIXME: split these into parameterized tests?
|
||||||
|
TEST(TorrentMetainfo, bucket)
|
||||||
|
{
|
||||||
|
struct LocalTest
|
||||||
|
{
|
||||||
|
std::string_view benc;
|
||||||
|
bool expected_parse_result;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto const tests = std::array<LocalTest, 9>{ {
|
||||||
|
{ BEFORE_PATH "5:a.txt" AFTER_PATH, true },
|
||||||
|
// allow empty components, but not =all= empty components, see bug #5517
|
||||||
|
{ BEFORE_PATH "0:5:a.txt" AFTER_PATH, true },
|
||||||
|
{ BEFORE_PATH "0:0:" AFTER_PATH, false },
|
||||||
|
// allow path separators in a filename (replaced with '_')
|
||||||
|
{ BEFORE_PATH "7:a/a.txt" AFTER_PATH, true },
|
||||||
|
// allow "." components (skipped)
|
||||||
|
{ BEFORE_PATH "1:.5:a.txt" AFTER_PATH, true },
|
||||||
|
{ BEFORE_PATH "5:a.txt1:." AFTER_PATH, true },
|
||||||
|
// allow ".." components (replaced with "__")
|
||||||
|
{ BEFORE_PATH "2:..5:a.txt" AFTER_PATH, true },
|
||||||
|
{ BEFORE_PATH "5:a.txt2:.." AFTER_PATH, true },
|
||||||
|
// fail on empty string
|
||||||
|
{ "", false },
|
||||||
|
} };
|
||||||
|
|
||||||
|
tr_logSetLevel(TR_LOG_SILENT);
|
||||||
|
|
||||||
|
for (auto const& test : tests)
|
||||||
|
{
|
||||||
|
auto metainfo = tr_torrent_metainfo{};
|
||||||
|
EXPECT_EQ(test.expected_parse_result, metainfo.parseBenc(test.benc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TorrentMetainfo, sanitize)
|
||||||
|
{
|
||||||
|
struct LocalTest
|
||||||
|
{
|
||||||
|
std::string_view input;
|
||||||
|
std::string_view expected_output;
|
||||||
|
bool expected_is_adjusted;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto const tests = std::array<LocalTest, 29>{
|
||||||
|
// skipped
|
||||||
|
LocalTest{ ""sv, ""sv, false },
|
||||||
|
{ "."sv, ""sv, true },
|
||||||
|
{ ".."sv, ""sv, true },
|
||||||
|
{ "....."sv, ""sv, true },
|
||||||
|
{ " "sv, ""sv, true },
|
||||||
|
{ " . "sv, ""sv, true },
|
||||||
|
{ ". . ."sv, ""sv, true },
|
||||||
|
// replaced with '_'
|
||||||
|
{ "/"sv, "_"sv, true },
|
||||||
|
{ "////"sv, "____"sv, true },
|
||||||
|
{ "\\\\"sv, "__"sv, true },
|
||||||
|
{ "/../"sv, "_.._"sv, true },
|
||||||
|
{ "foo<bar:baz/boo"sv, "foo_bar_baz_boo"sv, true },
|
||||||
|
{ "t\0e\x01s\tt\ri\nn\fg"sv, "t_e_s_t_i_n_g"sv, true },
|
||||||
|
// appended with '_'
|
||||||
|
{ "con"sv, "con_"sv, true },
|
||||||
|
{ "cOm4"sv, "cOm4_"sv, true },
|
||||||
|
{ "LPt9.txt"sv, "LPt9_.txt"sv, true },
|
||||||
|
{ "NUL.tar.gz"sv, "NUL_.tar.gz"sv, true },
|
||||||
|
// trimmed
|
||||||
|
{ " foo"sv, "foo"sv, true },
|
||||||
|
{ "foo "sv, "foo"sv, true },
|
||||||
|
{ " foo "sv, "foo"sv, true },
|
||||||
|
{ "foo."sv, "foo"sv, true },
|
||||||
|
{ "foo..."sv, "foo"sv, true },
|
||||||
|
{ " foo... "sv, "foo"sv, true },
|
||||||
|
// unmodified
|
||||||
|
{ "foo"sv, "foo"sv, false },
|
||||||
|
{ ".foo"sv, ".foo"sv, false },
|
||||||
|
{ "..foo"sv, "..foo"sv, false },
|
||||||
|
{ "foo.bar.baz"sv, "foo.bar.baz"sv, false },
|
||||||
|
{ "null"sv, "null"sv, false },
|
||||||
|
{ "compass"sv, "compass"sv, false },
|
||||||
|
};
|
||||||
|
|
||||||
|
auto out = std::string{};
|
||||||
|
auto is_adjusted = bool{};
|
||||||
|
for (auto const& test : tests)
|
||||||
|
{
|
||||||
|
out.clear();
|
||||||
|
auto const success = tr_metainfoAppendSanitizedPathComponent(out, test.input, &is_adjusted);
|
||||||
|
EXPECT_EQ(!std::empty(out), success);
|
||||||
|
EXPECT_EQ(test.expected_output, out);
|
||||||
|
EXPECT_EQ(test.expected_is_adjusted, is_adjusted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TorrentMetainfo, AndroidTorrent)
|
||||||
|
{
|
||||||
|
auto const filename = tr_strvJoin(LIBTRANSMISSION_TEST_ASSETS_DIR, "/Android-x86 8.1 r6 iso.torrent"sv);
|
||||||
|
|
||||||
|
auto* ctor = tr_ctorNew(nullptr);
|
||||||
|
auto const err = tr_ctorSetMetainfoFromFile(ctor, filename.c_str());
|
||||||
|
EXPECT_EQ(0, err);
|
||||||
|
tr_ctorFree(ctor);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TorrentMetainfo, ctorSaveContents)
|
||||||
|
{
|
||||||
|
auto const src_filename = tr_strvJoin(LIBTRANSMISSION_TEST_ASSETS_DIR, "/Android-x86 8.1 r6 iso.torrent"sv);
|
||||||
|
auto const tgt_filename = tr_strvJoin(::testing::TempDir(), "save-contents-test.torrent");
|
||||||
|
|
||||||
|
// try saving without passing any metainfo.
|
||||||
|
auto* ctor = tr_ctorNew(nullptr);
|
||||||
|
tr_error* error = nullptr;
|
||||||
|
EXPECT_FALSE(tr_ctorSaveContents(ctor, tgt_filename.c_str(), &error));
|
||||||
|
EXPECT_NE(nullptr, error);
|
||||||
|
if (error != nullptr)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(EINVAL, error->code);
|
||||||
|
tr_error_clear(&error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now try saving _with_ metainfo
|
||||||
|
EXPECT_EQ(0, tr_ctorSetMetainfoFromFile(ctor, src_filename.c_str()));
|
||||||
|
EXPECT_TRUE(tr_ctorSaveContents(ctor, tgt_filename.c_str(), &error));
|
||||||
|
EXPECT_EQ(nullptr, error);
|
||||||
|
|
||||||
|
// the saved contents should match the source file's contents
|
||||||
|
auto src_contents = std::vector<char>{};
|
||||||
|
EXPECT_TRUE(tr_loadFile(src_contents, src_filename.c_str(), &error));
|
||||||
|
auto tgt_contents = std::vector<char>{};
|
||||||
|
EXPECT_TRUE(tr_loadFile(tgt_contents, tgt_filename.c_str(), &error));
|
||||||
|
EXPECT_EQ(src_contents, tgt_contents);
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
EXPECT_TRUE(tr_sys_path_remove(tgt_filename.c_str(), &error));
|
||||||
|
EXPECT_EQ(nullptr, error);
|
||||||
|
tr_error_clear(&error);
|
||||||
|
tr_ctorFree(ctor);
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue