From 7c87cb36eb47123cd7a8022b1cce3c687caf001d Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Sat, 25 Dec 2021 15:21:13 -0600 Subject: [PATCH] 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. --- Transmission.xcodeproj/project.pbxproj | 8 + libtransmission/CMakeLists.txt | 2 + libtransmission/magnet-metainfo.cc | 44 +- libtransmission/magnet-metainfo.h | 53 +- libtransmission/torrent-metainfo.cc | 699 ++++++++++++++++++ libtransmission/torrent-metainfo.h | 167 +++++ libtransmission/torrent.cc | 2 +- libtransmission/tr-dht.cc | 4 +- tests/libtransmission/CMakeLists.txt | 3 +- tests/libtransmission/magnet-metainfo-test.cc | 12 +- .../libtransmission/torrent-metainfo-test.cc | 185 +++++ 11 files changed, 1138 insertions(+), 41 deletions(-) create mode 100644 libtransmission/torrent-metainfo.cc create mode 100644 libtransmission/torrent-metainfo.h create mode 100644 tests/libtransmission/torrent-metainfo-test.cc diff --git a/Transmission.xcodeproj/project.pbxproj b/Transmission.xcodeproj/project.pbxproj index 80f952707..44dc4a453 100644 --- a/Transmission.xcodeproj/project.pbxproj +++ b/Transmission.xcodeproj/project.pbxproj @@ -38,6 +38,8 @@ 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 */; }; 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 */; }; 4DB74F080E8CD75100AEB1A8 /* wildmat.c in Sources */ = {isa = PBXBuildFile; fileRef = 4DB74F070E8CD75100AEB1A8 /* wildmat.c */; }; 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 = ""; }; 4D80185710BBC0B0008A4AF2 /* magnet-metainfo.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "magnet-metainfo.cc"; sourceTree = ""; }; 4D80185810BBC0B0008A4AF2 /* magnet-metainfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "magnet-metainfo.h"; sourceTree = ""; }; + 0A89346B736DBCF81F3A4851 /* torrent-metainfo.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "torrent-metainfo.cc"; sourceTree = ""; }; + 0A89346B736DBCF81F3A4853 /* torrent-metainfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "torrent-metainfo.h"; sourceTree = ""; }; 4DB74F070E8CD75100AEB1A8 /* wildmat.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = wildmat.c; sourceTree = ""; }; 4DCCBB3C09C3D71100D3CABF /* TorrentCell.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = TorrentCell.mm; sourceTree = ""; }; 4DCCBB3D09C3D71100D3CABF /* TorrentCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TorrentCell.h; sourceTree = ""; }; @@ -1390,6 +1394,8 @@ C11DEA151FCD31C0009E22B9 /* subprocess.h */, 4D80185710BBC0B0008A4AF2 /* magnet-metainfo.cc */, 4D80185810BBC0B0008A4AF2 /* magnet-metainfo.h */, + 0A89346B736DBCF81F3A4851 /* torrent-metainfo.cc */, + 0A89346B736DBCF81F3A4853 /* torrent-metainfo.h */, 4D8017E810BBC073008A4AF2 /* torrent-magnet.cc */, 4D8017E910BBC073008A4AF2 /* torrent-magnet.h */, 0A6169A50FE5C9A200C66CE6 /* bitfield.cc */, @@ -1882,6 +1888,7 @@ ED8A16412735A8AA000D61F9 /* peer-mgr-wishlist.h in Headers */, 4D8017EB10BBC073008A4AF2 /* torrent-magnet.h in Headers */, 4D80185A10BBC0B0008A4AF2 /* magnet-metainfo.h in Headers */, + 0A89346B736DBCF81F3A4852 /* torrent-metainfo.h in Headers */, A209EE5D1144B51E002B02D1 /* history.h in Headers */, A247A443114C701800547DFC /* InfoViewController.h in Headers */, A220EC5C118C8A060022B4BE /* tr-lpd.h in Headers */, @@ -2479,6 +2486,7 @@ 66F977825E65AD498C028BB0 /* announce-list.cc in Sources */, 4D8017EA10BBC073008A4AF2 /* torrent-magnet.cc in Sources */, 4D80185910BBC0B0008A4AF2 /* magnet-metainfo.cc in Sources */, + 0A89346B736DBCF81F3A4850 /* torrent-metainfo.cc in Sources */, A220EC5B118C8A060022B4BE /* tr-lpd.cc in Sources */, C1FEE57A1C3223CC00D62832 /* watchdir.cc in Sources */, A23547E211CD0B090046EAE6 /* cache.cc in Sources */, diff --git a/libtransmission/CMakeLists.txt b/libtransmission/CMakeLists.txt index b0f682797..9991e196c 100644 --- a/libtransmission/CMakeLists.txt +++ b/libtransmission/CMakeLists.txt @@ -58,6 +58,7 @@ set(PROJECT_FILES subprocess-win32.cc torrent-ctor.cc torrent-magnet.cc + torrent-metainfo.cc torrent.cc tr-assert.cc tr-dht.cc @@ -183,6 +184,7 @@ set(${PROJECT_NAME}_PRIVATE_HEADERS stats.h subprocess.h torrent-magnet.h + torrent-metainfo.h torrent.h tr-dht.h tr-lpd.h diff --git a/libtransmission/magnet-metainfo.cc b/libtransmission/magnet-metainfo.cc index 0e3b99f35..acf6f6970 100644 --- a/libtransmission/magnet-metainfo.cc +++ b/libtransmission/magnet-metainfo.cc @@ -26,6 +26,8 @@ using namespace std::literals; /* this base32 code converted from code by Robert Kaye and Gordon Mohr * and is public domain. see http://bitzi.com/publicdomain for more info */ +namespace +{ namespace bitzi { @@ -106,11 +108,17 @@ void base32_to_sha1(uint8_t* out, char const* in, size_t const inlen) } } // namespace bitzi +} // namespace /*** **** ***/ +void tr_magnet_metainfo::clear() +{ + *this = tr_magnet_metainfo{}; +} + std::string tr_magnet_metainfo::magnet() const { auto s = std::string{}; @@ -118,19 +126,19 @@ std::string tr_magnet_metainfo::magnet() const s += "magnet:?xt=urn:btih:"sv; s += infoHashString(); - if (!std::empty(name)) + if (!std::empty(name_)) { 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; tr_http_escape(s, tracker.announce.full, true); } - for (auto const& webseed : webseed_urls) + for (auto const& webseed : webseed_urls_) { s += "&ws="sv; 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) { - this->name = tr_urlPercentDecode(value); + this->setName(tr_urlPercentDecode(value)); } else if (key == "tr"sv || tr_strvStartsWith(key, "tr."sv)) { // "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) { @@ -166,7 +174,7 @@ bool tr_magnet_metainfo::parseMagnet(std::string_view magnet_link, tr_error** er auto const url_sv = tr_strvStrip(url); if (tr_urlIsValid(url_sv)) { - this->webseed_urls.emplace_back(url_sv); + this->webseed_urls_.emplace_back(url_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)) { case TR_SHA1_DIGEST_STRLEN: - this->info_hash = tr_sha1_from_string(hash); + this->info_hash_ = tr_sha1_from_string(hash); got_checksum = true; break; case 32: bitzi::base32_to_sha1( - reinterpret_cast(std::data(this->info_hash)), + reinterpret_cast(std::data(this->info_hash_)), std::data(hash), std::size(hash)); 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; } @@ -205,10 +215,10 @@ void tr_magnet_metainfo::toVariant(tr_variant* top) const tr_variantInitDict(top, 4); // announce list - auto n = std::size(this->announce_list); + auto n = std::size(this->announceList()); 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 { @@ -216,7 +226,7 @@ void tr_magnet_metainfo::toVariant(tr_variant* top) const tr_variant* tracker_list = nullptr; 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) { @@ -229,12 +239,12 @@ void tr_magnet_metainfo::toVariant(tr_variant* top) const } // webseeds - n = std::size(this->webseed_urls); + n = std::size(this->webseeds()); if (n != 0) { 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); } @@ -243,10 +253,10 @@ void tr_magnet_metainfo::toVariant(tr_variant* top) const // nonstandard keys 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()); } } diff --git a/libtransmission/magnet-metainfo.h b/libtransmission/magnet-metainfo.h index dc430f732..744d803c2 100644 --- a/libtransmission/magnet-metainfo.h +++ b/libtransmission/magnet-metainfo.h @@ -19,33 +19,58 @@ #include "transmission.h" #include "announce-list.h" -#include "quark.h" -#include "tr-macros.h" struct tr_error; struct tr_variant; -struct tr_magnet_metainfo +class tr_magnet_metainfo { +public: bool parseMagnet(std::string_view magnet_link, tr_error** error = nullptr); - std::string magnet() const; + virtual ~tr_magnet_metainfo() = default; - void toVariant(tr_variant*) const; - - std::string_view infoHashString() const + auto const& infoHash() const { - // trim one byte off the end because of zero termination - return std::string_view{ std::data(info_hash_chars), std::size(info_hash_chars) - 1 }; + return info_hash_; + } + 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 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 webseed_urls_; + tr_sha1_digest_t info_hash_; + std::string info_hash_str_; + std::string name_; }; diff --git a/libtransmission/torrent-metainfo.cc b/libtransmission/torrent-metainfo.cc new file mode 100644 index 000000000..b8c640c7a --- /dev/null +++ b/libtransmission/torrent-metainfo.cc @@ -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 +#include +#include +#include +#include +#include + +#include // 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{ + "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(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* contents, tr_error** error) +{ + auto local_contents = std::vector{}; + + 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]; +} diff --git a/libtransmission/torrent-metainfo.h b/libtransmission/torrent-metainfo.h new file mode 100644 index 000000000..a0185e812 --- /dev/null +++ b/libtransmission/torrent-metainfo.h @@ -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 +#include +#include +#include + +#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* 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 pieces_; + std::vector 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; +}; diff --git a/libtransmission/torrent.cc b/libtransmission/torrent.cc index 6f1b2220b..c494738e0 100644 --- a/libtransmission/torrent.cc +++ b/libtransmission/torrent.cc @@ -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) { 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) diff --git a/libtransmission/tr-dht.cc b/libtransmission/tr-dht.cc index 240bc3e46..22de6d966 100644 --- a/libtransmission/tr-dht.cc +++ b/libtransmission/tr-dht.cc @@ -316,7 +316,7 @@ int tr_dhtInit(tr_session* ss) auto const dat_file = tr_strvPath(ss->configDir, "dht.dat"sv); 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; uint8_t* nodes = nullptr; @@ -466,7 +466,7 @@ void tr_dhtUninit(tr_session* ss) } 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); } diff --git a/tests/libtransmission/CMakeLists.txt b/tests/libtransmission/CMakeLists.txt index e579b63f1..e28a7370b 100644 --- a/tests/libtransmission/CMakeLists.txt +++ b/tests/libtransmission/CMakeLists.txt @@ -9,8 +9,8 @@ add_executable(libtransmission-test crypto-test-ref.h crypto-test.cc error-test.cc - file-test.cc file-piece-map-test.cc + file-test.cc getopt-test.cc history-test.cc json-test.cc @@ -28,6 +28,7 @@ add_executable(libtransmission-test subprocess-test-script.cmd subprocess-test.cc test-fixtures.h + torrent-metainfo-test.cc utils-test.cc variant-test.cc watchdir-test.cc diff --git a/tests/libtransmission/magnet-metainfo-test.cc b/tests/libtransmission/magnet-metainfo-test.cc index eb6e6f14a..fd74c6f59 100644 --- a/tests/libtransmission/magnet-metainfo-test.cc +++ b/tests/libtransmission/magnet-metainfo-test.cc @@ -46,8 +46,8 @@ TEST(MagnetMetainfo, magnetParse) auto mm = tr_magnet_metainfo{}; EXPECT_TRUE(mm.parseMagnet(uri)); - EXPECT_EQ(2, std::size(mm.announce_list)); - auto it = std::begin(mm.announce_list); + EXPECT_EQ(2, std::size(mm.announceList())); + auto it = std::begin(mm.announceList()); EXPECT_EQ(0, it->tier); EXPECT_EQ("http://tracker.openbittorrent.com/announce"sv, it->announce.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("http://tracker.opentracker.org/announce", it->announce.full); EXPECT_EQ("http://tracker.opentracker.org/scrape", it->scrape.full); - EXPECT_EQ(1, std::size(mm.webseed_urls)); - EXPECT_EQ("http://server.webseed.org/path/to/file"sv, mm.webseed_urls.front()); - EXPECT_EQ("Display Name"sv, mm.name); - EXPECT_EQ(ExpectedHash, mm.info_hash); + EXPECT_EQ(1, std::size(mm.webseeds())); + EXPECT_EQ("http://server.webseed.org/path/to/file"sv, mm.webseeds().front()); + EXPECT_EQ("Display Name"sv, mm.name()); + EXPECT_EQ(ExpectedHash, mm.infoHash()); } } diff --git a/tests/libtransmission/torrent-metainfo-test.cc b/tests/libtransmission/torrent-metainfo-test.cc new file mode 100644 index 000000000..c9cd4eae8 --- /dev/null +++ b/tests/libtransmission/torrent-metainfo-test.cc @@ -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 +#include +#include +#include + +#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:ÞÉ`âM‘‹Šs¡Å;˺¬.åÂà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{ { + { 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{ + // 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 }, + { "foocode); + 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{}; + EXPECT_TRUE(tr_loadFile(src_contents, src_filename.c_str(), &error)); + auto tgt_contents = std::vector{}; + 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); +}