From fe288b45e500ebfdcfe4ca3b98572f119f64b035 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Mon, 23 May 2022 23:05:16 -0500 Subject: [PATCH] perf: do not use tr_variant when parsing .torrent files --- libtransmission/benc.h | 14 + libtransmission/file.h | 6 - libtransmission/magnet-metainfo.cc | 18 + libtransmission/magnet-metainfo.h | 4 + libtransmission/torrent-files.h | 17 +- libtransmission/torrent-metainfo.cc | 777 +++++++++++------- libtransmission/torrent-metainfo.h | 12 +- libtransmission/utils.cc | 2 +- libtransmission/web-utils.cc | 26 +- .../assets/webseed-getright-list.torrent | 1 + .../assets/webseed-getright-string.torrent | 1 + tests/libtransmission/file-test.cc | 29 +- .../libtransmission/torrent-metainfo-test.cc | 33 + tests/utils/CMakeLists.txt | 5 +- tests/utils/assets/bittorrent-v2-test.show | 31 + tests/utils/assets/bittorrent-v2-test.torrent | Bin 0 -> 13592 bytes tests/utils/run_transmission_show.cmake | 2 +- utils/show.cc | 18 +- 18 files changed, 646 insertions(+), 350 deletions(-) create mode 100644 tests/libtransmission/assets/webseed-getright-list.torrent create mode 100644 tests/libtransmission/assets/webseed-getright-string.torrent create mode 100644 tests/utils/assets/bittorrent-v2-test.show create mode 100644 tests/utils/assets/bittorrent-v2-test.torrent diff --git a/libtransmission/benc.h b/libtransmission/benc.h index 7534b76fd..f332a0357 100644 --- a/libtransmission/benc.h +++ b/libtransmission/benc.h @@ -10,6 +10,7 @@ #include // size_t #include // int64_t #include +#include #include #include // make_pair @@ -129,6 +130,19 @@ struct BasicHandler : public Handler return key(depth()); } +protected: + [[nodiscard]] std::string path() const + { + auto ret = std::string{}; + for (size_t i = 0; i <= depth(); ++i) + { + ret += '['; + ret += key(i); + ret += ']'; + } + return ret; + } + private: constexpr void push() noexcept { diff --git a/libtransmission/file.h b/libtransmission/file.h index d15b35421..faea6b442 100644 --- a/libtransmission/file.h +++ b/libtransmission/file.h @@ -161,12 +161,6 @@ bool tr_sys_path_copy(char const* src_path, char const* dst_path, struct tr_erro */ bool tr_sys_path_get_info(char const* path, int flags, tr_sys_path_info* info, struct tr_error** error = nullptr); -template::value>> -bool tr_sys_path_get_info(T const& path, int flags, tr_sys_path_info* info, struct tr_error** error = nullptr) -{ - return tr_sys_path_get_info(path.c_str(), flags, info, error); -} - /** * @brief Portability wrapper for `access()`. * diff --git a/libtransmission/magnet-metainfo.cc b/libtransmission/magnet-metainfo.cc index 206afba5b..7bb0fc7f0 100644 --- a/libtransmission/magnet-metainfo.cc +++ b/libtransmission/magnet-metainfo.cc @@ -3,6 +3,7 @@ // or any future license endorsed by Mnemosyne LLC. // License text can be found in the licenses/ folder. +#include #include #include #include // back_inserter @@ -181,6 +182,23 @@ tr_urlbuf tr_magnet_metainfo::magnet() const return s; } +void tr_magnet_metainfo::addWebseed(std::string_view webseed) +{ + if (!tr_urlIsValid(webseed)) + { + return; + } + + auto& urls = webseed_urls_; + auto const it = std::find(std::begin(urls), std::end(urls), webseed); + if (it != std::end(urls)) + { + return; + } + + urls.emplace_back(std::string{ webseed }); +} + bool tr_magnet_metainfo::parseMagnet(std::string_view magnet_link, tr_error** error) { magnet_link = tr_strvStrip(magnet_link); diff --git a/libtransmission/magnet-metainfo.h b/libtransmission/magnet-metainfo.h index 5cc960c73..e34ea067d 100644 --- a/libtransmission/magnet-metainfo.h +++ b/libtransmission/magnet-metainfo.h @@ -20,6 +20,8 @@ struct tr_variant; class tr_magnet_metainfo { + friend struct MetainfoHandler; + public: bool parseMagnet(std::string_view magnet_link, tr_error** error = nullptr); @@ -65,6 +67,8 @@ public: name_ = name; } + void addWebseed(std::string_view webseed); + protected: tr_announce_list announce_list_; std::vector webseed_urls_; diff --git a/libtransmission/torrent-files.h b/libtransmission/torrent-files.h index 4133de3ad..7fe09b67f 100644 --- a/libtransmission/torrent-files.h +++ b/libtransmission/torrent-files.h @@ -58,6 +58,17 @@ public: files_.at(file_index).setPath(path); } + void insertSubpathPrefix(std::string_view path) + { + auto const buf = tr_pathbuf{ path, '/' }; + + for (auto& file : files_) + { + file.path_.insert(0, buf.sv()); + file.path_.shrink_to_fit(); + } + } + void reserve(size_t n_files) { files_.reserve(n_files); @@ -165,7 +176,11 @@ private: public: void setPath(std::string_view subpath) { - path_ = subpath; + if (path_ != subpath) + { + path_ = subpath; + path_.shrink_to_fit(); + } } file_t(std::string_view path, uint64_t size) diff --git a/libtransmission/torrent-metainfo.cc b/libtransmission/torrent-metainfo.cc index 40504038c..171067142 100644 --- a/libtransmission/torrent-metainfo.cc +++ b/libtransmission/torrent-metainfo.cc @@ -16,6 +16,7 @@ #include "transmission.h" +#include "benc.h" #include "crypto-utils.h" #include "error-types.h" #include "error.h" @@ -24,8 +25,8 @@ #include "quark.h" #include "torrent-metainfo.h" #include "tr-assert.h" +#include "tr-strbuf.h" #include "utils.h" -#include "variant.h" #include "web-utils.h" using namespace std::literals; @@ -155,329 +156,512 @@ std::string tr_torrent_metainfo::fixWebseedUrl(tr_torrent_metainfo const& tm, st return std::string{ url }; } -void tr_torrent_metainfo::parseWebseeds(tr_torrent_metainfo& setme, tr_variant* meta) -{ - setme.webseed_urls_.clear(); +static auto constexpr MaxBencDepth = 32; - 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)); - } +bool tr_error_is_set(tr_error const* const* error) +{ + return (error != nullptr) && (*error != nullptr); } -bool tr_torrent_metainfo::parsePath(std::string_view root, tr_variant* path, std::string& setme) +struct MetainfoHandler final : public transmission::benc::BasicHandler { - if (!tr_variantIsList(path)) + using BasicHandler = transmission::benc::BasicHandler; + + tr_torrent_metainfo& tm_; + int64_t piece_size_ = 0; + int64_t length_ = 0; + std::string encoding_ = "UTF-8"; + std::string_view info_dict_begin_; + tr_tracker_tier_t tier_ = 0; + tr_pathbuf file_subpath_; + std::string_view pieces_root_; + int64_t file_length_ = 0; + + enum class State + { + UsePath, + FileTree, + Files, + FilesIgnored, + PieceLayers, + }; + State state_ = State::UsePath; + + explicit MetainfoHandler(tr_torrent_metainfo& tm) + : tm_{ tm } { - return false; } - setme = root; - - for (size_t i = 0, n = tr_variantListSize(path); i < n; ++i) + bool Key(std::string_view key, Context const& context) override { - auto raw = std::string_view{}; + return BasicHandler::Key(key, context); + } - if (!tr_variantGetStrView(tr_variantListChild(path, i), &raw)) + bool StartDict(Context const& context) override + { + if (state_ == State::FileTree) { + if (!std::empty(file_subpath_)) + { + file_subpath_ += '/'; + } + tr_torrent_files::makeSubpathPortable(currentKey(), file_subpath_); + } + else if (pathIs(InfoKey)) + { + info_dict_begin_ = context.raw(); + tm_.info_dict_offset_ = context.tokenSpan().first; + } + else if (pathIs(InfoKey, FileTreeKey)) + { + state_ = State::FileTree; + file_subpath_.clear(); + file_length_ = 0; + } + else if (pathIs(PieceLayersKey)) + { + state_ = State::PieceLayers; + } + + return BasicHandler::StartDict(context); + } + + bool EndDict(Context const& context) override + { + BasicHandler::EndDict(context); + + if (pathIs(InfoKey)) + { + return finishInfoDict(context); + } + + if (depth() == 0) // top + { + return finish(context); + } + + if (state_ == State::FileTree) // bittorrent v2 format + { + if (!addFile(context)) + { + return false; + } + + file_subpath_.popdir(); + if (file_subpath_ == "."sv) + { + file_subpath_.clear(); + } + + if (pathIs(InfoKey, FileTreeKey)) + { + state_ = State::UsePath; + } + } + else if (state_ == State::Files) // bittorrent v1 format + { + if (!addFile(context)) + { + return false; + } + + file_subpath_.clear(); + } + + return depth() > 0; + } + + bool StartArray(Context const& context) override + { + if (pathIs(InfoKey, FilesKey)) + { + state_ = std::empty(tm_.files_) ? State::Files : State::FilesIgnored; + file_subpath_.clear(); + file_length_ = 0; + } + + return BasicHandler::StartArray(context); + } + + bool EndArray(Context const& context) override + { + BasicHandler::EndArray(context); + + if ((state_ == State::Files || state_ == State::FilesIgnored) && currentKey() == FilesKey) // bittorrent v1 format + { + state_ = State::UsePath; + return true; + } + + if (depth() == 2 && key(1) == AnnounceListKey) + { + ++tier_; + } + + return true; + } + + bool Int64(int64_t value, Context const& /*context*/) override + { + auto unhandled = bool{ false }; + + if (state_ == State::FilesIgnored) + { + // no-op + } + else if (state_ == State::FileTree || state_ == State::Files) + { + if (currentKey() == LengthKey) + { + file_length_ = value; + } + else if (pathIs(InfoKey, FilesKey, ""sv, MtimeKey)) + { + // unused by Transmission + } + else + { + unhandled = true; + } + } + else if (pathIs(CreationDateKey)) + { + tm_.date_created_ = value; + } + else if (pathIs(PrivateKey) || pathIs(InfoKey, PrivateKey)) + { + tm_.is_private_ = value != 0; + } + else if (pathIs(PieceLengthKey) || pathIs(InfoKey, PieceLengthKey)) + { + piece_size_ = value; + } + else if (pathIs(InfoKey, LengthKey)) + { + length_ = value; + } + else if (pathIs(InfoKey, MetaVersionKey)) + { + // currently unused. TODO support for bittorrent v2 + // TODO https://github.com/transmission/transmission/issues/458 + } + else if ( + pathIs(DurationKey) || pathIs(EncodedRateKey) || pathIs(HeightKey) || pathIs(InfoKey, EntropyKey) || + pathIs(ProfilesKey, HeightKey) || pathIs(ProfilesKey, WidthKey) || pathIs(WidthKey) || + pathStartsWith(AzureusPropertiesKey) || pathStartsWith(InfoKey, FileDurationKey) || + pathStartsWith(InfoKey, FileMediaKey) || pathStartsWith(InfoKey, ProfilesKey)) + { + // unused by Transmission + } + else + { + unhandled = true; + } + + if (unhandled) + { + tr_logAddWarn(fmt::format("unexpected: path '{}', int '{}'", path(), value)); + } + + return true; + } + + bool String(std::string_view value, Context const& context) override + { + auto const curdepth = depth(); + auto const current_key = currentKey(); + auto unhandled = bool{ false }; + + if (state_ == State::FilesIgnored) + { + // no-op + } + else if (state_ == State::FileTree) + { + if (current_key == AttrKey || current_key == PiecesRootKey) + { + // currently unused. TODO support for bittorrent v2 + // TODO https://github.com/transmission/transmission/issues/458 + } + else + { + unhandled = true; + } + } + else if (state_ == State::Files) + { + if (curdepth > 1 && key(curdepth - 1) == PathKey) + { + if (!std::empty(file_subpath_)) + { + file_subpath_ += '/'; + } + tr_torrent_files::makeSubpathPortable(value, file_subpath_); + } + else if (current_key == AttrKey) + { + // currently unused. TODO support for bittorrent v2 + // TODO https://github.com/transmission/transmission/issues/458 + } + else + { + unhandled = true; + } + } + else if (pathIs(CommentKey) || pathIs(CommentUtf8Key)) + { + tr_strvUtf8Clean(value, tm_.comment_); + } + else if (pathIs(CreatedByKey) || pathIs(CreatedByUtf8Key)) + { + tr_strvUtf8Clean(value, tm_.creator_); + } + else if (pathIs(SourceKey) || pathIs(InfoKey, SourceKey) || pathIs(PublisherKey) || pathIs(InfoKey, PublisherKey)) + { + // “publisher” is rare, but used by BitComet and appears + // to have the same use as the 'source' key + // http://wiki.bitcomet.com/inside_bitcomet + + tr_strvUtf8Clean(value, tm_.source_); + } + else if (pathIs(AnnounceKey)) + { + tm_.announceList().add(value, tier_); + } + else if (pathIs(EncodingKey)) + { + encoding_ = tr_strvStrip(value); + } + else if (pathIs(UrlListKey)) + { + tm_.addWebseed(value); + } + else if (pathIs(InfoKey, NameKey) || pathIs(InfoKey, NameUtf8Key)) + { + tr_strvUtf8Clean(value, tm_.name_); + } + else if (pathIs(InfoKey, PiecesKey)) + { + auto const n = std::size(value) / sizeof(tr_sha1_digest_t); + tm_.pieces_.resize(n); + std::copy_n(std::data(value), std::size(value), reinterpret_cast(std::data(tm_.pieces_))); + tm_.pieces_offset_ = context.tokenSpan().first; + } + else if (pathStartsWith(PieceLayersKey)) + { + // currently unused. TODO support for bittorrent v2 + // TODO https://github.com/transmission/transmission/issues/458 + } + else if (pathStartsWith(AnnounceListKey)) + { + tm_.announceList().add(value, tier_); + } + else if ( + pathIs(ChecksumKey) || pathIs(InfoKey, FilesKey, ""sv, MtimeKey) || pathIs(InfoKey, PublisherUrlKey) || + pathIs(PublisherUrlKey) || pathStartsWith(AzureusPropertiesKey) || pathStartsWith(InfoKey, ProfilesKey)) + { + // unused by Transmission + } + else if (curdepth == 2 && (pathStartsWith(HttpSeedsKey) || pathStartsWith(UrlListKey))) + { + tm_.addWebseed(value); + } + else + { + unhandled = true; + } + + if (unhandled) + { + tr_logAddWarn(fmt::format("unexpected: path '{}', str '{}'", path(), value)); + } + + return true; + } + +private: + template + [[nodiscard]] bool pathStartsWith(Args... args) const noexcept + { + auto i = 1U; + return (depth() >= sizeof...(args)) && ((key(i++) == args) && ...); + } + + template + [[nodiscard]] bool pathIs(Args... args) const noexcept + { + auto i = 1U; + return (depth() == sizeof...(args)) && ((key(i++) == args) && ...); + } + + [[nodiscard]] bool addFile(Context const& context) + { + bool ok = true; + + if (file_length_ == 0) + { + return ok; + } + + // FIXME: Check to see if we already added this file. This is a safeguard + // for hybrid torrents with duplicate info between "file tree" and "files" + if (std::empty(file_subpath_)) + { + tr_error_set(context.error, EINVAL, fmt::format("invalid path [{:s}]", file_subpath_)); + ok = false; + } + else + { + tm_.files_.add(file_subpath_, file_length_); + } + + file_length_ = 0; + pieces_root_ = {}; + // NB: let caller decide how to clear file_tree_. + // if we're in "files" mode we clear it; if in "file tree" we pop it + return ok; + } + + bool finishInfoDict(Context const& context) + { + if (std::empty(info_dict_begin_)) + { + tr_error_set(context.error, EINVAL, "no info_dict found"); return false; } - if (!std::empty(raw)) + auto root = tr_pathbuf{}; + tr_torrent_files::makeSubpathPortable(tm_.name_, root); + if (!std::empty(root)) { - setme += TR_PATH_DELIMITER; - setme += raw; + tm_.files_.insertSubpathPrefix(root); } - } - auto const sanitized = tr_torrent_files::makeSubpathPortable(setme); - - if (std::size(sanitized) <= std::size(root)) - { - return false; - } - - tr_strvUtf8Clean(sanitized, setme); - return true; -} - -std::string_view tr_torrent_metainfo::parseFiles(tr_torrent_metainfo& setme, tr_variant* info_dict, uint64_t* setme_total_size) -{ - auto total_size = uint64_t{ 0 }; - - setme.files_.clear(); - - auto const root_name = tr_torrent_files::makeSubpathPortable(setme.name_); - - if (std::empty(root_name)) - { - 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_.add(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{}; - buf.reserve(1024); // arbitrary - auto const n_files = size_t{ tr_variantListSize(files_entry) }; - setme.files_.reserve(n_files); - 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"; - } - - if (!parsePath(root_name, path_variant, buf)) - { - return "path"; - } - - setme.files_.add(buf, 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] ] - if (tr_variant* tiers = nullptr; 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(url, i); - } - } - } - - // single 'announce' url - if (std::empty(setme.announce_list_) && tr_variantDictFindStrView(meta, TR_KEY_announce, &url)) - { - setme.announce_list_.add(url, 0); - } - - 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. - auto const info_dict_benc = tr_variantToStr(info_dict, TR_VARIANT_FMT_BENC); + TR_ASSERT(info_dict_begin_[0] == 'd'); + TR_ASSERT(context.raw().back() == 'e'); + char const* const begin = &info_dict_begin_.front(); + char const* const end = &context.raw().back() + 1; + auto const info_dict_benc = std::string_view{ begin, size_t(end - begin) }; auto const hash = tr_sha1(info_dict_benc); if (!hash) { - return "bad info_dict checksum"; + tr_error_set(context.error, EINVAL, "bad info_dict checksum"); } - setme.info_hash_ = *hash; - setme.info_hash_str_ = tr_sha1_to_string(setme.info_hash_); + tm_.info_hash_ = *hash; + tm_.info_hash_str_ = tr_sha1_to_string(tm_.info_hash_); + tm_.info_dict_size_ = std::size(info_dict_benc); - // 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). + return true; + } + + bool finish(Context const& context) + { + // bittorrent 1.0 spec + // http://bittorrent.org/beps/bep_0003.html // - // 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), std::begin(info_dict_benc), std::end(info_dict_benc)); - setme.info_dict_offset_ = std::distance(std::begin(benc), it); - setme.info_dict_size_ = std::size(info_dict_benc); + // "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. + if (tm_.fileCount() == 0 && length_ != 0 && !std::empty(tm_.name_)) + { + tm_.files_.add(tm_.name_, length_); + } - // 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 const pit = std::search(std::begin(benc), std::end(benc), std::begin(Key), std::end(Key)); - setme.pieces_offset_ = std::distance(std::begin(benc), pit) + std::size(Key); - } - else - { - return "missing 'info' dictionary"; + if (tm_.fileCount() == 0) + { + if (!tr_error_is_set(context.error)) + { + tr_error_set(context.error, EINVAL, "no files found"); + } + return false; + } + + if (piece_size_ == 0) + { + if (!tr_error_is_set(context.error)) + { + tr_error_set(context.error, EINVAL, fmt::format("invalid piece size: {}", piece_size_)); + } + return false; + } + + tm_.block_info_.initSizes(tm_.files_.totalSize(), piece_size_); + return true; } - // name - if (tr_variantDictFindStrView(info_dict, TR_KEY_name_utf_8, &sv) || tr_variantDictFindStrView(info_dict, TR_KEY_name, &sv)) - { - tr_strvUtf8Clean(sv, setme.name_); - } - else - { - return "'info' dictionary has neither 'name.utf-8' nor 'name'"; - } - - // comment (optional) - setme.comment_.clear(); - if (tr_variantDictFindStrView(meta, TR_KEY_comment_utf_8, &sv) || tr_variantDictFindStrView(meta, TR_KEY_comment, &sv)) - { - tr_strvUtf8Clean(sv, setme.comment_); - } - - // created by (optional) - setme.creator_.clear(); - if (tr_variantDictFindStrView(meta, TR_KEY_created_by_utf_8, &sv) || - tr_variantDictFindStrView(meta, TR_KEY_created_by, &sv)) - { - tr_strvUtf8Clean(sv, setme.creator_); - } - - // creation date (optional) - setme.date_created_ = tr_variantDictFindInt(meta, TR_KEY_creation_date, &i) ? i : 0; - - // private (optional) - setme.is_private_ = (tr_variantDictFindInt(info_dict, TR_KEY_private, &i) || - tr_variantDictFindInt(meta, TR_KEY_private, &i)) && - (i != 0); - - // source (optional) - setme.source_.clear(); - if (tr_variantDictFindStrView(info_dict, TR_KEY_source, &sv) || tr_variantDictFindStrView(meta, TR_KEY_source, &sv)) - { - tr_strvUtf8Clean(sv, setme.source_); - } - - // piece length - if (!tr_variantDictFindInt(info_dict, TR_KEY_piece_length, &i) || (i <= 0) || (i > UINT32_MAX)) - { - return "'info' dict 'piece length' is missing or has an invalid value"; - } - auto const piece_size = (uint32_t)i; - - // pieces - if (!tr_variantDictFindStrView(info_dict, TR_KEY_pieces, &sv) || (std::size(sv) % sizeof(tr_sha1_digest_t) != 0)) - { - return "'info' dict 'pieces' is missing or has an invalid value"; - } - 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_))); - - // files - auto total_size = uint64_t{ 0 }; - if (auto const errstr = parseFiles(setme, info_dict, &total_size); !std::empty(errstr)) - { - return errstr; - } - - if (std::empty(setme.files_)) - { - 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_.pieceCount() != std::size(setme.pieces_)) - { - return "piece count and file sizes do not match"; - } - - parseAnnounce(setme, meta); - parseWebseeds(setme, meta); - - return {}; -} + static constexpr std::string_view AcodecKey = "acodec"sv; + static constexpr std::string_view AnnounceKey = "announce"sv; + static constexpr std::string_view AnnounceListKey = "announce-list"sv; + static constexpr std::string_view AttrKey = "attr"sv; + static constexpr std::string_view AzureusPropertiesKey = "azureus_properties"sv; + static constexpr std::string_view ChecksumKey = "checksum"sv; + static constexpr std::string_view CommentKey = "comment"sv; + static constexpr std::string_view CommentUtf8Key = "comment.utf-8"sv; + static constexpr std::string_view CreatedByKey = "created by"sv; + static constexpr std::string_view CreatedByUtf8Key = "created by.utf-8"sv; + static constexpr std::string_view CreationDateKey = "creation date"sv; + static constexpr std::string_view DurationKey = "duration"sv; + static constexpr std::string_view EncodedRateKey = "encoded rate"sv; + static constexpr std::string_view EncodingKey = "encoding"sv; + static constexpr std::string_view EntropyKey = "entropy"sv; + static constexpr std::string_view FileDurationKey = "file-duration"sv; + static constexpr std::string_view FileMediaKey = "file-media"sv; + static constexpr std::string_view FileTreeKey = "file tree"sv; + static constexpr std::string_view FilesKey = "files"sv; + static constexpr std::string_view HeightKey = "height"sv; + static constexpr std::string_view HttpSeedsKey = "httpseeds"sv; + static constexpr std::string_view InfoKey = "info"sv; + static constexpr std::string_view LengthKey = "length"sv; + static constexpr std::string_view Md5sumKey = "md5sum"sv; + static constexpr std::string_view MetaVersionKey = "meta version"sv; + static constexpr std::string_view MtimeKey = "mtime"sv; + static constexpr std::string_view NameKey = "name"sv; + static constexpr std::string_view NameUtf8Key = "name.utf-8"sv; + static constexpr std::string_view PathKey = "path"sv; + static constexpr std::string_view PieceLayersKey = "piece layers"sv; + static constexpr std::string_view PieceLengthKey = "piece length"sv; + static constexpr std::string_view PiecesKey = "pieces"sv; + static constexpr std::string_view PiecesRootKey = "pieces root"sv; + static constexpr std::string_view PrivateKey = "private"sv; + static constexpr std::string_view ProfilesKey = "profiles"sv; + static constexpr std::string_view PublisherKey = "publisher"sv; + static constexpr std::string_view PublisherUrlKey = "publisher-url"sv; + static constexpr std::string_view SourceKey = "source"sv; + static constexpr std::string_view UrlListKey = "url-list"sv; + static constexpr std::string_view VcodecKey = "vcodec"sv; + static constexpr std::string_view WidthKey = "width"sv; +}; 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)) + auto stack = transmission::benc::ParserStack{}; + auto handler = MetainfoHandler{ *this }; + + tr_error* my_error = nullptr; + + if (error == nullptr) + { + error = &my_error; + } + auto const ok = transmission::benc::parse(benc, stack, handler, nullptr, error); + + if (tr_error_is_set(error)) + { + tr_logAddError(fmt::format("{} ({})", (*error)->message, (*error)->code)); + } + + tr_error_clear(&my_error); + + if (!ok) { return false; } - auto const errmsg = parseImpl(*this, &top, benc); - tr_variantFree(&top); - if (!std::empty(errmsg)) + if (std::empty(name_)) { - tr_error_set(error, TR_ERROR_EINVAL, fmt::format(FMT_STRING("Error parsing metainfo: {:s}"), errmsg)); - return false; + // TODO from first file } return true; @@ -509,8 +693,8 @@ tr_pathbuf tr_torrent_metainfo::makeFilename( { // `${dirname}/${name}.${info_hash}${suffix}` // `${dirname}/${info_hash}${suffix}` - return format == BasenameFormat::Hash ? tr_pathbuf{ dirname, "/"sv, info_hash_string, suffix } : - tr_pathbuf{ dirname, "/"sv, name, "."sv, info_hash_string.substr(0, 16), suffix }; + return format == BasenameFormat::Hash ? tr_pathbuf{ dirname, '/', info_hash_string, suffix } : + tr_pathbuf{ dirname, '/', name, '.', info_hash_string.substr(0, 16), suffix }; } bool tr_torrent_metainfo::migrateFile( @@ -555,6 +739,9 @@ void tr_torrent_metainfo::removeFile( std::string_view info_hash_string, std::string_view suffix) { - tr_sys_path_remove(makeFilename(dirname, name, info_hash_string, BasenameFormat::NameAndPartialHash, suffix)); - tr_sys_path_remove(makeFilename(dirname, name, info_hash_string, BasenameFormat::Hash, suffix)); + auto filename = makeFilename(dirname, name, info_hash_string, BasenameFormat::NameAndPartialHash, suffix); + tr_sys_path_remove(filename, nullptr); + + filename = makeFilename(dirname, name, info_hash_string, BasenameFormat::Hash, suffix); + tr_sys_path_remove(filename, nullptr); } diff --git a/libtransmission/torrent-metainfo.h b/libtransmission/torrent-metainfo.h index 698168e79..b3cc1300e 100644 --- a/libtransmission/torrent-metainfo.h +++ b/libtransmission/torrent-metainfo.h @@ -54,6 +54,7 @@ public: { return files().path(i); } + void setFileSubpath(tr_file_index_t i, std::string_view subpath) { files_.setPath(i, subpath); @@ -181,12 +182,13 @@ public: std::string_view suffix); private: - static bool parsePath(std::string_view root, tr_variant* path, std::string& setme); + friend struct MetainfoHandler; + static bool parseImpl(tr_torrent_metainfo& setme, std::string_view benc, tr_error** error); + // static bool parsePath(std::string_view root, tr_variant* path, std::string& setme); 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); + // static std::string_view parseFiles(tr_torrent_metainfo& setme, tr_variant* info_dict, uint64_t* setme_total_size); + // static std::string_view parseAnnounce(tr_torrent_metainfo& setme, tr_variant* meta); + // static void parseWebseeds(tr_torrent_metainfo& setme, tr_variant* meta); enum class BasenameFormat { diff --git a/libtransmission/utils.cc b/libtransmission/utils.cc index a0a5a4a71..d0616ddf7 100644 --- a/libtransmission/utils.cc +++ b/libtransmission/utils.cc @@ -149,7 +149,7 @@ uint8_t* tr_loadFile(std::string_view path_in, size_t* size, tr_error** error) /* try to stat the file */ auto info = tr_sys_path_info{}; tr_error* my_error = nullptr; - if (!tr_sys_path_get_info(path, 0, &info, &my_error)) + if (!tr_sys_path_get_info(path.c_str(), 0, &info, &my_error)) { tr_logAddError(fmt::format( _("Couldn't read '{path}': {error} ({error_code})"), diff --git a/libtransmission/web-utils.cc b/libtransmission/web-utils.cc index f6185f8b9..a51f03ab8 100644 --- a/libtransmission/web-utils.cc +++ b/libtransmission/web-utils.cc @@ -21,8 +21,9 @@ #include "transmission.h" #include "net.h" -#include "web-utils.h" +#include "tr-strbuf.h" #include "utils.h" +#include "web-utils.h" using namespace std::literals; @@ -251,6 +252,14 @@ bool tr_isValidTrackerScheme(std::string_view scheme) return std::find(std::begin(Schemes), std::end(Schemes), scheme) != std::end(Schemes); } +bool isAsciiLowerCase(std::string_view host) +{ + return std::all_of( + std::begin(host), + std::end(host), + [](unsigned char ch) { return (ch < 128) && (std::islower(ch) != 0); }); +} + // www.example.com -> example // www.example.co.uk -> example // 127.0.0.1 -> 127.0.0.1 @@ -262,16 +271,25 @@ std::string_view getSiteName(std::string_view host) return host; } + // psl needs a zero-terminated hostname + auto const szhost = tr_urlbuf{ host }; + // is it an IP? auto addr = tr_address{}; - auto const szhost = std::string(host); - if (tr_address_from_string(&addr, szhost.c_str())) + if (tr_address_from_string(&addr, std::data(szhost))) { return host; } // is it a registered name? - if (char* lower = nullptr; psl_str_to_utf8lower(szhost.c_str(), nullptr, nullptr, &lower) == PSL_SUCCESS) + if (isAsciiLowerCase(host)) + { + if (char const* const top = psl_registrable_domain(psl_builtin(), std::data(szhost)); top != nullptr) + { + host.remove_prefix(top - std::data(szhost)); + } + } + else if (char* lower = nullptr; psl_str_to_utf8lower(std::data(szhost), nullptr, nullptr, &lower) == PSL_SUCCESS) { // www.example.com -> example.com if (char const* const top = psl_registrable_domain(psl_builtin(), lower); top != nullptr) diff --git a/tests/libtransmission/assets/webseed-getright-list.torrent b/tests/libtransmission/assets/webseed-getright-list.torrent new file mode 100644 index 000000000..de39b3c16 --- /dev/null +++ b/tests/libtransmission/assets/webseed-getright-list.torrent @@ -0,0 +1 @@ +d8:announce31:http://www.example.com/announce10:created by16:buildtorrent/0.813:creation datei1646282405e4:infod5:filesld6:lengthi14e4:pathl5:worldeee4:name5:hello12:piece lengthi262144e6:pieces20:+U卑V):e8:url-listl27:http://www.webseed-one.com/23:http://webseed-two.com/ee \ No newline at end of file diff --git a/tests/libtransmission/assets/webseed-getright-string.torrent b/tests/libtransmission/assets/webseed-getright-string.torrent new file mode 100644 index 000000000..8e73076f2 --- /dev/null +++ b/tests/libtransmission/assets/webseed-getright-string.torrent @@ -0,0 +1 @@ +d8:announce31:http://www.example.com/announce10:created by16:buildtorrent/0.813:creation datei1646282405e4:infod5:filesld6:lengthi14e4:pathl5:worldeee4:name5:hello12:piece lengthi262144e6:pieces20:+U卑V):e8:url-list27:http://www.webseed-one.com/e diff --git a/tests/libtransmission/file-test.cc b/tests/libtransmission/file-test.cc index 7fd2a3e68..37f8ebd59 100644 --- a/tests/libtransmission/file-test.cc +++ b/tests/libtransmission/file-test.cc @@ -127,7 +127,8 @@ protected: } auto const path_part = std::string{ path, size_t(slash_pos - path + 1) }; - if (!tr_sys_path_get_info(path_part, TR_SYS_PATH_NO_FOLLOW, &info) || + + if (!tr_sys_path_get_info(path_part.c_str(), TR_SYS_PATH_NO_FOLLOW, &info) || (info.type != TR_SYS_PATH_IS_FILE && info.type != TR_SYS_PATH_IS_DIRECTORY)) { return false; @@ -182,32 +183,6 @@ protected: } } - static void testPathXname( - XnameTestData const* data, - size_t data_size, - std::string_view (*func)(std::string_view, tr_error**)) - { - for (size_t i = 0; i < data_size; ++i) - { - tr_error* err = nullptr; - auto const name = func(data[i].input, &err); - std::cerr << __FILE__ << ':' << __LINE__ << " in [" << data[i].input << "] out [" << name << ']' << std::endl; - - if (data[i].output != nullptr) - { - EXPECT_NE(""sv, name); - EXPECT_EQ(nullptr, err) << *err; - EXPECT_EQ(std::string{ data[i].output }, name); - } - else - { - EXPECT_EQ(""sv, name); - EXPECT_NE(nullptr, err); - tr_error_clear(&err); - } - } - } - static void testDirReadImpl(tr_pathbuf const& path, bool* have1, bool* have2) { *have1 = *have2 = false; diff --git a/tests/libtransmission/torrent-metainfo-test.cc b/tests/libtransmission/torrent-metainfo-test.cc index 1495036c0..fa7043b7b 100644 --- a/tests/libtransmission/torrent-metainfo-test.cc +++ b/tests/libtransmission/torrent-metainfo-test.cc @@ -222,5 +222,38 @@ TEST_F(TorrentMetainfoTest, ctorSaveContents) tr_ctorFree(ctor); } +TEST_F(TorrentMetainfoTest, HoffmanStyleWebseeds) +{ + auto const src_filename = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, "/debian-11.2.0-amd64-DVD-1.iso.torrent"sv }; + auto tm = tr_torrent_metainfo{}; + EXPECT_TRUE(tm.parseTorrentFile(src_filename)); + EXPECT_EQ(2, tm.webseedCount()); + EXPECT_EQ( + "https://cdimage.debian.org/cdimage/release/11.2.0//srv/cdbuilder.debian.org/dst/deb-cd/weekly-builds/amd64/iso-dvd/debian-11.2.0-amd64-DVD-1.iso"sv, + tm.webseed(0)); + EXPECT_EQ( + "https://cdimage.debian.org/cdimage/archive/11.2.0//srv/cdbuilder.debian.org/dst/deb-cd/weekly-builds/amd64/iso-dvd/debian-11.2.0-amd64-DVD-1.iso"sv, + tm.webseed(1)); +} + +TEST_F(TorrentMetainfoTest, GetRightStyleWebseedList) +{ + auto const src_filename = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, "/webseed-getright-list.torrent"sv }; + auto tm = tr_torrent_metainfo{}; + EXPECT_TRUE(tm.parseTorrentFile(src_filename)); + EXPECT_EQ(2, tm.webseedCount()); + EXPECT_EQ("http://www.webseed-one.com/"sv, tm.webseed(0)); + EXPECT_EQ("http://webseed-two.com/"sv, tm.webseed(1)); +} + +TEST_F(TorrentMetainfoTest, GetRightStyleWebseedString) +{ + auto const src_filename = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, "/webseed-getright-string.torrent"sv }; + auto tm = tr_torrent_metainfo{}; + EXPECT_TRUE(tm.parseTorrentFile(src_filename)); + EXPECT_EQ(1, tm.webseedCount()); + EXPECT_EQ("http://www.webseed-one.com/"sv, tm.webseed(0)); +} + } // namespace test } // namespace libtransmission diff --git a/tests/utils/CMakeLists.txt b/tests/utils/CMakeLists.txt index 540c4ede0..eb09952cd 100644 --- a/tests/utils/CMakeLists.txt +++ b/tests/utils/CMakeLists.txt @@ -10,6 +10,7 @@ function(AddShowTest name file_basename) ) endfunction() -AddShowTest(transmission-show-ubuntu ubuntu-20.04.3-desktop-amd64.iso) -AddShowTest(transmission-show-thor Thor_and_the_Amazon_Women.avi) +AddShowTest(transmission-show-bittorrent-v2 bittorrent-v2-test) AddShowTest(transmission-show-inner-sanctum Inner_Sanctum_movie_archive) +AddShowTest(transmission-show-thor Thor_and_the_Amazon_Women.avi) +AddShowTest(transmission-show-ubuntu ubuntu-20.04.3-desktop-amd64.iso) diff --git a/tests/utils/assets/bittorrent-v2-test.show b/tests/utils/assets/bittorrent-v2-test.show new file mode 100644 index 000000000..febaea5ad --- /dev/null +++ b/tests/utils/assets/bittorrent-v2-test.show @@ -0,0 +1,31 @@ +Name: bittorrent-v2-test +File: assets/bittorrent-v2-test.torrent + +GENERAL + + Name: bittorrent-v2-test + Hash: f987ab6bb50f831a861c3754ecd1b47dc2cf2e30 + Created by: libtorrent + Created on: Thu May 21 21:40:57 2020 + + Piece Count: 366 + Piece Size: 4.00 MiB + Total Size: 1.53 GB + Privacy: Public torrent + +TRACKERS + +FILES + + bittorrent-v2-test/13.Popsy Team - ViP 2.vob.mp4 (27.55 MB) + bittorrent-v2-test/Chameleon by ASD (female voice).mov (91.86 MB) + bittorrent-v2-test/Darkroom (Stellar, 1994, Amiga ECS) HQ.mp4 (6.54 MB) + bittorrent-v2-test/Struct by Outracks (FullHD 1080p HQ demoscene).mov (141.1 MB) + bittorrent-v2-test/asd-rupture.mp4 (263.7 MB) + bittorrent-v2-test/cncd_fairlight-ceasefire_(all_falls_down)-1080p.mp4 (342.2 MB) + bittorrent-v2-test/crionics & silents - hardwired (1991, hpad, divx5).avi (135.0 MB) + bittorrent-v2-test/elevated_4000.avi (113.3 MB) + bittorrent-v2-test/luma - mercury _ 64k _ Final.mp4 (183.8 MB) + bittorrent-v2-test/readme.txt (0.06 kB) + bittorrent-v2-test/tbl-starstruck-2006.avi (229.1 MB) + diff --git a/tests/utils/assets/bittorrent-v2-test.torrent b/tests/utils/assets/bittorrent-v2-test.torrent new file mode 100644 index 0000000000000000000000000000000000000000..8ad4c7eab07617e0c2b5c53df29c58d629cf3566 GIT binary patch literal 13592 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 literal 0 HcmV?d00001 diff --git a/tests/utils/run_transmission_show.cmake b/tests/utils/run_transmission_show.cmake index f35df0b5c..115ab8c90 100644 --- a/tests/utils/run_transmission_show.cmake +++ b/tests/utils/run_transmission_show.cmake @@ -39,7 +39,7 @@ else() find_program(DIFF_EXEC diff) if (DIFF_EXEC) message("DIFF:") - execute_process(COMMAND ${DIFF_EXEC} ${output_file} ${reference_file}) + execute_process(COMMAND ${DIFF_EXEC} -u ${output_file} ${reference_file}) endif() file(REMOVE ${output_file}) diff --git a/utils/show.cc b/utils/show.cc index d04f4d231..d4252b160 100644 --- a/utils/show.cc +++ b/utils/show.cc @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -166,20 +167,21 @@ int parseCommandLine(app_opts& opts, int argc, char const* const* argv) return now == 0 ? "Unknown" : fmt::format("{:%a %b %d %T %Y}", fmt::localtime(now)); } -bool compare_2nd_field(std::string_view const& l, std::string_view const& r) +bool compareSecondField(std::string_view l, std::string_view r) { - auto l_ = l.find(" "); - auto r_ = r.find(" "); - - if (l_ == std::string_view::npos) + auto const lpos = l.find(' '); + if (lpos == std::string_view::npos) { return false; } - if (r_ == std::string_view::npos) + + auto const rpos = r.find(' '); + if (rpos == std::string_view::npos) { return true; } - return l.substr(l_) <= r.substr(r_); + + return l.substr(lpos) <= r.substr(rpos); } void showInfo(app_opts const& opts, tr_torrent_metainfo const& metainfo) @@ -283,7 +285,7 @@ void showInfo(app_opts const& opts, tr_torrent_metainfo const& metainfo) { if (opts.show_bytesize) { - std::sort(std::begin(filenames), std::end(filenames), compare_2nd_field); + std::sort(std::begin(filenames), std::end(filenames), compareSecondField); } else {