diff --git a/Transmission.xcodeproj/project.pbxproj b/Transmission.xcodeproj/project.pbxproj index 6f1ae5363..c5c231094 100644 --- a/Transmission.xcodeproj/project.pbxproj +++ b/Transmission.xcodeproj/project.pbxproj @@ -155,6 +155,8 @@ A257C1820CAD3003004E121C /* PeerTableView.mm in Sources */ = {isa = PBXBuildFile; fileRef = A257C1800CAD3003004E121C /* PeerTableView.mm */; }; A25892640CF1F7E800CCCDDF /* StatsWindowController.mm in Sources */ = {isa = PBXBuildFile; fileRef = A25892630CF1F7E800CCCDDF /* StatsWindowController.mm */; }; A259317E0A73B2CC002F4FE7 /* TransmissionHelp in Resources */ = {isa = PBXBuildFile; fileRef = A259316A0A73B2CC002F4FE7 /* TransmissionHelp */; }; + 66F977825E65AD498C028BB0 /* announce-list.cc in Sources */ = {isa = PBXBuildFile; fileRef = 66F977825E65AD498C028BB1 /* announcer-list.cc */; }; + 66F977825E65AD498C028BB2 /* announce-list.h in Headers */ = {isa = PBXBuildFile; fileRef = 66F977825E65AD498C028BB3 /* announce-list.h */; }; A25964A6106D73A800453B31 /* announcer.cc in Sources */ = {isa = PBXBuildFile; fileRef = A25964A4106D73A800453B31 /* announcer.cc */; }; A25964A7106D73A800453B31 /* announcer.h in Headers */ = {isa = PBXBuildFile; fileRef = A25964A5106D73A800453B31 /* announcer.h */; }; A25BFD69167BED3B0039D1AA /* variant-benc.cc in Sources */ = {isa = PBXBuildFile; fileRef = A25BFD63167BED3B0039D1AA /* variant-benc.cc */; }; @@ -702,6 +704,8 @@ A25892620CF1F7E800CCCDDF /* StatsWindowController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StatsWindowController.h; sourceTree = ""; }; A25892630CF1F7E800CCCDDF /* StatsWindowController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = StatsWindowController.mm; sourceTree = ""; }; A259316A0A73B2CC002F4FE7 /* TransmissionHelp */ = {isa = PBXFileReference; lastKnownFileType = folder; path = TransmissionHelp; sourceTree = ""; }; + 66F977825E65AD498C028BB1 /* announce-list.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = announce-list.cc; sourceTree = ""; }; + 66F977825E65AD498C028BB3 /* announce-list.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = announce-list.h; sourceTree = ""; }; A25964A4106D73A800453B31 /* announcer.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = announcer.cc; sourceTree = ""; }; A25964A5106D73A800453B31 /* announcer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = announcer.h; sourceTree = ""; }; A25BFD63167BED3B0039D1AA /* variant-benc.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "variant-benc.cc"; sourceTree = ""; }; @@ -1447,6 +1451,8 @@ C10C644C1D9AF328003C1B4C /* session-id.h */, A20152790D1C26EB0081714F /* torrent-ctor.cc */, A23F299F132A447400E9A83B /* announcer-common.h */, + 66F977825E65AD498C028BB1 /* announce-list.cc */, + 66F977825E65AD498C028BB3 /* announce-list.h */, A25964A4106D73A800453B31 /* announcer.cc */, A25964A5106D73A800453B31 /* announcer.h */, A23F29A0132A447400E9A83B /* announcer-http.cc */, @@ -1869,6 +1875,7 @@ 0A6169A80FE5C9A200C66CE6 /* bitfield.h in Headers */, 1BB44E07B1B52E28291B4E33 /* file-piece-map.h in Headers */, A25964A7106D73A800453B31 /* announcer.h in Headers */, + 66F977825E65AD498C028BB2 /* announce-list.h in Headers */, ED8A16412735A8AA000D61F9 /* peer-mgr-wishlist.h in Headers */, 4D8017EB10BBC073008A4AF2 /* torrent-magnet.h in Headers */, 4D80185A10BBC0B0008A4AF2 /* magnet-metainfo.h in Headers */, @@ -2465,6 +2472,7 @@ 0A6169A70FE5C9A200C66CE6 /* bitfield.cc in Sources */, 1BB44E07B1B52E28291B4E32 /* file-piece-map.cc in Sources */, A25964A6106D73A800453B31 /* announcer.cc in Sources */, + 66F977825E65AD498C028BB0 /* announce-list.cc in Sources */, 4D8017EA10BBC073008A4AF2 /* torrent-magnet.cc in Sources */, 4D80185910BBC0B0008A4AF2 /* magnet-metainfo.cc in Sources */, A220EC5B118C8A060022B4BE /* tr-lpd.cc in Sources */, diff --git a/gtk/DetailsDialog.cc b/gtk/DetailsDialog.cc index 4754494d9..e8b4cb7a0 100644 --- a/gtk/DetailsDialog.cc +++ b/gtk/DetailsDialog.cc @@ -2219,9 +2219,10 @@ void DetailsDialog::Impl::on_edit_trackers_response(int response, std::shared_pt auto const tracker_text = text_buffer->get_text(false); std::istringstream tracker_strings(tracker_text); - std::vector trackers; - std::list announce_urls; - int tier = 0; + auto announce_url_strings = std::vector{}; + auto announce_urls = std::vector{}; + auto tiers = std::vector{}; + auto tier = tr_tracker_tier_t{ 0 }; std::string str; while (std::getline(tracker_strings, str)) @@ -2232,13 +2233,17 @@ void DetailsDialog::Impl::on_edit_trackers_response(int response, std::shared_pt } else { - announce_urls.push_front(str); - trackers.push_back(tr_tracker_info{ tier, announce_urls.front().data(), nullptr, 0 }); + announce_url_strings.push_back(str); + tiers.push_back(tier); } } - /* update the torrent */ - if (tr_torrentSetAnnounceList(tor, trackers.data(), trackers.size())) + std::transform( + std::begin(announce_url_strings), + std::end(announce_url_strings), + std::back_inserter(announce_urls), + [](auto const& url) { return url.c_str(); }); + if (tr_torrentSetAnnounceList(tor, std::data(announce_urls), std::data(tiers), std::size(announce_urls))) { refresh(); } diff --git a/libtransmission/CMakeLists.txt b/libtransmission/CMakeLists.txt index 82d6cf0d6..b0f682797 100644 --- a/libtransmission/CMakeLists.txt +++ b/libtransmission/CMakeLists.txt @@ -6,6 +6,7 @@ configure_file( ) set(PROJECT_FILES + announce-list.cc announcer-http.cc announcer-udp.cc announcer.cc @@ -143,6 +144,7 @@ set(${PROJECT_NAME}_PUBLIC_HEADERS ) set(${PROJECT_NAME}_PRIVATE_HEADERS + announce-list.h announcer-common.h announcer.h bandwidth.h diff --git a/libtransmission/announce-list.cc b/libtransmission/announce-list.cc new file mode 100644 index 000000000..6d667dd59 --- /dev/null +++ b/libtransmission/announce-list.cc @@ -0,0 +1,259 @@ +/* + * This file Copyright (C) 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 "announce-list.h" +#include "metainfo.h" +#include "utils.h" +#include "variant.h" + +size_t tr_announce_list::set(char const* const* announce_urls, tr_tracker_tier_t const* tiers, size_t n) +{ + trackers_.clear(); + + for (size_t i = 0; i < n; ++i) + { + add(tiers[i], announce_urls[i]); + } + + return size(); +} + +bool tr_announce_list::remove(std::string_view announce_url) +{ + auto it = find(announce_url); + if (it == std::end(trackers_)) + { + return false; + } + + trackers_.erase(it); + return true; +} + +bool tr_announce_list::remove(tr_tracker_id_t id) +{ + auto it = find(id); + if (it == std::end(trackers_)) + { + return false; + } + + trackers_.erase(it); + return true; +} + +bool tr_announce_list::replace(tr_tracker_id_t id, std::string_view announce_url_sv) +{ + auto const announce = tr_urlParseTracker(announce_url_sv); + if (!announce || !canAdd(*announce)) + { + return false; + } + + auto it = find(id); + if (it == std::end(trackers_)) + { + return false; + } + + auto const tier = it->tier; + trackers_.erase(it); + return add(tier, announce_url_sv); +} + +bool tr_announce_list::add(tr_tracker_tier_t tier, std::string_view announce_url_sv) +{ + auto const announce = tr_urlParseTracker(announce_url_sv); + if (!announce || !canAdd(*announce)) + { + return false; + } + + auto tracker = tracker_info{}; + tracker.announce_interned = tr_quark_new(announce_url_sv); + tracker.announce = *tr_urlParseTracker(tr_quark_get_string_view(tracker.announce_interned)); + tracker.tier = getTier(tier, *announce); + tracker.id = nextUniqueId(); + auto host = std::string{ tracker.announce.host }; + host += ':'; + host += tracker.announce.portstr; + tracker.host = tr_quark_new(host); + + auto const scrape_str = announceToScrape(announce_url_sv); + if (scrape_str) + { + tracker.scrape_interned = tr_quark_new(*scrape_str); + tracker.scrape = *tr_urlParseTracker(tr_quark_get_string_view(tracker.scrape_interned)); + } + + auto const it = std::lower_bound(std::begin(trackers_), std::end(trackers_), tracker); + trackers_.insert(it, tracker); + return true; +} + +std::optional tr_announce_list::announceToScrape(std::string_view announce) +{ + // To derive the scrape URL use the following steps: + // Begin with the announce URL. Find the last '/' in it. + // If the text immediately following that '/' isn't 'announce' + // it will be taken as a sign that that tracker doesn't support + // the scrape convention. If it does, substitute 'scrape' for + // 'announce' to find the scrape page. + auto constexpr oldval = std::string_view{ "/announce" }; + if (auto pos = announce.rfind(oldval.front()); pos != std::string_view::npos && announce.find(oldval, pos) == pos) + { + auto const prefix = announce.substr(0, pos); + auto const suffix = announce.substr(pos + std::size(oldval)); + return tr_strvJoin(prefix, std::string_view{ "/scrape" }, suffix); + } + + // some torrents with UDP announce URLs don't have /announce + if (tr_strvStartsWith(announce, std::string_view{ "udp:" })) + { + return std::string{ announce }; + } + + return {}; +} + +tr_quark tr_announce_list::announceToScrape(tr_quark announce) +{ + auto const scrape_str = announceToScrape(tr_quark_get_string_view(announce)); + if (scrape_str) + { + return tr_quark_new(*scrape_str); + } + return TR_KEY_NONE; +} + +std::set tr_announce_list::tiers() const +{ + auto tiers = std::set{}; + for (auto const& tracker : trackers_) + { + tiers.insert(tracker.tier); + } + + return tiers; +} + +tr_tracker_tier_t tr_announce_list::nextTier() const +{ + return std::empty(trackers_) ? 0 : trackers_.back().tier + 1; +} + +tr_tracker_id_t tr_announce_list::nextUniqueId() +{ + static tr_tracker_id_t id = 0; + return id++; +} + +tr_announce_list::trackers_t::iterator tr_announce_list::find(tr_tracker_id_t id) +{ + auto const test = [&id](auto const& tracker) + { + return tracker.id == id; + }; + return std::find_if(std::begin(trackers_), std::end(trackers_), test); +} + +tr_announce_list::trackers_t::iterator tr_announce_list::find(std::string_view announce) +{ + auto const test = [&announce](auto const& tracker) + { + return announce == tracker.announce.full; + }; + return std::find_if(std::begin(trackers_), std::end(trackers_), test); +} + +// if two announce URLs differ only by scheme, put them in the same tier. +// (note: this can leave gaps in the `tier' values, but since the calling +// function doesn't care, there's no point in removing the gaps...) +tr_tracker_tier_t tr_announce_list::getTier(tr_tracker_tier_t tier, tr_url_parsed_t const& announce) const +{ + auto const is_sibling = [&announce](tracker_info const& tracker) + { + return tracker.announce.host == announce.host && tracker.announce.path == announce.path; + }; + + auto const it = std::find_if(std::begin(trackers_), std::end(trackers_), is_sibling); + return it != std::end(trackers_) ? it->tier : tier; +} + +bool tr_announce_list::canAdd(tr_url_parsed_t const& announce) +{ + // looking at components instead of the full original URL lets + // us weed out implicit-vs-explicit port duplicates e.g. + // "http://tracker/announce" + "http://tracker:80/announce" + auto const is_same = [&announce](auto const& tracker) + { + return tracker.announce.scheme == announce.scheme && tracker.announce.host == announce.host && + tracker.announce.port == announce.port && tracker.announce.path == announce.path; + }; + return std::none_of(std::begin(trackers_), std::end(trackers_), is_same); +} + +bool tr_announce_list::save(char const* torrent_file, tr_error** error) const +{ + // load the .torrent file + auto metainfo = tr_variant{}; + if (!tr_variantFromFile(&metainfo, TR_VARIANT_PARSE_BENC, torrent_file, error)) + { + return false; + } + + // remove the old fields + tr_variantDictRemove(&metainfo, TR_KEY_announce); + tr_variantDictRemove(&metainfo, TR_KEY_announce_list); + + // add the new fields + if (this->size() == 1) + { + tr_variantDictAddQuark(&metainfo, TR_KEY_announce, at(0).announce_interned); + } + else if (this->size() > 1) + { + tr_variant* tier_list = tr_variantDictAddList(&metainfo, TR_KEY_announce_list, 0); + + auto current_tier = std::optional{}; + tr_variant* tracker_list = nullptr; + + for (auto const& tracker : *this) + { + if (tracker_list == nullptr || !current_tier || current_tier != tracker.tier) + { + tracker_list = tr_variantListAddList(tier_list, 1); + current_tier = tracker.tier; + } + + tr_variantListAddQuark(tracker_list, tracker.announce_interned); + } + } + + // confirm that it's good by parsing it back again + if (!tr_metainfoParse(nullptr, &metainfo, error)) + { + tr_variantFree(&metainfo); + return false; + } + + // save it + auto contents_len = size_t{}; + auto* const contents = tr_variantToStr(&metainfo, TR_VARIANT_FMT_BENC, &contents_len); + tr_variantFree(&metainfo); + auto const success = tr_saveFile(torrent_file, { contents, contents_len }, error); + tr_free(contents); + return success; +} diff --git a/libtransmission/announce-list.h b/libtransmission/announce-list.h new file mode 100644 index 000000000..d67ab0e6d --- /dev/null +++ b/libtransmission/announce-list.h @@ -0,0 +1,123 @@ +/* + * This file Copyright (C) Mnemosyne LLC + * + * It may be used under the GNU GPL versions 2 or 3 + * or any future license endorsed by Mnemosyne LLC. + * + */ + +#pragma once + +#if 0 // TODO(ckerr): re-enable this after tr_info is made private +#ifndef __TRANSMISSION__ +#error only libtransmission should #include this header. +#endif +#endif + +#include +#include +#include +#include +#include +#include + +#include "transmission.h" + +#include "error.h" +#include "quark.h" +#include "tr-assert.h" +#include "utils.h" +#include "web-utils.h" + +class tr_announce_list +{ +public: + struct tracker_info + { + tr_quark host; + tr_url_parsed_t announce; + tr_url_parsed_t scrape; + tr_quark announce_interned = TR_KEY_NONE; + tr_quark scrape_interned = TR_KEY_NONE; + tr_tracker_tier_t tier = 0; + tr_tracker_id_t id = 0; + + int compare(tracker_info const& that) const // <=> + { + if (this->tier != that.tier) + { + return this->tier < that.tier ? -1 : 1; + } + + if (this->announce.full != that.announce.full) + { + return this->announce.full < that.announce.full ? -1 : 1; + } + + return 0; + } + + bool operator<(tracker_info const& that) const + { + return compare(that) < 0; + } + + bool operator==(tracker_info const& that) const + { + return compare(that) == 0; + } + }; + +private: + using trackers_t = std::vector; + +public: + auto begin() const + { + return std::begin(trackers_); + } + auto end() const + { + return std::end(trackers_); + } + bool empty() const + { + return std::empty(trackers_); + } + size_t size() const + { + return std::size(trackers_); + } + tracker_info const& at(size_t i) const + { + return trackers_.at(i); + } + + std::set tiers() const; + tr_tracker_tier_t nextTier() const; + + bool add(tr_tracker_tier_t tier, std::string_view announce_url_sv); + bool remove(std::string_view announce_url); + bool remove(tr_tracker_id_t id); + bool replace(tr_tracker_id_t id, std::string_view announce_url_sv); + size_t set(char const* const* announce_urls, tr_tracker_tier_t const* tiers, size_t n); + void clear() + { + return trackers_.clear(); + } + + bool save(char const* torrent_file, tr_error** error = nullptr) const; + + static std::optional announceToScrape(std::string_view announce); + static tr_quark announceToScrape(tr_quark announce); + +private: + tr_tracker_tier_t getTier(tr_tracker_tier_t tier, tr_url_parsed_t const& announce) const; + + bool canAdd(tr_url_parsed_t const& announce); + tr_tracker_id_t nextUniqueId(); + trackers_t::iterator find(std::string_view announce); + trackers_t::iterator find(tr_tracker_id_t id); + + trackers_t trackers_; +}; diff --git a/libtransmission/announcer.cc b/libtransmission/announcer.cc index c69badb52..41796dd90 100644 --- a/libtransmission/announcer.cc +++ b/libtransmission/announcer.cc @@ -23,8 +23,10 @@ #define LIBTRANSMISSION_ANNOUNCER_MODULE #include "transmission.h" -#include "announcer.h" + +#include "announce-list.h" #include "announcer-common.h" +#include "announcer.h" #include "crypto-utils.h" /* tr_rand_int(), tr_rand_int_weak() */ #include "log.h" #include "peer-mgr.h" /* tr_peerMgrCompactToPex() */ @@ -249,13 +251,14 @@ tr_quark tr_announcerGetKey(tr_url_parsed_t const& parsed) return tr_quark_new(buf); } -static void trackerConstruct(tr_announcer* announcer, tr_tracker* tracker, tr_tracker_info const* inf) +static void trackerConstruct(tr_announcer* announcer, tr_tracker* tracker, tr_announce_list::tracker_info const& info) { memset(tracker, 0, sizeof(tr_tracker)); - tracker->key = tr_announcerGetKey(inf->announce); - tracker->announce_url = tr_quark_new(tr_strvStrip(inf->announce)); - tracker->scrape_info = inf->scrape == nullptr ? nullptr : tr_announcerGetScrapeInfo(announcer, tr_quark_new(inf->scrape)); - tracker->id = inf->id; + tracker->key = info.host; + tracker->announce_url = info.announce_interned; + tracker->scrape_info = info.scrape_interned == TR_KEY_NONE ? nullptr : + tr_announcerGetScrapeInfo(announcer, info.scrape_interned); + tracker->id = info.id; tracker->seederCount = -1; tracker->leecherCount = -1; tracker->downloadCount = -1; @@ -270,7 +273,7 @@ static void trackerDestruct(tr_tracker* tracker) **** ***/ -struct tr_torrent_tiers; +struct tr_announcer_tiers; /** @brief A group of trackers in a single tier, as per the multitracker spec */ struct tr_tier @@ -398,7 +401,7 @@ static void tierIncrementTracker(tr_tier* tier) * * this opaque data structure can be found in tr_torrent.tiers */ -struct tr_torrent_tiers +struct tr_announcer_tiers { tr_tier* tiers; int tier_count; @@ -410,12 +413,12 @@ struct tr_torrent_tiers void* callbackData; }; -static tr_torrent_tiers* tiersNew(void) +static tr_announcer_tiers* tiersNew(void) { - return tr_new0(tr_torrent_tiers, 1); + return tr_new0(tr_announcer_tiers, 1); } -static void tiersDestruct(tr_torrent_tiers* tt) +static void tiersDestruct(tr_announcer_tiers* tt) { for (int i = 0; i < tt->tracker_count; ++i) { @@ -432,7 +435,7 @@ static void tiersDestruct(tr_torrent_tiers* tt) tr_free(tt->tiers); } -static void tiersFree(tr_torrent_tiers* tt) +static void tiersFree(tr_announcer_tiers* tt) { tiersDestruct(tt); tr_free(tt); @@ -447,9 +450,9 @@ static tr_tier* getTier(tr_announcer* announcer, tr_sha1_digest_t const& info_ha tr_session* session = announcer->session; tr_torrent* tor = tr_torrentFindFromHash(session, info_hash); - if (tor != nullptr && tor->tiers != nullptr) + if (tor != nullptr && tor->announcer_tiers != nullptr) { - tr_torrent_tiers* tt = tor->tiers; + tr_announcer_tiers* tt = tor->announcer_tiers; for (int i = 0; tier == nullptr && i < tt->tier_count; ++i) { @@ -470,9 +473,10 @@ static tr_tier* getTier(tr_announcer* announcer, tr_sha1_digest_t const& info_ha static void publishMessage(tr_tier* tier, char const* msg, TrackerEventType type) { - if (tier != nullptr && tier->tor != nullptr && tier->tor->tiers != nullptr && tier->tor->tiers->callback != nullptr) + if (tier != nullptr && tier->tor != nullptr && tier->tor->announcer_tiers != nullptr && + tier->tor->announcer_tiers->callback != nullptr) { - tr_torrent_tiers* tiers = tier->tor->tiers; + tr_announcer_tiers* tiers = tier->tor->announcer_tiers; auto event = tr_tracker_event{}; event.messageType = type; event.text = msg; @@ -503,7 +507,7 @@ static void publishError(tr_tier* tier, char const* msg) static void publishPeerCounts(tr_tier* tier, int seeders, int leechers) { - if (tier->tor->tiers->callback != nullptr) + if (tier->tor->announcer_tiers->callback != nullptr) { auto e = tr_tracker_event{}; e.messageType = TR_TRACKER_COUNTS; @@ -511,13 +515,13 @@ static void publishPeerCounts(tr_tier* tier, int seeders, int leechers) e.leechers = leechers; dbgmsg(tier, "peer counts: %d seeders, %d leechers.", seeders, leechers); - (*tier->tor->tiers->callback)(tier->tor, &e, nullptr); + (*tier->tor->announcer_tiers->callback)(tier->tor, &e, nullptr); } } static void publishPeersPex(tr_tier* tier, int seeders, int leechers, tr_pex const* pex, int n) { - if (tier->tor->tiers->callback != nullptr) + if (tier->tor->announcer_tiers->callback != nullptr) { auto e = tr_tracker_event{}; e.messageType = TR_TRACKER_PEERS; @@ -527,7 +531,7 @@ static void publishPeersPex(tr_tier* tier, int seeders, int leechers, tr_pex con e.pexCount = n; dbgmsg(tier, "tracker knows of %d seeders and %d leechers and gave a list of %d peers.", seeders, leechers, n); - (*tier->tor->tiers->callback)(tier->tor, &e, nullptr); + (*tier->tor->announcer_tiers->callback)(tier->tor, &e, nullptr); } } @@ -535,152 +539,46 @@ static void publishPeersPex(tr_tier* tier, int seeders, int leechers, tr_pex con **** ***/ -struct AnnTrackerInfo +static void addTorrentToTier(tr_announcer_tiers* tt, tr_torrent* tor) { - AnnTrackerInfo(tr_tracker_info const& info_in, tr_url_parsed_t const& url_in) - : info{ info_in } - , url{ url_in } - { - } + auto const n = tor->trackerCount(); + auto const tiers = tor->tiers(); - tr_tracker_info info; - tr_url_parsed_t url; - - /* primary key: tier - * secondary key: udp comes before http */ - int compare(AnnTrackerInfo const& that) const // <=> - { - if (this->info.tier != that.info.tier) - { - return this->info.tier - that.info.tier; - } - - return -this->url.scheme.compare(that.url.scheme); - } - - bool operator<(AnnTrackerInfo const& that) const // less than - { - return this->compare(that) < 0; - } -}; - -/** - * Massages the incoming list of trackers into something we can use. - */ -static tr_tracker_info* filter_trackers(tr_tracker_info const* input, int input_count, int* setme_count) -{ - auto tmp = std::vector{}; - tmp.reserve(input_count); - - // build a list of valid trackers - for (auto const *walk = input, *const end = walk + input_count; walk != end; ++walk) - { - auto const parsed = tr_urlParseTracker(walk->announce); - if (!parsed) - { - continue; - } - - // weed out implicit-vs-explicit port duplicates e.g. - // "http://tracker/announce" + "http://tracker:80/announce" - auto const is_same = [&parsed](auto const& item) - { - return item.url.scheme == parsed->scheme && item.url.host == parsed->host && item.url.port == parsed->port && - item.url.path == parsed->path; - }; - if (std::any_of(std::begin(tmp), std::end(tmp), is_same)) - { - continue; - } - - tmp.emplace_back(*walk, *parsed); - } - - // if two announce URLs differ only by scheme, put them in the same tier. - // (note: this can leave gaps in the `tier' values, but since the calling - // function doesn't care, there's no point in removing the gaps...) - for (size_t i = 0, n = std::size(tmp); i < n; ++i) - { - for (size_t j = i + 1; j < n; ++j) - { - auto const& a = tmp[i]; - auto& b = tmp[j]; - - if ((a.info.tier != b.info.tier) && (a.url.port == b.url.port) && (a.url.host == b.url.host) && - (a.url.path == b.url.path)) - { - b.info.tier = a.info.tier; - } - } - } - - // sort them, for two reasons: - // 1. unjumble the tiers from the previous step - // 2. move the UDP trackers to the front of each tier - std::sort(std::begin(tmp), std::end(tmp)); - - // build the output - auto const n = std::size(tmp); - *setme_count = n; - auto* const ret = tr_new0(tr_tracker_info, n); - std::transform(std::begin(tmp), std::end(tmp), ret, [](auto const& item) { return item.info; }); - return ret; -} - -static void addTorrentToTier(tr_torrent_tiers* tt, tr_torrent* tor) -{ - auto n = int{}; - tr_tracker_info* infos = filter_trackers(tor->info.trackers, tor->info.trackerCount, &n); - - /* build the array of trackers */ + // build the tracker and tier arrays tt->trackers = tr_new0(tr_tracker, n); tt->tracker_count = n; + tt->tiers = tr_new0(tr_tier, std::size(tiers)); - for (int i = 0; i < n; ++i) - { - trackerConstruct(tor->session->announcer, &tt->trackers[i], &infos[i]); - } - - /* count how many tiers there are */ - auto tier_count = int{}; - for (int i = 0; i < n; ++i) - { - if (i == 0 || infos[i].tier != infos[i - 1].tier) - { - ++tier_count; - } - } - - /* build the array of tiers */ + auto prev_tier = std::optional{}; tr_tier* tier = nullptr; - tt->tiers = tr_new0(tr_tier, tier_count); - tt->tier_count = 0; - - for (int i = 0; i < n; ++i) + for (size_t i = 0; i < n; ++i) { - if (i != 0 && infos[i].tier == infos[i - 1].tier) - { - ++tier->tracker_count; - } - else + auto const info = tor->tracker(i); + + trackerConstruct(tor->session->announcer, &tt->trackers[i], info); + + if (!prev_tier || *prev_tier != info.tier) { tier = &tt->tiers[tt->tier_count++]; tierConstruct(tier, tor); tier->trackers = &tt->trackers[i]; tier->tracker_count = 1; tierIncrementTracker(tier); + + prev_tier = info.tier; + } + else + { + ++tier->tracker_count; } } - - /* cleanup */ - tr_free(infos); } -tr_torrent_tiers* tr_announcerAddTorrent(tr_torrent* tor, tr_tracker_callback callback, void* callbackData) +tr_announcer_tiers* tr_announcerAddTorrent(tr_torrent* tor, tr_tracker_callback callback, void* callbackData) { TR_ASSERT(tr_isTorrent(tor)); - tr_torrent_tiers* tiers = tiersNew(); + tr_announcer_tiers* tiers = tiersNew(); tiers->callback = callback; tiers->callbackData = callbackData; @@ -701,13 +599,13 @@ static bool tierCanManualAnnounce(tr_tier const* tier) bool tr_announcerCanManualAnnounce(tr_torrent const* tor) { TR_ASSERT(tr_isTorrent(tor)); - TR_ASSERT(tor->tiers != nullptr); + TR_ASSERT(tor->announcer_tiers != nullptr); - struct tr_torrent_tiers const* tt = nullptr; + struct tr_announcer_tiers const* tt = nullptr; if (tor->isRunning) { - tt = tor->tiers; + tt = tor->announcer_tiers; } /* return true if any tier can manual announce */ @@ -725,7 +623,7 @@ bool tr_announcerCanManualAnnounce(tr_torrent const* tor) time_t tr_announcerNextManualAnnounce(tr_torrent const* tor) { time_t ret = ~(time_t)0; - struct tr_torrent_tiers const* tt = tor->tiers; + struct tr_announcer_tiers const* tt = tor->announcer_tiers; /* find the earliest manual announce time from all peers */ for (int i = 0; tt != nullptr && i < tt->tier_count; ++i) @@ -850,7 +748,7 @@ static tr_announce_event tier_announce_event_pull(tr_tier* tier) static void torrentAddAnnounce(tr_torrent* tor, tr_announce_event e, time_t announceAt) { - struct tr_torrent_tiers* tt = tor->tiers; + struct tr_announcer_tiers* tt = tor->announcer_tiers; /* walk through each tier and tell them to announce */ for (int i = 0; i < tt->tier_count; ++i) @@ -893,7 +791,7 @@ void tr_announcerAddBytes(tr_torrent* tor, int type, uint32_t byteCount) TR_ASSERT(tr_isTorrent(tor)); TR_ASSERT(type == TR_ANN_UP || type == TR_ANN_DOWN || type == TR_ANN_CORRUPT); - struct tr_torrent_tiers* tt = tor->tiers; + struct tr_announcer_tiers* tt = tor->announcer_tiers; for (int i = 0; i < tt->tier_count; ++i) { @@ -933,7 +831,7 @@ static void announce_request_free(tr_announce_request* req); void tr_announcerRemoveTorrent(tr_announcer* announcer, tr_torrent* tor) { - struct tr_torrent_tiers const* tt = tor->tiers; + struct tr_announcer_tiers const* tt = tor->announcer_tiers; if (tt != nullptr) { @@ -957,8 +855,8 @@ void tr_announcerRemoveTorrent(tr_announcer* announcer, tr_torrent* tor) } } - tiersFree(tor->tiers); - tor->tiers = nullptr; + tiersFree(tor->announcer_tiers); + tor->announcer_tiers = nullptr; } } @@ -1080,7 +978,7 @@ static void on_announce_done(tr_announce_response const* response, void* vdata) Don't bother publishing if there are other trackers -- it's all too common for people to load up dozens of dead trackers in a torrent's metainfo... */ - if (tier->tor->info.trackerCount < 2) + if (tier->tor->trackerCount() < 2) { publishError(tier, response->errmsg); } @@ -1323,7 +1221,7 @@ static void on_scrape_error(tr_session const* session, tr_tier* tier, char const static tr_tier* find_tier(tr_torrent* tor, tr_quark scrape_url) { - struct tr_torrent_tiers* tt = tor->tiers; + struct tr_announcer_tiers* tt = tor->announcer_tiers; for (int i = 0; tt != nullptr && i < tt->tier_count; ++i) { @@ -1637,7 +1535,7 @@ static void scrapeAndAnnounceMore(tr_announcer* announcer) auto scrape_me = std::vector{}; for (auto* tor : announcer->session->torrents) { - struct tr_torrent_tiers* tt = tor->tiers; + struct tr_announcer_tiers* tt = tor->announcer_tiers; for (int i = 0; tt != nullptr && i < tt->tier_count; ++i) { @@ -1798,25 +1696,25 @@ static tr_tracker_view trackerView(tr_torrent const& tor, int tier_index, tr_tie } TR_ASSERT(0 <= view.tier); - TR_ASSERT(view.tier < tor.tiers->tier_count); + TR_ASSERT(view.tier < tor.announcer_tiers->tier_count); return view; } size_t tr_announcerTrackerCount(tr_torrent const* tor) { TR_ASSERT(tr_isTorrent(tor)); - TR_ASSERT(tor->tiers != nullptr); + TR_ASSERT(tor->announcer_tiers != nullptr); - return tor->tiers->tracker_count; + return tor->announcer_tiers->tracker_count; } tr_tracker_view tr_announcerTracker(tr_torrent const* tor, size_t nth) { TR_ASSERT(tr_isTorrent(tor)); - TR_ASSERT(tor->tiers != nullptr); + TR_ASSERT(tor->announcer_tiers != nullptr); // find the nth tracker - struct tr_torrent_tiers const* const tt = tor->tiers; + struct tr_announcer_tiers const* const tt = tor->announcer_tiers; if (nth >= size_t(tt->tracker_count)) { return {}; @@ -1869,7 +1767,7 @@ static void copy_tier_attributes_impl(struct tr_tier* tgt, int trackerIndex, tr_ tgt->currentTracker->downloaderCount = src->currentTracker->downloaderCount; } -static void copy_tier_attributes(struct tr_torrent_tiers* tt, tr_tier const* src) +static void copy_tier_attributes(struct tr_announcer_tiers* tt, tr_tier const* src) { bool found = false; @@ -1889,12 +1787,12 @@ static void copy_tier_attributes(struct tr_torrent_tiers* tt, tr_tier const* src void tr_announcerResetTorrent(tr_announcer* /*announcer*/, tr_torrent* tor) { - TR_ASSERT(tor->tiers != nullptr); + TR_ASSERT(tor->announcer_tiers != nullptr); time_t const now = tr_time(); - struct tr_torrent_tiers* tt = tor->tiers; - tr_torrent_tiers old = *tt; + tr_announcer_tiers* tt = tor->announcer_tiers; + tr_announcer_tiers old = *tt; /* remove the old tiers / trackers */ tt->tiers = nullptr; diff --git a/libtransmission/announcer.h b/libtransmission/announcer.h index 1b047dea2..9b31f90b7 100644 --- a/libtransmission/announcer.h +++ b/libtransmission/announcer.h @@ -16,7 +16,7 @@ #include "quark.h" struct tr_announcer; -struct tr_torrent_tiers; +struct tr_announcer_tiers; /** * *** Tracker Publish / Subscribe @@ -66,7 +66,7 @@ void tr_announcerClose(tr_session*); *** For torrent customers **/ -struct tr_torrent_tiers* tr_announcerAddTorrent(tr_torrent* torrent, tr_tracker_callback cb, void* cbdata); +struct tr_announcer_tiers* tr_announcerAddTorrent(tr_torrent* torrent, tr_tracker_callback cb, void* cbdata); void tr_announcerResetTorrent(struct tr_announcer*, tr_torrent*); diff --git a/libtransmission/magnet-metainfo.cc b/libtransmission/magnet-metainfo.cc index a4954b5a5..6c1f7697b 100644 --- a/libtransmission/magnet-metainfo.cc +++ b/libtransmission/magnet-metainfo.cc @@ -123,10 +123,10 @@ std::string tr_magnet_metainfo::magnet() const tr_http_escape(s, name, true); } - for (auto const& [tier, tracker] : trackers) + for (auto const& tracker : this->announce_list) { s += "&tr="sv; - tr_http_escape(s, tr_quark_get_string_view(tracker.announce_url), true); + tr_http_escape(s, tracker.announce.full, true); } for (auto const& webseed : webseed_urls) @@ -138,33 +138,6 @@ std::string tr_magnet_metainfo::magnet() const return s; } -static tr_quark announceToScrape(std::string_view announce) -{ - auto buf = std::string{}; - - if (!tr_magnet_metainfo::convertAnnounceToScrape(buf, announce)) - { - return TR_KEY_NONE; - } - - return tr_quark_new(buf); -} - -bool tr_magnet_metainfo::addTracker(tr_tracker_tier_t tier, std::string_view announce_sv) -{ - announce_sv = tr_strvStrip(announce_sv); - - if (!tr_urlIsValidTracker(announce_sv)) - { - return false; - } - - auto const announce_url = tr_quark_new(announce_sv); - auto const scrape_url = announceToScrape(announce_sv); - this->trackers.insert({ tier, { announce_url, scrape_url, tier } }); - return true; -} - bool tr_magnet_metainfo::parseMagnet(std::string_view magnet_link, tr_error** error) { auto const parsed = tr_urlParse(magnet_link); @@ -175,7 +148,6 @@ bool tr_magnet_metainfo::parseMagnet(std::string_view magnet_link, tr_error** er } bool got_checksum = false; - auto tier = tr_tracker_tier_t{ 0 }; for (auto const& [key, value] : tr_url_query_view{ parsed->query }) { if (key == "dn"sv) @@ -185,8 +157,7 @@ bool tr_magnet_metainfo::parseMagnet(std::string_view magnet_link, tr_error** er else if (key == "tr"sv || tr_strvStartsWith(key, "tr."sv)) { // "tr." explanation @ https://trac.transmissionbt.com/ticket/3341 - addTracker(tier, tr_urlPercentDecode(value)); - ++tier; + this->announce_list.add(this->announce_list.nextTier(), tr_urlPercentDecode(value)); } else if (key == "ws"sv) { @@ -228,49 +199,31 @@ bool tr_magnet_metainfo::parseMagnet(std::string_view magnet_link, tr_error** er return got_checksum; } -bool tr_magnet_metainfo::convertAnnounceToScrape(std::string& out, std::string_view in) -{ - // To derive the scrape URL use the following steps: - // Begin with the announce URL. Find the last '/' in it. - // If the text immediately following that '/' isn't 'announce' - // it will be taken as a sign that that tracker doesn't support - // the scrape convention. If it does, substitute 'scrape' for - // 'announce' to find the scrape page. - auto constexpr oldval = "/announce"sv; - if (auto pos = in.rfind(oldval.front()); pos != in.npos && in.find(oldval, pos) == pos) - { - auto const prefix = in.substr(0, pos); - auto const suffix = in.substr(pos + std::size(oldval)); - tr_buildBuf(out, prefix, "/scrape"sv, suffix); - return true; - } - - // some torrents with UDP announce URLs don't have /announce - if (tr_strvStartsWith(in, "udp:"sv)) - { - out = in; - return true; - } - - return false; -} - void tr_magnet_metainfo::toVariant(tr_variant* top) const { tr_variantInitDict(top, 4); // announce list - auto n = std::size(this->trackers); + auto n = std::size(this->announce_list); if (n == 1) { - tr_variantDictAddStr(top, TR_KEY_announce, tr_quark_get_string_view(std::begin(this->trackers)->second.announce_url)); + tr_variantDictAddQuark(top, TR_KEY_announce, this->announce_list.at(0).announce_interned); } else { - auto* list = tr_variantDictAddList(top, TR_KEY_announce_list, n); - for (auto const& [tier, tracker] : this->trackers) + auto current_tier = tr_tracker_tier_t{}; + tr_variant* tracker_list = nullptr; + + auto* tier_list = tr_variantDictAddList(top, TR_KEY_announce_list, n); + for (auto const& tracker : this->announce_list) { - tr_variantListAddStr(tr_variantListAddList(list, 1), tr_quark_get_string_view(tracker.announce_url)); + if (tracker_list == nullptr || current_tier != tracker.tier) + { + tracker_list = tr_variantListAddList(tier_list, 1); + current_tier = tracker.tier; + } + + tr_variantListAddQuark(tracker_list, tracker.announce_interned); } } diff --git a/libtransmission/magnet-metainfo.h b/libtransmission/magnet-metainfo.h index b1c0ce572..f9c303c9e 100644 --- a/libtransmission/magnet-metainfo.h +++ b/libtransmission/magnet-metainfo.h @@ -19,6 +19,7 @@ #include "transmission.h" +#include "announce-list.h" #include "error.h" #include "quark.h" @@ -32,43 +33,19 @@ struct tr_magnet_metainfo void toVariant(tr_variant*) const; - static bool convertAnnounceToScrape(std::string& setme, std::string_view announce_url); - std::string_view infoHashString() 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 }; } - struct tracker_t - { - tr_quark announce_url; - tr_quark scrape_url; - tr_tracker_tier_t tier; - - tracker_t(tr_quark announce_in, tr_quark scrape_in, tr_tracker_tier_t tier_in) - : announce_url{ announce_in } - , scrape_url{ scrape_in } - , tier{ tier_in } - { - } - - bool operator<(tracker_t const& that) const - { - return announce_url < that.announce_url; - } - }; + tr_announce_list announce_list; std::vector webseed_urls; std::string name; - std::multimap trackers; - tr_sha1_digest_string_t info_hash_chars; tr_sha1_digest_t info_hash; - -protected: - bool addTracker(tr_tracker_tier_t tier, std::string_view announce_url); }; diff --git a/libtransmission/makemeta.h b/libtransmission/makemeta.h index 60fce0de2..66a70e47b 100644 --- a/libtransmission/makemeta.h +++ b/libtransmission/makemeta.h @@ -26,6 +26,14 @@ enum tr_metainfo_builder_err TR_MAKEMETA_IO_WRITE /* see builder.errfile, builder.my_errno */ }; +struct tr_tracker_info +{ + int tier; + char* announce; + char* scrape; + uint32_t id; /* unique identifier used to match to a tr_tracker_stat */ +}; + struct tr_metainfo_builder { /** diff --git a/libtransmission/metainfo.cc b/libtransmission/metainfo.cc index 19dbd7401..b9b890d36 100644 --- a/libtransmission/metainfo.cc +++ b/libtransmission/metainfo.cc @@ -236,122 +236,44 @@ static char const* parseFiles(tr_info* inf, tr_variant* files, tr_variant const* return errstr; } -static char* tr_convertAnnounceToScrape(std::string_view url) -{ - char* scrape = nullptr; - - /* To derive the scrape URL use the following steps: - * Begin with the announce URL. Find the last '/' in it. - * If the text immediately following that '/' isn't 'announce' - * it will be taken as a sign that that tracker doesn't support - * the scrape convention. If it does, substitute 'scrape' for - * 'announce' to find the scrape page. */ - - auto constexpr oldval = "/announce"sv; - auto pos = url.rfind(oldval.front()); - if (pos != url.npos && url.find(oldval, pos) == pos) - { - auto constexpr newval = "/scrape"sv; - auto const prefix = url.substr(0, pos); - auto const suffix = url.substr(pos + std::size(oldval)); - auto const n = std::size(prefix) + std::size(newval) + std::size(suffix); - scrape = tr_new(char, n + 1); - auto* walk = scrape; - walk = std::copy(std::begin(prefix), std::end(prefix), walk); - walk = std::copy(std::begin(newval), std::end(newval), walk); - walk = std::copy(std::begin(suffix), std::end(suffix), walk); - *walk = '\0'; - TR_ASSERT(scrape + n == walk); - } - // some torrents with UDP announce URLs don't have /announce - else if (url.find("udp:"sv) == 0) - { - scrape = tr_strvDup(url); - } - - return scrape; -} - static char const* getannounce(tr_info* inf, tr_variant* meta) { - tr_tracker_info* trackers = nullptr; - int trackerCount = 0; + inf->announce_list = std::make_shared(); + + // tr_tracker_info* trackers = nullptr; + // int trackerCount = 0; auto url = std::string_view{}; /* Announce-list */ tr_variant* tiers = nullptr; if (tr_variantDictFindList(meta, TR_KEY_announce_list, &tiers)) { - int const numTiers = tr_variantListSize(tiers); - int n = 0; - - for (int i = 0; i < numTiers; i++) + for (size_t i = 0, in = tr_variantListSize(tiers); i < in; ++i) { - n += tr_variantListSize(tr_variantListChild(tiers, i)); - } - - trackers = tr_new0(tr_tracker_info, n); - - int validTiers = 0; - for (int i = 0; i < numTiers; ++i) - { - tr_variant* tier = tr_variantListChild(tiers, i); - int const tierSize = tr_variantListSize(tier); - bool anyAdded = false; - - for (int j = 0; j < tierSize; j++) + tr_variant* tier_list = tr_variantListChild(tiers, i); + if (tier_list == nullptr) { - if (tr_variantGetStrView(tr_variantListChild(tier, j), &url)) + continue; + } + + for (size_t j = 0, jn = tr_variantListSize(tier_list); j < jn; ++j) + { + if (!tr_variantGetStrView(tr_variantListChild(tier_list, j), &url)) { - url = tr_strvStrip(url); - - if (tr_urlIsValidTracker(url)) - { - tr_tracker_info* t = trackers + trackerCount; - t->tier = validTiers; - t->announce = tr_strvDup(url); - t->scrape = tr_convertAnnounceToScrape(url); - t->id = trackerCount; - - anyAdded = true; - ++trackerCount; - } + continue; } - } - if (anyAdded) - { - ++validTiers; + inf->announce_list->add(i, url); } } - - /* did we use any of the tiers? */ - if (trackerCount == 0) - { - tr_free(trackers); - trackers = nullptr; - } } /* Regular announce value */ - if (trackerCount == 0 && tr_variantDictFindStrView(meta, TR_KEY_announce, &url)) + if (std::empty(*inf->announce_list) && tr_variantDictFindStrView(meta, TR_KEY_announce, &url)) { - url = tr_strvStrip(url); - - if (tr_urlIsValidTracker(url)) - { - trackers = tr_new0(tr_tracker_info, 1); - trackers[trackerCount].tier = 0; - trackers[trackerCount].announce = tr_strvDup(url); - trackers[trackerCount].scrape = tr_convertAnnounceToScrape(url); - trackers[trackerCount].id = 0; - trackerCount++; - } + inf->announce_list->add(0, url); } - inf->trackers = trackers; - inf->trackerCount = trackerCount; - return nullptr; } @@ -630,33 +552,39 @@ std::optional tr_metainfoParse(tr_session const* session, tr void tr_metainfoFree(tr_info* inf) { - for (unsigned int i = 0; i < inf->webseedCount; i++) + if (inf->webseeds != nullptr) { - tr_free(inf->webseeds[i]); + 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++) + if (inf->files != nullptr) { - tr_free(inf->files[ff].name); + 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->files); + tr_free(inf->name); tr_free(inf->source); tr_free(inf->torrent); - tr_free(inf->name); + tr_free(inf->webseeds); - for (unsigned int i = 0; i < inf->trackerCount; i++) - { - tr_free(inf->trackers[i].announce); - tr_free(inf->trackers[i].scrape); - } + inf->comment = nullptr; + inf->creator = nullptr; + inf->files = nullptr; + inf->name = nullptr; + inf->source = nullptr; + inf->torrent = nullptr; + inf->webseeds = nullptr; - tr_free(inf->trackers); - - memset(inf, '\0', sizeof(tr_info)); + inf->announce_list.reset(); } void tr_metainfoRemoveSaved(tr_session const* session, tr_info const* inf) diff --git a/libtransmission/rpcimpl.cc b/libtransmission/rpcimpl.cc index 7d97e5f9f..91f157d57 100644 --- a/libtransmission/rpcimpl.cc +++ b/libtransmission/rpcimpl.cc @@ -413,16 +413,16 @@ static void addWebseeds(tr_info const* info, tr_variant* webseeds) } } -static void addTrackers(tr_info const* info, tr_variant* trackers) +static void addTrackers(tr_torrent const* tor, tr_variant* trackers) { - for (unsigned int i = 0; i < info->trackerCount; ++i) + for (size_t i = 0, n = tor->trackerCount(); i < n; ++i) { - tr_tracker_info const* t = &info->trackers[i]; + auto const info = tor->tracker(i); tr_variant* d = tr_variantListAddDict(trackers, 4); - tr_variantDictAddStrView(d, TR_KEY_announce, t->announce); - tr_variantDictAddInt(d, TR_KEY_id, t->id); - tr_variantDictAddStrView(d, TR_KEY_scrape, t->scrape != nullptr ? t->scrape : ""sv); - tr_variantDictAddInt(d, TR_KEY_tier, t->tier); + tr_variantDictAddQuark(d, TR_KEY_announce, info.announce_interned); + tr_variantDictAddInt(d, TR_KEY_id, info.id); + tr_variantDictAddQuark(d, TR_KEY_scrape, info.scrape_interned); + tr_variantDictAddInt(d, TR_KEY_tier, info.tier); } } @@ -781,8 +781,8 @@ static void initField( break; case TR_KEY_trackers: - tr_variantInitList(initme, inf->trackerCount); - addTrackers(inf, initme); + tr_variantInitList(initme, tor->trackerCount()); + addTrackers(tor, initme); break; case TR_KEY_trackerStats: @@ -1056,175 +1056,79 @@ static char const* setFileDLs(tr_torrent* tor, bool wanted, tr_variant* list) return errmsg; } -static bool hasAnnounceUrl(tr_tracker_info const* t, int n, std::string_view url) -{ - return std::any_of(t, t + n, [&url](auto const& row) { return row.announce == url; }); -} - -static int copyTrackers(tr_tracker_info* tgt, tr_tracker_info const* src, int n) -{ - int maxTier = -1; - - for (int i = 0; i < n; ++i) - { - tgt[i].tier = src[i].tier; - tgt[i].announce = tr_strdup(src[i].announce); - maxTier = std::max(maxTier, src[i].tier); - } - - return maxTier; -} - -static void freeTrackers(tr_tracker_info* trackers, int n) -{ - for (int i = 0; i < n; ++i) - { - tr_free(trackers[i].announce); - } - - tr_free(trackers); -} - static char const* addTrackerUrls(tr_torrent* tor, tr_variant* urls) { - char const* errmsg = nullptr; + auto const old_size = tor->trackerCount(); - /* make a working copy of the existing announce list */ - auto const* const inf = tr_torrentInfo(tor); - int n = inf->trackerCount; - auto* const trackers = tr_new0(tr_tracker_info, n + tr_variantListSize(urls)); - int tier = copyTrackers(trackers, inf->trackers, n); - - /* and add the new ones */ - auto i = int{ 0 }; - auto changed = bool{ false }; - tr_variant const* val = nullptr; - while ((val = tr_variantListChild(urls, i)) != nullptr) + for (size_t i = 0, n = tr_variantListSize(urls); i < n; ++i) { - auto announce = std::string_view{}; - - if (tr_variantGetStrView(val, &announce) && tr_urlIsValidTracker(announce) && !hasAnnounceUrl(trackers, n, announce)) - { - trackers[n].tier = ++tier; /* add a new tier */ - trackers[n].announce = tr_strvDup(announce); - ++n; - changed = true; - } - - ++i; - } - - if (!changed) - { - errmsg = "invalid argument"; - } - else if (!tr_torrentSetAnnounceList(tor, trackers, n)) - { - errmsg = "error setting announce list"; - } - - freeTrackers(trackers, n); - return errmsg; -} - -static char const* replaceTrackers(tr_torrent* tor, tr_variant* urls) -{ - char const* errmsg = nullptr; - - /* make a working copy of the existing announce list */ - auto const* const inf = tr_torrentInfo(tor); - int const n = inf->trackerCount; - auto* const trackers = tr_new0(tr_tracker_info, n); - copyTrackers(trackers, inf->trackers, n); - - /* make the substitutions... */ - bool changed = false; - for (size_t i = 0, url_count = tr_variantListSize(urls); i + 1 < url_count; i += 2) - { - auto pos = int64_t{}; - auto newval = std::string_view{}; - - if (tr_variantGetInt(tr_variantListChild(urls, i), &pos) && - tr_variantGetStrView(tr_variantListChild(urls, i + 1), &newval) && tr_urlIsValidTracker(newval) && pos < n && - pos >= 0) - { - tr_free(trackers[pos].announce); - trackers[pos].announce = tr_strvDup(newval); - changed = true; - } - } - - if (!changed) - { - errmsg = "invalid argument"; - } - else if (!tr_torrentSetAnnounceList(tor, trackers, n)) - { - errmsg = "error setting announce list"; - } - - freeTrackers(trackers, n); - return errmsg; -} - -static char const* removeTrackers(tr_torrent* tor, tr_variant* ids) -{ - /* make a working copy of the existing announce list */ - tr_info const* inf = tr_torrentInfo(tor); - int n = inf->trackerCount; - int* tids = tr_new0(int, n); - tr_tracker_info* trackers = tr_new0(tr_tracker_info, n); - copyTrackers(trackers, inf->trackers, n); - - /* remove the ones specified in the urls list */ - int i = 0; - int t = 0; - tr_variant const* val = nullptr; - while ((val = tr_variantListChild(ids, i)) != nullptr) - { - auto pos = int64_t{}; - - if (tr_variantGetInt(val, &pos) && 0 <= pos && pos < n) - { - tids[t++] = (int)pos; - } - - ++i; - } - - /* sort trackerIds and remove from largest to smallest so there is no need to recalculate array indicies */ - std::sort(tids, tids + t); - - bool changed = false; - int dup = -1; - while (t-- != 0) - { - /* check for duplicates */ - if (tids[t] == dup) + auto announce = std::string_view(); + auto* const val = tr_variantListChild(urls, i); + if (val == nullptr || !tr_variantGetStrView(val, &announce)) { continue; } - tr_removeElementFromArray(trackers, tids[t], sizeof(tr_tracker_info), n); - --n; - - dup = tids[t]; - changed = true; + tor->info.announce_list->add(tor->info.announce_list->nextTier(), announce); + } + + if (tor->trackerCount() == old_size) + { + return "error setting announce list"; + } + + tor->info.announce_list->save(tor->info.torrent); + return nullptr; +} + +static char const* replaceTrackers(tr_torrent* tor, tr_variant* urls) +{ + auto changed = bool{ false }; + + for (size_t i = 0, url_count = tr_variantListSize(urls); i + 1 < url_count; i += 2) + { + auto id = int64_t{}; + auto newval = std::string_view{}; + + if (tr_variantGetInt(tr_variantListChild(urls, i), &id) && + tr_variantGetStrView(tr_variantListChild(urls, i + 1), &newval)) + { + changed |= tor->info.announce_list->replace(id, newval); + } } - char const* errmsg = nullptr; if (!changed) { - errmsg = "invalid argument"; - } - else if (!tr_torrentSetAnnounceList(tor, trackers, n)) - { - errmsg = "error setting announce list"; + return "error setting announce list"; } - freeTrackers(trackers, n); - tr_free(tids); - return errmsg; + tor->info.announce_list->save(tor->info.torrent); + return nullptr; +} + +static char const* removeTrackers(tr_torrent* tor, tr_variant* ids) +{ + auto const old_size = tor->trackerCount(); + + for (size_t i = 0, n = tr_variantListSize(ids); i < n; ++i) + { + auto id = int64_t{}; + auto* const val = tr_variantListChild(ids, i); + if (val == nullptr || !tr_variantGetInt(val, &id)) + { + continue; + } + + tor->info.announce_list->remove(id); + } + + if (tor->trackerCount() == old_size) + { + return "error setting announce list"; + } + + tor->info.announce_list->save(tor->info.torrent); + return nullptr; } static char const* torrentSet( diff --git a/libtransmission/torrent-magnet.cc b/libtransmission/torrent-magnet.cc index 071167785..8e2e523e7 100644 --- a/libtransmission/torrent-magnet.cc +++ b/libtransmission/torrent-magnet.cc @@ -399,10 +399,10 @@ char* tr_torrentInfoGetMagnetLink(tr_info const* inf) tr_http_escape(s, name, true); } - for (unsigned int i = 0; i < inf->trackerCount; ++i) + for (size_t i = 0, n = std::size(*inf->announce_list); i < n; ++i) { evbuffer_add_printf(s, "%s", "&tr="); - tr_http_escape(s, inf->trackers[i].announce, true); + tr_http_escape(s, inf->announce_list->at(i).announce.full, true); } for (unsigned int i = 0; i < inf->webseedCount; i++) diff --git a/libtransmission/torrent.cc b/libtransmission/torrent.cc index 30c244e68..e868c3c07 100644 --- a/libtransmission/torrent.cc +++ b/libtransmission/torrent.cc @@ -764,7 +764,7 @@ static void torrentInit(tr_torrent* tor, tr_ctor const* ctor) tr_error_clear(&error); } - tor->tiers = tr_announcerAddTorrent(tor, onTrackerResponse, nullptr); + tor->announcer_tiers = tr_announcerAddTorrent(tor, onTrackerResponse, nullptr); if (isNewTorrent) { @@ -2138,114 +2138,40 @@ bool tr_torrent::checkPiece(tr_piece_index_t piece) **** ***/ -static int compareTrackerByTier(void const* va, void const* vb) -{ - auto const* const a = static_cast(va); - auto const* const b = static_cast(vb); - - /* sort by tier */ - if (a->tier != b->tier) - { - return a->tier - b->tier; - } - - /* get the effects of a stable sort by comparing the two elements' addresses */ - return a - b; -} - -bool tr_torrentSetAnnounceList(tr_torrent* tor, tr_tracker_info const* trackers_in, int trackerCount) +bool tr_torrentSetAnnounceList(tr_torrent* tor, char const* const* announce_urls, tr_tracker_tier_t const* tiers, size_t n) { TR_ASSERT(tr_isTorrent(tor)); auto const lock = tor->unique_lock(); - auto metainfo = tr_variant{}; - auto ok = bool{ true }; - - /* ensure the trackers' tiers are in ascending order */ - auto* trackers = static_cast(tr_memdup(trackers_in, sizeof(tr_tracker_info) * trackerCount)); - qsort(trackers, trackerCount, sizeof(tr_tracker_info), compareTrackerByTier); - - /* look for bad URLs */ - for (int i = 0; ok && i < trackerCount; ++i) + auto announce_list = tr_announce_list(); + if (!announce_list.set(announce_urls, tiers, n) || !announce_list.save(tor->info.torrent)) { - if (!tr_urlIsValidTracker(trackers[i].announce)) + return false; + } + + std::swap(*tor->info.announce_list, announce_list); + tr_torrentMarkEdited(tor); + + /* if we had a tracker-related error on this torrent, + * and that tracker's been removed, + * then clear the error */ + if (tor->error == TR_STAT_TRACKER_WARNING || tor->error == TR_STAT_TRACKER_ERROR) + { + auto const error_url = tor->error_announce_url; + + if (std::any_of( + std::begin(*tor->info.announce_list), + std::end(*tor->info.announce_list), + [error_url](auto const& tracker) { return tracker.announce_interned == error_url; })) { - ok = false; + tr_torrentClearError(tor); } } - /* save to the .torrent file */ - if (ok && tr_variantFromFile(&metainfo, TR_VARIANT_PARSE_BENC, tor->info.torrent, nullptr)) - { - /* remove the old fields */ - tr_variantDictRemove(&metainfo, TR_KEY_announce); - tr_variantDictRemove(&metainfo, TR_KEY_announce_list); + /* tell the announcer to reload this torrent's tracker list */ + tr_announcerResetTorrent(tor->session->announcer, tor); - /* add the new fields */ - if (trackerCount > 0) - { - tr_variantDictAddStr(&metainfo, TR_KEY_announce, trackers[0].announce); - } - - if (trackerCount > 1) - { - int prevTier = -1; - tr_variant* tier = nullptr; - tr_variant* announceList = tr_variantDictAddList(&metainfo, TR_KEY_announce_list, 0); - - for (int i = 0; i < trackerCount; ++i) - { - if (prevTier != trackers[i].tier) - { - prevTier = trackers[i].tier; - tier = tr_variantListAddList(announceList, 0); - } - - tr_variantListAddStr(tier, trackers[i].announce); - } - } - - /* try to parse it back again, to make sure it's good */ - auto parsed = tr_metainfoParse(tor->session, &metainfo, nullptr); - if (parsed) - { - /* it's good, so keep these new trackers and free the old ones */ - std::swap(tor->info.trackers, parsed->info.trackers); - std::swap(tor->info.trackerCount, parsed->info.trackerCount); - tr_torrentMarkEdited(tor); - tr_variantToFile(&metainfo, TR_VARIANT_FMT_BENC, tor->info.torrent); - } - - /* cleanup */ - tr_variantFree(&metainfo); - - /* if we had a tracker-related error on this torrent, - * and that tracker's been removed, - * then clear the error */ - if (tor->error == TR_STAT_TRACKER_WARNING || tor->error == TR_STAT_TRACKER_ERROR) - { - bool clear = true; - - for (int i = 0; clear && i < trackerCount; ++i) - { - if (strcmp(trackers[i].announce, tr_quark_get_string(tor->error_announce_url)) == 0) - { - clear = false; - } - } - - if (clear) - { - tr_torrentClearError(tor); - } - } - - /* tell the announcer to reload this torrent's tracker list */ - tr_announcerResetTorrent(tor->session->announcer, tor); - } - - tr_free(trackers); - return ok; + return true; } /** diff --git a/libtransmission/torrent.h b/libtransmission/torrent.h index 593e493ad..003381139 100644 --- a/libtransmission/torrent.h +++ b/libtransmission/torrent.h @@ -20,6 +20,7 @@ #include "transmission.h" +#include "announce-list.h" #include "bandwidth.h" #include "bitfield.h" #include "block-info.h" @@ -36,7 +37,7 @@ struct tr_magnet_info; struct tr_metainfo_parsed; struct tr_session; struct tr_torrent; -struct tr_torrent_tiers; +struct tr_announcer_tiers; /** *** Package-visible ctor API @@ -327,6 +328,23 @@ public: std::optional findFile(std::string& filename, tr_file_index_t i) const; + /// TRACKERS + + size_t trackerCount() const + { + return std::size(*info.announce_list); + } + + auto const& tracker(size_t i) const + { + return info.announce_list->at(i); + } + + auto tiers() const + { + return info.announce_list->tiers(); + } + /// WEBSEEDS auto webseedCount() const @@ -398,7 +416,7 @@ public: tr_session* session = nullptr; - struct tr_torrent_tiers* tiers = nullptr; + tr_announcer_tiers* announcer_tiers = nullptr; // Changed to non-owning pointer temporarily till tr_torrent becomes C++-constructible and destructible // TODO: change tr_bandwidth* to owning pointer to the bandwidth, or remove * and own the value diff --git a/libtransmission/transmission.h b/libtransmission/transmission.h index ef91b255b..b16d6b074 100644 --- a/libtransmission/transmission.h +++ b/libtransmission/transmission.h @@ -20,6 +20,7 @@ **** ***/ +#include #include /* bool */ #include /* size_t */ #include /* uintN_t */ @@ -34,6 +35,9 @@ using tr_piece_index_t = uint32_t; using tr_block_index_t = uint32_t; using tr_port = uint16_t; using tr_tracker_tier_t = uint32_t; +using tr_tracker_id_t = uint32_t; + +#include "announce-list.h" struct tr_block_span_t { @@ -1215,15 +1219,6 @@ static inline char* tr_torrentGetMagnetLink(tr_torrent const* tor) *** **/ -/** @brief a part of tr_info that represents a single tracker */ -struct tr_tracker_info -{ - int tier; - char* announce; - char* scrape; - uint32_t id; /* unique identifier used to match to a tr_tracker_stat */ -}; - /** * @brief Modify a torrent's tracker list. * @@ -1231,13 +1226,11 @@ struct tr_tracker_info * and the metainfo file in tr_sessionGetConfigDir()'s torrent subdirectory. * * @param torrent The torrent whose tracker list is to be modified - * @param trackers An array of trackers, sorted by tier from first to last. - * NOTE: only the `tier' and `announce' fields are used. - * libtransmission derives `scrape' from `announce' - * and reassigns 'id'. - * @param trackerCount size of the `trackers' array + * @param urls Array of n announce url strings + * @param tiers Array of n tier numbers for grouping 'urls' into tiers + * @param n the number of urls/tiers */ -bool tr_torrentSetAnnounceList(tr_torrent* torrent, tr_tracker_info const* trackers, int trackerCount); +bool tr_torrentSetAnnounceList(tr_torrent* torrent, char const* const* announce_urls, tr_tracker_tier_t const* tiers, size_t n); /** *** @@ -1522,6 +1515,7 @@ struct tr_file_priv time_t mtime; bool is_renamed; // true if we're using a different path from the one in the metainfo; ie, if the user has renamed it */ }; + /** @brief a part of tr_info that represents a single file of the torrent's content */ struct tr_file { @@ -1557,13 +1551,12 @@ struct tr_info // Use tr_torrentFile() and tr_torrentFileCount() instead. tr_file* files; - /* these trackers are sorted by tier */ - tr_tracker_info* trackers; + // TODO(ckerr) aggregate this directly, rather than using a shared_ptr, when tr_info is private + std::shared_ptr announce_list; /* Torrent info */ time_t dateCreated; - unsigned int trackerCount; unsigned int webseedCount; tr_file_index_t fileCount; uint32_t pieceSize; diff --git a/libtransmission/variant.h b/libtransmission/variant.h index ecc662ebd..ed41c389f 100644 --- a/libtransmission/variant.h +++ b/libtransmission/variant.h @@ -119,7 +119,6 @@ enum tr_variant_parse_opts TR_VARIANT_PARSE_INPLACE = (1 << 2) }; -/* TR_VARIANT_FMT_JSON_LEAN and TR_VARIANT_FMT_JSON are equivalent here. */ bool tr_variantFromFile(tr_variant* setme, tr_variant_parse_opts opts, char const* filename, struct tr_error** error = nullptr); bool tr_variantFromBuf( diff --git a/macosx/QuickLookPlugin/GeneratePreviewForURL.mm b/macosx/QuickLookPlugin/GeneratePreviewForURL.mm index e18653a8e..2cfd89c4c 100644 --- a/macosx/QuickLookPlugin/GeneratePreviewForURL.mm +++ b/macosx/QuickLookPlugin/GeneratePreviewForURL.mm @@ -156,21 +156,22 @@ OSStatus GeneratePreviewForURL(void* thisInterface, QLPreviewRequestRef preview, [lists addObject:listSection]; } - if (inf.trackerCount > 0) + if (!std::empty(*inf.announce_list)) { NSMutableString* listSection = [NSMutableString string]; [listSection appendString:@""]; - NSString* headerTitleString = inf.trackerCount == 1 ? + auto const n = std::size(*inf.announce_list); + NSString* headerTitleString = n == 1 ? NSLocalizedStringFromTableInBundle(@"1 Tracker", nil, bundle, "quicklook tracker header") : [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ Trackers", nil, bundle, "quicklook tracker header"), - [NSString formattedUInteger:inf.trackerCount]]; + [NSString formattedUInteger:n]]; [listSection appendFormat:@"", headerTitleString]; #warning handle tiers? - for (int i = 0; i < inf.trackerCount; ++i) + for (auto const tracker : *inf.announce_list) { - [listSection appendFormat:@"", inf.trackers[i].announce]; + [listSection appendFormat:@"", tr_quark_get_string(tracker.announce_interned)]; } [listSection appendString:@"
%@
%s
%s
"]; diff --git a/macosx/Torrent.mm b/macosx/Torrent.mm index 4a5a5bf1b..3bfdbecba 100644 --- a/macosx/Torrent.mm +++ b/macosx/Torrent.mm @@ -763,61 +763,63 @@ bool trashDataFile(char const* filename, tr_error** error) - (NSArray*)allTrackersFlat { - NSMutableArray* allTrackers = [NSMutableArray arrayWithCapacity:fInfo->trackerCount]; + auto const n = tr_torrentTrackerCount(fHandle); + NSMutableArray* allTrackers = [NSMutableArray arrayWithCapacity:n]; - for (NSInteger i = 0; i < fInfo->trackerCount; i++) + for (size_t i = 0; i < n; ++i) { - [allTrackers addObject:@(fInfo->trackers[i].announce)]; + [allTrackers addObject:@(tr_torrentTracker(fHandle, i).announce)]; } return allTrackers; } -- (BOOL)addTrackerToNewTier:(NSString*)tracker +- (BOOL)addTrackerToNewTier:(NSString*)new_tracker { - tracker = [tracker stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; - - if ([tracker rangeOfString:@"://"].location == NSNotFound) + new_tracker = [new_tracker stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; + if ([new_tracker rangeOfString:@"://"].location == NSNotFound) { - tracker = [@"http://" stringByAppendingString:tracker]; + new_tracker = [@"http://" stringByAppendingString:new_tracker]; } - //recreate the tracker structure - int const oldTrackerCount = fInfo->trackerCount; - tr_tracker_info* trackerStructs = tr_new(tr_tracker_info, oldTrackerCount + 1); - for (int i = 0; i < oldTrackerCount; ++i) + auto urls = std::vector{}; + auto tiers = std::vector{}; + + for (size_t i = 0, n = tr_torrentTrackerCount(fHandle); i < n; ++i) { - trackerStructs[i] = fInfo->trackers[i]; + auto const tracker = tr_torrentTracker(fHandle, i); + urls.push_back(tracker.announce); + tiers.push_back(tracker.tier); } - trackerStructs[oldTrackerCount].announce = (char*)tracker.UTF8String; - trackerStructs[oldTrackerCount].tier = trackerStructs[oldTrackerCount - 1].tier + 1; - trackerStructs[oldTrackerCount].id = oldTrackerCount; + urls.push_back(new_tracker.UTF8String); + tiers.push_back(std::empty(tiers) ? 0 : tiers.back() + 1); - BOOL const success = tr_torrentSetAnnounceList(fHandle, trackerStructs, oldTrackerCount + 1); - tr_free(trackerStructs); + BOOL const success = tr_torrentSetAnnounceList(fHandle, std::data(urls), std::data(tiers), std::size(urls)); return success; } - (void)removeTrackers:(NSSet*)trackers { - //recreate the tracker structure - tr_tracker_info* trackerStructs = tr_new(tr_tracker_info, fInfo->trackerCount); + auto urls = std::vector{}; + auto tiers = std::vector{}; - NSUInteger newCount = 0; - for (NSUInteger i = 0; i < fInfo->trackerCount; i++) + for (size_t i = 0, n = tr_torrentTrackerCount(fHandle); i < n; ++i) { - if (![trackers containsObject:@(fInfo->trackers[i].announce)]) + auto const tracker = tr_torrentTracker(fHandle, i); + + if ([trackers containsObject:@(tracker.announce)]) { - trackerStructs[newCount++] = fInfo->trackers[i]; + continue; } + + urls.push_back(tracker.announce); + tiers.push_back(tracker.tier); } - BOOL const success = tr_torrentSetAnnounceList(fHandle, trackerStructs, newCount); + BOOL const success = tr_torrentSetAnnounceList(fHandle, std::data(urls), std::data(tiers), std::size(urls)); NSAssert(success, @"Removing tracker addresses failed"); - - tr_free(trackerStructs); } - (NSString*)comment diff --git a/tests/libtransmission/CMakeLists.txt b/tests/libtransmission/CMakeLists.txt index 91a7360ae..e579b63f1 100644 --- a/tests/libtransmission/CMakeLists.txt +++ b/tests/libtransmission/CMakeLists.txt @@ -1,4 +1,5 @@ add_executable(libtransmission-test + announce-list-test.cc bitfield-test.cc block-info-test.cc blocklist-test.cc diff --git a/tests/libtransmission/announce-list-test.cc b/tests/libtransmission/announce-list-test.cc new file mode 100644 index 000000000..bc281a49e --- /dev/null +++ b/tests/libtransmission/announce-list-test.cc @@ -0,0 +1,404 @@ +/* + * This file Copyright (C) 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 "transmission.h" + +#include "announce-list.h" +#include "metainfo.h" +#include "utils.h" +#include "variant.h" + +#include "gtest/gtest.h" + +using AnnounceListTest = ::testing::Test; +using namespace std::literals; + +TEST_F(AnnounceListTest, canAdd) +{ + auto constexpr Tier = tr_tracker_tier_t{ 2 }; + auto constexpr Announce = "https://example.org/announce"sv; + + auto announce_list = tr_announce_list{}; + EXPECT_EQ(1, announce_list.add(Tier, Announce)); + auto const tracker = announce_list.at(0); + EXPECT_EQ(Announce, tracker.announce.full); + EXPECT_EQ("https://example.org/scrape"sv, tracker.scrape.full); + EXPECT_EQ(Tier, tracker.tier); + EXPECT_EQ("example.org:443"sv, tr_quark_get_string_view(tracker.host)); +} + +TEST_F(AnnounceListTest, groupsSiblingsIntoSameTier) +{ + auto constexpr Tier1 = tr_tracker_tier_t{ 1 }; + auto constexpr Tier2 = tr_tracker_tier_t{ 2 }; + auto constexpr Tier3 = tr_tracker_tier_t{ 3 }; + auto constexpr Announce1 = "https://example.org/announce"sv; + auto constexpr Announce2 = "http://example.org/announce"sv; + auto constexpr Announce3 = "udp://example.org:999/announce"sv; + + auto announce_list = tr_announce_list{}; + EXPECT_TRUE(announce_list.add(Tier1, Announce1)); + EXPECT_TRUE(announce_list.add(Tier2, Announce2)); + EXPECT_TRUE(announce_list.add(Tier3, Announce3)); + + EXPECT_EQ(3, std::size(announce_list)); + EXPECT_EQ(Tier1, announce_list.at(0).tier); + EXPECT_EQ(Tier1, announce_list.at(1).tier); + EXPECT_EQ(Tier1, announce_list.at(2).tier); + EXPECT_EQ(Announce1, announce_list.at(1).announce.full); + EXPECT_EQ(Announce2, announce_list.at(0).announce.full); + EXPECT_EQ(Announce3, announce_list.at(2).announce.full); + EXPECT_EQ("example.org:443"sv, tr_quark_get_string_view(announce_list.at(1).host)); + EXPECT_EQ("example.org:80"sv, tr_quark_get_string_view(announce_list.at(0).host)); + EXPECT_EQ("example.org:999"sv, tr_quark_get_string_view(announce_list.at(2).host)); +} + +TEST_F(AnnounceListTest, canAddWithoutScrape) +{ + auto constexpr Tier = tr_tracker_tier_t{ 2 }; + auto constexpr Announce = "https://example.org/foo"sv; + + auto announce_list = tr_announce_list{}; + EXPECT_TRUE(announce_list.add(Tier, Announce)); + auto const tracker = announce_list.at(0); + EXPECT_EQ(Announce, tracker.announce.full); + EXPECT_EQ(TR_KEY_NONE, tracker.scrape_interned); + EXPECT_EQ(Tier, tracker.tier); +} + +TEST_F(AnnounceListTest, canAddUdp) +{ + auto constexpr Tier = tr_tracker_tier_t{ 2 }; + auto constexpr Announce = "udp://example.org/"sv; + + auto announce_list = tr_announce_list{}; + EXPECT_TRUE(announce_list.add(Tier, Announce)); + auto const tracker = announce_list.at(0); + EXPECT_EQ(Announce, tracker.announce.full); + EXPECT_EQ("udp://example.org/"sv, tracker.scrape.full); + EXPECT_EQ(Tier, tracker.tier); +} + +TEST_F(AnnounceListTest, canNotAddDuplicateAnnounce) +{ + auto constexpr Tier = tr_tracker_tier_t{ 2 }; + auto constexpr Announce = "https://example.org/announce"sv; + + auto announce_list = tr_announce_list{}; + EXPECT_TRUE(announce_list.add(Tier, Announce)); + EXPECT_EQ(1, announce_list.size()); + EXPECT_FALSE(announce_list.add(Tier, Announce)); + EXPECT_EQ(1, announce_list.size()); + + auto constexpr Announce2 = "https://example.org:443/announce"sv; + EXPECT_FALSE(announce_list.add(Tier, Announce2)); + EXPECT_EQ(1, announce_list.size()); +} + +TEST_F(AnnounceListTest, canNotAddInvalidUrl) +{ + auto constexpr Tier = tr_tracker_tier_t{ 2 }; + auto constexpr Announce = "telnet://example.org/announce"sv; + + auto announce_list = tr_announce_list{}; + EXPECT_FALSE(announce_list.add(Tier, Announce)); + EXPECT_EQ(0, announce_list.size()); +} + +TEST_F(AnnounceListTest, canSet) +{ + auto constexpr Urls = std::array{ + "https://www.example.com/a/announce", + "https://www.example.com/b/announce", + "https://www.example.com/c/announce", + }; + auto constexpr Tiers = std::array{ 1, 2, 3 }; + + auto announce_list = tr_announce_list{}; + EXPECT_EQ(3, announce_list.set(std::data(Urls), std::data(Tiers), 3)); + EXPECT_EQ(3, announce_list.size()); + EXPECT_EQ(Tiers[0], announce_list.at(0).tier); + EXPECT_EQ(Tiers[1], announce_list.at(1).tier); + EXPECT_EQ(Tiers[2], announce_list.at(2).tier); + EXPECT_EQ(Urls[0], announce_list.at(0).announce.full); + EXPECT_EQ(Urls[1], announce_list.at(1).announce.full); + EXPECT_EQ(Urls[2], announce_list.at(2).announce.full); + auto const expected_tiers = std::set(std::begin(Tiers), std::end(Tiers)); + auto const tiers = announce_list.tiers(); + EXPECT_EQ(expected_tiers, tiers); +} + +TEST_F(AnnounceListTest, canSetUnsortedWithBackupsInTiers) +{ + auto constexpr Urls = std::array{ + "https://www.backup-a.com/announce", "https://www.backup-b.com/announce", "https://www.backup-c.com/announce", + "https://www.primary-a.com/announce", "https://www.primary-b.com/announce", "https://www.primary-c.com/announce", + }; + auto constexpr Tiers = std::array{ 0, 1, 2, 0, 1, 2 }; + + auto announce_list = tr_announce_list{}; + EXPECT_EQ(6, announce_list.set(std::data(Urls), std::data(Tiers), 6)); + EXPECT_EQ(6, announce_list.size()); + EXPECT_EQ(0, announce_list.at(0).tier); + EXPECT_EQ(0, announce_list.at(1).tier); + EXPECT_EQ(1, announce_list.at(2).tier); + EXPECT_EQ(1, announce_list.at(3).tier); + EXPECT_EQ(2, announce_list.at(4).tier); + EXPECT_EQ(2, announce_list.at(5).tier); + EXPECT_EQ(Urls[0], announce_list.at(0).announce.full); + EXPECT_EQ(Urls[3], announce_list.at(1).announce.full); + EXPECT_EQ(Urls[1], announce_list.at(2).announce.full); + EXPECT_EQ(Urls[4], announce_list.at(3).announce.full); + EXPECT_EQ(Urls[2], announce_list.at(4).announce.full); + EXPECT_EQ(Urls[5], announce_list.at(5).announce.full); + + // confirm that each has a unique id + auto ids = std::set{}; + for (size_t i = 0, n = std::size(announce_list); i < n; ++i) + { + ids.insert(announce_list.at(i).id); + } + EXPECT_EQ(std::size(announce_list), std::size(ids)); +} + +TEST_F(AnnounceListTest, canSetExceptDuplicate) +{ + auto constexpr Urls = std::array{ + "https://www.example.com/a/announce", + "https://www.example.com/b/announce", + "https://www.example.com/b/announce", + }; + auto constexpr Tiers = std::array{ 3, 2, 1 }; + + auto announce_list = tr_announce_list{}; + EXPECT_EQ(2, announce_list.set(std::data(Urls), std::data(Tiers), 3)); + EXPECT_EQ(2, announce_list.size()); + EXPECT_EQ(Tiers[0], announce_list.at(1).tier); + EXPECT_EQ(Tiers[1], announce_list.at(0).tier); + EXPECT_EQ(Urls[0], announce_list.at(1).announce.full); + EXPECT_EQ(Urls[1], announce_list.at(0).announce.full); +} + +TEST_F(AnnounceListTest, canSetExceptInvalid) +{ + auto constexpr Urls = std::array{ + "https://www.example.com/a/announce", + "telnet://www.example.com/b/announce", + "https://www.example.com/c/announce", + }; + auto constexpr Tiers = std::array{ 1, 2, 3 }; + + auto announce_list = tr_announce_list{}; + EXPECT_EQ(2, announce_list.set(std::data(Urls), std::data(Tiers), 3)); + EXPECT_EQ(2, announce_list.size()); + EXPECT_EQ(Tiers[0], announce_list.at(0).tier); + EXPECT_EQ(Tiers[2], announce_list.at(1).tier); + EXPECT_EQ(Urls[0], announce_list.at(0).announce.full); + EXPECT_EQ(Urls[2], announce_list.at(1).announce.full); +} + +TEST_F(AnnounceListTest, canRemoveById) +{ + auto constexpr Announce = "https://www.example.com/announce"sv; + auto constexpr Tier = tr_tracker_tier_t{ 1 }; + + auto announce_list = tr_announce_list{}; + announce_list.add(Tier, Announce); + EXPECT_EQ(1, std::size(announce_list)); + auto const id = announce_list.at(0).id; + + EXPECT_TRUE(announce_list.remove(id)); + EXPECT_EQ(0, std::size(announce_list)); +} + +TEST_F(AnnounceListTest, canNotRemoveByInvalidId) +{ + auto constexpr Announce = "https://www.example.com/announce"sv; + auto constexpr Tier = tr_tracker_tier_t{ 1 }; + + auto announce_list = tr_announce_list{}; + announce_list.add(Tier, Announce); + EXPECT_EQ(1, std::size(announce_list)); + auto const id = announce_list.at(0).id; + + EXPECT_FALSE(announce_list.remove(id + 1)); + EXPECT_EQ(1, std::size(announce_list)); + EXPECT_EQ(Announce, announce_list.at(0).announce.full); +} + +TEST_F(AnnounceListTest, canRemoveByAnnounce) +{ + auto constexpr Announce = "https://www.example.com/announce"sv; + auto constexpr Tier = tr_tracker_tier_t{ 1 }; + + auto announce_list = tr_announce_list{}; + announce_list.add(Tier, Announce); + EXPECT_EQ(1, std::size(announce_list)); + + EXPECT_TRUE(announce_list.remove(Announce)); + EXPECT_EQ(0, std::size(announce_list)); +} + +TEST_F(AnnounceListTest, canNotRemoveByInvalidAnnounce) +{ + auto constexpr Announce = "https://www.example.com/announce"sv; + auto constexpr Tier = tr_tracker_tier_t{ 1 }; + + auto announce_list = tr_announce_list{}; + announce_list.add(Tier, Announce); + EXPECT_EQ(1, std::size(announce_list)); + + EXPECT_FALSE(announce_list.remove("https://www.not-example.com/announce"sv)); + EXPECT_EQ(1, std::size(announce_list)); +} + +TEST_F(AnnounceListTest, canReplace) +{ + auto constexpr Tier = tr_tracker_tier_t{ 1 }; + auto constexpr Announce1 = "https://www.example.com/1/announce"sv; + auto constexpr Announce2 = "https://www.example.com/2/announce"sv; + + auto announce_list = tr_announce_list{}; + EXPECT_TRUE(announce_list.add(Tier, Announce1)); + EXPECT_TRUE(announce_list.replace(announce_list.at(0).id, Announce2)); + EXPECT_EQ(Announce2, announce_list.at(0).announce.full); +} + +TEST_F(AnnounceListTest, canNotReplaceInvalidId) +{ + auto constexpr Tier = tr_tracker_tier_t{ 1 }; + auto constexpr Announce1 = "https://www.example.com/1/announce"sv; + auto constexpr Announce2 = "https://www.example.com/2/announce"sv; + + auto announce_list = tr_announce_list{}; + EXPECT_TRUE(announce_list.add(Tier, Announce1)); + EXPECT_FALSE(announce_list.replace(announce_list.at(0).id + 1, Announce2)); + EXPECT_EQ(Announce1, announce_list.at(0).announce.full); +} + +TEST_F(AnnounceListTest, canNotReplaceWithInvalidAnnounce) +{ + auto constexpr Tier = tr_tracker_tier_t{ 1 }; + auto constexpr Announce1 = "https://www.example.com/1/announce"sv; + auto constexpr Announce2 = "telnet://www.example.com/2/announce"sv; + + auto announce_list = tr_announce_list{}; + EXPECT_TRUE(announce_list.add(Tier, Announce1)); + EXPECT_FALSE(announce_list.replace(announce_list.at(0).id, Announce2)); + EXPECT_EQ(Announce1, announce_list.at(0).announce.full); +} + +TEST_F(AnnounceListTest, canNotReplaceWithDuplicate) +{ + auto constexpr Tier = tr_tracker_tier_t{ 1 }; + auto constexpr Announce = "https://www.example.com/announce"sv; + + auto announce_list = tr_announce_list{}; + EXPECT_TRUE(announce_list.add(Tier, Announce)); + EXPECT_FALSE(announce_list.replace(announce_list.at(0).id, Announce)); + EXPECT_EQ(Announce, announce_list.at(0).announce.full); +} + +TEST_F(AnnounceListTest, announceToScrape) +{ + struct ScrapeTest + { + std::string_view announce; + std::string_view expected_scrape; + }; + + auto constexpr Tests = std::array{ { + { "https://www.example.com/announce"sv, "https://www.example.com/scrape"sv }, + { "https://www.example.com/foo"sv, ""sv }, + { "udp://www.example.com:999/"sv, "udp://www.example.com:999/"sv }, + } }; + + for (auto const test : Tests) + { + auto const scrape = tr_announce_list::announceToScrape(tr_quark_new(test.announce)); + EXPECT_EQ(tr_quark_new(test.expected_scrape), scrape); + } +} + +TEST_F(AnnounceListTest, save) +{ + auto constexpr Urls = std::array{ + "https://www.example.com/a/announce", + "https://www.example.com/b/announce", + "https://www.example.com/c/announce", + }; + auto constexpr Tiers = std::array{ 0, 1, 2 }; + + // first, set up a scratch .torrent + auto constexpr* const OriginalFile = LIBTRANSMISSION_TEST_ASSETS_DIR "/Android-x86 8.1 r6 iso.torrent"; + auto original_content = std::vector{}; + auto const test_file = tr_strvJoin(::testing::TempDir(), "transmission-announce-list-test.torrent"sv); + tr_error* error = nullptr; + EXPECT_TRUE(tr_loadFile(original_content, OriginalFile, &error)); + EXPECT_EQ(nullptr, error); + EXPECT_TRUE(tr_saveFile(test_file.c_str(), { std::data(original_content), std::size(original_content) }, &error)); + EXPECT_EQ(nullptr, error); + + // make an announce_list for it + auto announce_list = tr_announce_list(); + EXPECT_TRUE(announce_list.add(Tiers[0], Urls[0])); + EXPECT_TRUE(announce_list.add(Tiers[1], Urls[1])); + EXPECT_TRUE(announce_list.add(Tiers[2], Urls[2])); + + // try saving to a nonexistent .torrent file + EXPECT_FALSE(announce_list.save("/this/path/does/not/exist", &error)); + EXPECT_NE(nullptr, error); + EXPECT_NE(0, error->code); + tr_error_clear(&error); + + // now save to a real .torrent fi le + EXPECT_TRUE(announce_list.save(test_file.c_str(), &error)); + EXPECT_EQ(nullptr, error); + + // load the original + auto metainfo = tr_variant{}; + EXPECT_TRUE(tr_variantFromBuf( + &metainfo, + TR_VARIANT_PARSE_BENC | TR_VARIANT_PARSE_INPLACE, + { std::data(original_content), std::size(original_content) }, + nullptr, + nullptr)); + auto original = tr_metainfoParse(nullptr, &metainfo, nullptr); + EXPECT_TRUE(original); + tr_variantFree(&metainfo); + + // load the scratch that we saved to + metainfo = tr_variant{}; + EXPECT_TRUE(tr_variantFromFile(&metainfo, TR_VARIANT_PARSE_BENC, test_file.c_str(), nullptr)); + auto saved = tr_metainfoParse(nullptr, &metainfo, nullptr); + EXPECT_TRUE(saved); + tr_variantFree(&metainfo); + + // test that non-announce parts of the metainfo are the same + EXPECT_STREQ(original->info.name, saved->info.name); + EXPECT_EQ(original->info.fileCount, saved->info.fileCount); + EXPECT_EQ(original->info.dateCreated, saved->info.dateCreated); + EXPECT_EQ(original->pieces, saved->pieces); + + // test that the saved version has the updated announce list + EXPECT_EQ(std::size(announce_list), std::size(*saved->info.announce_list)); + EXPECT_TRUE(std::equal( + std::begin(announce_list), + std::end(announce_list), + std::begin(*saved->info.announce_list), + std::end(*saved->info.announce_list))); + + // cleanup + std::remove(test_file.c_str()); +} diff --git a/tests/libtransmission/magnet-metainfo-test.cc b/tests/libtransmission/magnet-metainfo-test.cc index 52e5384ee..eb6e6f14a 100644 --- a/tests/libtransmission/magnet-metainfo-test.cc +++ b/tests/libtransmission/magnet-metainfo-test.cc @@ -46,15 +46,15 @@ TEST(MagnetMetainfo, magnetParse) auto mm = tr_magnet_metainfo{}; EXPECT_TRUE(mm.parseMagnet(uri)); - EXPECT_EQ(2, std::size(mm.trackers)); - auto it = std::begin(mm.trackers); - EXPECT_EQ(0, it->first); - EXPECT_EQ("http://tracker.openbittorrent.com/announce"sv, tr_quark_get_string_view(it->second.announce_url)); - EXPECT_EQ("http://tracker.openbittorrent.com/scrape"sv, tr_quark_get_string_view(it->second.scrape_url)); + EXPECT_EQ(2, std::size(mm.announce_list)); + auto it = std::begin(mm.announce_list); + 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); ++it; - EXPECT_EQ(1, it->first); - EXPECT_EQ("http://tracker.opentracker.org/announce", tr_quark_get_string_view(it->second.announce_url)); - EXPECT_EQ("http://tracker.opentracker.org/scrape", tr_quark_get_string_view(it->second.scrape_url)); + 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); diff --git a/tests/libtransmission/makemeta-test.cc b/tests/libtransmission/makemeta-test.cc index ebb0b5ff3..dc6d38d50 100644 --- a/tests/libtransmission/makemeta-test.cc +++ b/tests/libtransmission/makemeta-test.cc @@ -82,7 +82,7 @@ protected: EXPECT_EQ(tr_file_index_t{ 1 }, inf.fileCount); EXPECT_EQ(isPrivate, inf.isPrivate); EXPECT_FALSE(inf.isFolder); - EXPECT_EQ(trackerCount, inf.trackerCount); + EXPECT_EQ(trackerCount, std::size(*inf.announce_list)); // cleanup tr_ctorFree(ctor); @@ -168,7 +168,7 @@ protected: EXPECT_EQ(payload_count, inf.fileCount); EXPECT_EQ(is_private, inf.isPrivate); EXPECT_EQ(builder->isFolder, inf.isFolder); - EXPECT_EQ(tracker_count, inf.trackerCount); + EXPECT_EQ(tracker_count, std::size(*inf.announce_list)); // cleanup tr_ctorFree(ctor); diff --git a/tests/libtransmission/metainfo-test.cc b/tests/libtransmission/metainfo-test.cc index 1e11ebc2a..3b865bc88 100644 --- a/tests/libtransmission/metainfo-test.cc +++ b/tests/libtransmission/metainfo-test.cc @@ -39,14 +39,15 @@ TEST(Metainfo, magnetLink) auto const parse_result = tr_torrentParse(ctor, &inf); EXPECT_EQ(TR_PARSE_OK, parse_result); EXPECT_EQ(0, inf.fileCount); // because it's a magnet link - EXPECT_EQ(2, inf.trackerCount); - if (inf.trackerCount >= 1) + auto const n = std::size(*inf.announce_list); + EXPECT_EQ(2, n); + if (n >= 1) { - EXPECT_STREQ("http://tracker.publicbt.com/announce", inf.trackers[0].announce); + EXPECT_EQ("http://tracker.publicbt.com/announce"sv, inf.announce_list->at(0).announce.full); } - if (inf.trackerCount >= 2) + if (n >= 2) { - EXPECT_STREQ("udp://tracker.publicbt.com:80", inf.trackers[1].announce); + EXPECT_EQ("udp://tracker.publicbt.com:80"sv, inf.announce_list->at(1).announce.full); } EXPECT_EQ(1, inf.webseedCount); if (inf.webseedCount >= 1) diff --git a/utils/show.cc b/utils/show.cc index 0f8f20d3a..8e75ac702 100644 --- a/utils/show.cc +++ b/utils/show.cc @@ -27,6 +27,8 @@ #define MY_NAME "transmission-show" #define TIMEOUT_SECS 30 +using namespace std::literals; + static tr_option options[] = { { 'm', "magnet", "Give a magnet link for the specified torrent", "m", false, nullptr }, { 's', "scrape", "Ask the torrent's trackers how many peers are in the torrent's swarm", "s", false, nullptr }, @@ -115,7 +117,6 @@ static void showInfo(tr_info const* inf) { char buf[128]; tr_file** files; - int prevTier = -1; /** *** General Info @@ -132,7 +133,7 @@ static void showInfo(tr_info const* inf) printf(" Comment: %s\n", inf->comment); } - printf(" Piece Count: %d\n", inf->pieceCount); + printf(" Piece Count: %zu\n", size_t(inf->pieceCount)); printf(" Piece Size: %s\n", tr_formatter_mem_B(buf, inf->pieceSize, sizeof(buf))); printf(" Total Size: %s\n", tr_formatter_size_B(buf, inf->totalSize, sizeof(buf))); printf(" Privacy: %s\n", inf->isPrivate ? "Private torrent" : "Public torrent"); @@ -142,16 +143,18 @@ static void showInfo(tr_info const* inf) **/ printf("\nTRACKERS\n"); - - for (unsigned int i = 0; i < inf->trackerCount; ++i) + auto current_tier = std::optional{}; + auto print_tier = size_t{ 1 }; + for (auto const& tracker : *inf->announce_list) { - if (prevTier != inf->trackers[i].tier) + if (!current_tier || current_tier != tracker.tier) { - prevTier = inf->trackers[i].tier; - printf("\n Tier #%d\n", prevTier + 1); + current_tier = tracker.tier; + printf("\n Tier #%zu\n", print_tier); + ++print_tier; } - printf(" %s\n", inf->trackers[i].announce); + printf(" %" TR_PRIsv "\n", TR_PRIsv_ARG(tracker.announce.full)); } /** @@ -215,33 +218,28 @@ static CURL* tr_curl_easy_init(struct evbuffer* writebuf) static void doScrape(tr_info const* inf) { - for (unsigned int i = 0; i < inf->trackerCount; ++i) + for (auto const& tracker : *inf->announce_list) { - CURL* curl; - CURLcode res; - struct evbuffer* buf; - char const* scrape = inf->trackers[i].scrape; - char* url; - char escaped[SHA_DIGEST_LENGTH * 3 + 1]; - - if (scrape == nullptr) + if (tracker.scrape_interned == TR_KEY_NONE) { continue; } + char escaped[SHA_DIGEST_LENGTH * 3 + 1]; tr_http_escape_sha1(escaped, inf->hash); + auto const scrape = tracker.scrape.full; + auto const url = tr_strvJoin(scrape, (tr_strvContains(scrape, '?') ? "&"sv : "?"sv), "info_hash="sv, escaped); - url = tr_strdup_printf("%s%cinfo_hash=%s", scrape, strchr(scrape, '?') != nullptr ? '&' : '?', escaped); - - printf("%s ... ", url); + printf("%" TR_PRIsv " ... ", TR_PRIsv_ARG(url)); fflush(stdout); - buf = evbuffer_new(); - curl = tr_curl_easy_init(buf); - curl_easy_setopt(curl, CURLOPT_URL, url); + auto* const buf = evbuffer_new(); + auto* const curl = tr_curl_easy_init(buf); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_TIMEOUT, TIMEOUT_SECS); - if ((res = curl_easy_perform(curl)) != CURLE_OK) + auto const res = curl_easy_perform(curl); + if (res != CURLE_OK) { printf("error: %s\n", curl_easy_strerror(res)); } @@ -305,7 +303,6 @@ static void doScrape(tr_info const* inf) curl_easy_cleanup(curl); evbuffer_free(buf); - tr_free(url); } } @@ -327,7 +324,7 @@ int tr_main(int argc, char* argv[]) if (showVersion) { - fprintf(stderr, MY_NAME " " LONG_VERSION_STRING "\n"); + fprintf(stderr, "%s %s\n", MY_NAME, LONG_VERSION_STRING); return EXIT_SUCCESS; }