feat: add support for adding torrents by raw hash values (#2608)

* Add support for adding torrents by raw hash values

Co-authored-by: vjunk <vjunk@mail.ru>
This commit is contained in:
Charles Kerr 2022-02-12 22:16:55 -06:00 committed by GitHub
parent dfe79af34c
commit 96178b1a9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 210 additions and 270 deletions

View File

@ -18,6 +18,7 @@
#include <libtransmission/utils.h> /* tr_wait_msec */
#include <libtransmission/variant.h>
#include <libtransmission/version.h>
#include <libtransmission/web-utils.h>
#include <libtransmission/web.h> /* tr_webRun */
/***
@ -216,8 +217,6 @@ static char const* getConfigDir(int argc, char const** argv)
int tr_main(int argc, char* argv[])
{
tr_session* h;
tr_ctor* ctor;
tr_variant settings;
char const* configDir;
@ -275,25 +274,20 @@ int tr_main(int argc, char* argv[])
}
}
h = tr_sessionInit(configDir, false, &settings);
ctor = tr_ctorNew(h);
auto* const h = tr_sessionInit(configDir, false, &settings);
auto* const ctor = tr_ctorNew(h);
tr_ctorSetPaused(ctor, TR_FORCE, false);
if (tr_sys_path_exists(torrentPath, nullptr))
if (tr_ctorSetMetainfoFromFile(ctor, torrentPath, nullptr) || tr_ctorSetMetainfoFromMagnetLink(ctor, torrentPath, nullptr))
{
tr_ctorSetMetainfoFromFile(ctor, torrentPath, nullptr);
// all good
}
else if (memcmp(torrentPath, "magnet:?", 8) == 0)
{
tr_ctorSetMetainfoFromMagnetLink(ctor, torrentPath, nullptr);
}
else if (memcmp(torrentPath, "http", 4) == 0)
else if (tr_urlIsValid(torrentPath))
{
// fetch it
tr_webRun(h, torrentPath, onTorrentFileDownloaded, ctor);
waitingOnWeb = true;
while (waitingOnWeb)
{
tr_wait_msec(1000);

View File

@ -461,32 +461,30 @@ TorrentFileChooserDialog::TorrentFileChooserDialog(Gtk::Window& parent, Glib::Re
void TorrentUrlChooserDialog::onOpenURLResponse(int response, Glib::RefPtr<Session> const& core)
{
bool handled = false;
if (response == Gtk::RESPONSE_ACCEPT)
{
auto* e = static_cast<Gtk::Entry*>(get_data("url-entry"));
auto const url = gtr_str_strip(e->get_text());
if (!url.empty())
{
handled = core->add_from_url(url);
if (!handled)
{
gtr_unrecognized_url_dialog(*this, url);
}
}
}
else if (response == Gtk::RESPONSE_CANCEL)
{
handled = true;
}
if (handled)
if (response == Gtk::RESPONSE_CANCEL)
{
hide();
}
else if (response == Gtk::RESPONSE_ACCEPT)
{
auto* const e = static_cast<Gtk::Entry*>(get_data("url-entry"));
auto const url = gtr_str_strip(e->get_text());
if (url.empty())
{
return;
}
if (core->add_from_url(url))
{
hide();
}
else
{
gtr_unrecognized_url_dialog(*this, url);
}
}
}
std::unique_ptr<TorrentUrlChooserDialog> TorrentUrlChooserDialog::create(Gtk::Window& parent, Glib::RefPtr<Session> const& core)

View File

@ -1128,69 +1128,47 @@ void Session::Impl::add_file_async_callback(
bool Session::Impl::add_file(Glib::RefPtr<Gio::File> const& file, bool do_start, bool do_prompt, bool do_notify)
{
bool handled = false;
if (auto const* const session = get_session(); session != nullptr)
auto const* const session = get_session();
if (session == nullptr)
{
tr_ctor* ctor;
bool tried = false;
bool loaded = false;
return false;
}
ctor = tr_ctorNew(session);
core_apply_defaults(ctor);
tr_ctorSetPaused(ctor, TR_FORCE, !do_start);
bool handled = false;
auto* ctor = tr_ctorNew(session);
core_apply_defaults(ctor);
tr_ctorSetPaused(ctor, TR_FORCE, !do_start);
/* local files... */
if (!tried)
{
auto const str = file->get_path();
bool loaded = false;
if (auto const path = file->get_path(); !std::empty(path))
{
// try to treat it as a file...
loaded = tr_ctorSetMetainfoFromFile(ctor, path.c_str(), nullptr);
}
if ((tried = !str.empty() && Glib::file_test(str, Glib::FILE_TEST_EXISTS)))
{
loaded = tr_ctorSetMetainfoFromFile(ctor, str.c_str(), nullptr);
}
}
if (!loaded)
{
// try to treat it as a magnet link...
loaded = tr_ctorSetMetainfoFromMagnetLink(ctor, file->get_uri().c_str(), nullptr);
}
/* magnet links... */
if (!tried && file->has_uri_scheme("magnet"))
{
/* GFile mangles the original string with /// so we have to un-mangle */
auto const str = file->get_parse_name();
auto const magnet = gtr_sprintf("magnet:%s", str.substr(str.find('?')));
tried = true;
loaded = tr_ctorSetMetainfoFromMagnetLink(ctor, magnet.c_str(), nullptr);
}
/* hashcodes that we can turn into magnet links... */
if (!tried)
{
auto const str = file->get_basename();
if (gtr_is_hex_hashcode(str))
{
auto const magnet = gtr_sprintf("magnet:?xt=urn:btih:%s", str);
loaded = tr_ctorSetMetainfoFromMagnetLink(ctor, magnet.c_str(), nullptr);
}
}
/* if we were able to load the metainfo, add the torrent */
if (loaded)
{
handled = true;
add_ctor(ctor, do_prompt, do_notify);
}
else if (file->has_uri_scheme("http") || file->has_uri_scheme("https") || file->has_uri_scheme("ftp"))
{
handled = true;
inc_busy();
file->load_contents_async([this, file, ctor, do_prompt, do_notify](auto& result)
{ add_file_async_callback(file, result, ctor, do_prompt, do_notify); });
}
else
{
tr_ctorFree(ctor);
g_message(_("Skipping unknown torrent \"%s\""), file->get_parse_name().c_str());
}
// if we could make sense of it, add it
if (loaded)
{
handled = true;
add_ctor(ctor, do_prompt, do_notify);
}
else if (tr_urlIsValid(file->get_uri()))
{
handled = true;
inc_busy();
file->load_contents_async([this, file, ctor, do_prompt, do_notify](auto& result)
{ add_file_async_callback(file, result, ctor, do_prompt, do_notify); });
}
else
{
tr_ctorFree(ctor);
g_message(_("Skipping unknown torrent \"%s\""), file->get_parse_name().c_str());
}
return handled;
@ -1203,15 +1181,13 @@ bool Session::add_from_url(Glib::ustring const& uri)
bool Session::Impl::add_from_url(Glib::ustring const& uri)
{
bool handled;
bool const do_start = gtr_pref_flag_get(TR_KEY_start_added_torrents);
bool const do_prompt = gtr_pref_flag_get(TR_KEY_show_options_window);
bool const do_notify = false;
auto const file = Gio::File::create_for_uri(uri);
handled = add_file(file, do_start, do_prompt, do_notify);
torrents_added();
auto const do_start = gtr_pref_flag_get(TR_KEY_start_added_torrents);
auto const do_prompt = gtr_pref_flag_get(TR_KEY_show_options_window);
auto const do_notify = false;
auto const handled = add_file(file, do_start, do_prompt, do_notify);
torrents_added();
return handled;
}

View File

@ -13,10 +13,12 @@
#include <glibmm/i18n.h>
#include <libtransmission/transmission.h> /* TR_RATIO_NA, TR_RATIO_INF */
#include <libtransmission/error.h>
#include <libtransmission/torrent-metainfo.h>
#include <libtransmission/utils.h> /* tr_strratio() */
#include <libtransmission/web-utils.h>
#include <libtransmission/version.h> /* SHORT_VERSION_STRING */
#include <libtransmission/web-utils.h>
#include "HigWorkarea.h"
#include "Prefs.h"
@ -126,40 +128,6 @@ Glib::ustring tr_strltime(time_t seconds)
namespace
{
bool gtr_is_supported_url(Glib::ustring const& str)
{
return !str.empty() &&
(Glib::str_has_prefix(str, "ftp://") || Glib::str_has_prefix(str, "http://") || Glib::str_has_prefix(str, "https://"));
}
} // namespace
bool gtr_is_magnet_link(Glib::ustring const& str)
{
return !str.empty() && Glib::str_has_prefix(str, "magnet:?");
}
bool gtr_is_hex_hashcode(std::string const& str)
{
if (str.size() != 40)
{
return false;
}
for (int i = 0; i < 40; ++i)
{
if (!isxdigit(str[i]))
{
return false;
}
}
return true;
}
namespace
{
Gtk::Window* getWindow(Gtk::Widget* w)
{
if (w == nullptr)
@ -476,8 +444,6 @@ void gtr_dialog_set_content(Gtk::Dialog& dialog, Gtk::Widget& content)
void gtr_unrecognized_url_dialog(Gtk::Widget& parent, Glib::ustring const& url)
{
char const* xt = "xt=urn:btih";
auto* window = getWindow(&parent);
Glib::ustring gstr;
@ -492,13 +458,10 @@ void gtr_unrecognized_url_dialog(Gtk::Widget& parent, Glib::ustring const& url)
gstr += gtr_sprintf(_("Transmission doesn't know how to use \"%s\""), url);
if (gtr_is_magnet_link(url) && url.find(xt) == Glib::ustring::npos)
if (tr_magnet_metainfo{}.parseMagnet(url.raw()))
{
gstr += "\n \n";
gstr += gtr_sprintf(
_("This magnet link appears to be intended for something other than BitTorrent. "
"BitTorrent magnet links have a section containing \"%s\"."),
xt);
gstr += _("This magnet link appears to be intended for something other than BitTorrent.");
}
w->set_secondary_text(gstr);
@ -510,19 +473,16 @@ void gtr_unrecognized_url_dialog(Gtk::Widget& parent, Glib::ustring const& url)
****
***/
void gtr_paste_clipboard_url_into_entry(Gtk::Entry& e)
void gtr_paste_clipboard_url_into_entry(Gtk::Entry& entry)
{
Glib::ustring const text[] = {
gtr_str_strip(Gtk::Clipboard::get(GDK_SELECTION_PRIMARY)->wait_for_text()),
gtr_str_strip(Gtk::Clipboard::get(GDK_SELECTION_CLIPBOARD)->wait_for_text()),
};
for (auto const& s : text)
for (auto const& str : { Gtk::Clipboard::get(GDK_SELECTION_PRIMARY)->wait_for_text(),
Gtk::Clipboard::get(GDK_SELECTION_CLIPBOARD)->wait_for_text() })
{
if (!s.empty() && (gtr_is_supported_url(s) || gtr_is_magnet_link(s) || gtr_is_hex_hashcode(s)))
auto const sv = tr_strvStrip(str.raw());
if (!sv.empty() && (tr_urlIsValid(sv) || tr_magnet_metainfo{}.parseMagnet(sv)))
{
e.set_text(s);
break;
entry.set_text(str);
return;
}
}
}

View File

@ -61,14 +61,6 @@ Glib::ustring tr_strltime(time_t secs);
****
***/
bool gtr_is_magnet_link(Glib::ustring const& str);
bool gtr_is_hex_hashcode(std::string const& str);
/***
****
***/
void gtr_open_uri(Glib::ustring const& uri);
void gtr_open_file(std::string const& path);

View File

@ -223,9 +223,17 @@ static void tr_hex_to_binary(char const* input, void* voutput, size_t byte_lengt
}
}
tr_sha1_digest_t tr_sha1_from_string(std::string_view hex)
std::optional<tr_sha1_digest_t> tr_sha1_from_string(std::string_view hex)
{
TR_ASSERT(std::size(hex) == TR_SHA1_DIGEST_STRLEN);
if (std::size(hex) != TR_SHA1_DIGEST_STRLEN)
{
return {};
}
if (!std::all_of(std::begin(hex), std::end(hex), [](unsigned char ch) { return isxdigit(ch); }))
{
return {};
}
auto digest = tr_sha1_digest_t{};
tr_hex_to_binary(std::data(hex), std::data(digest), std::size(digest));

View File

@ -194,7 +194,7 @@ std::string tr_sha1_to_string(tr_sha1_digest_t const&);
/**
* @brief Generate a sha1 digest from a hex string.
*/
tr_sha1_digest_t tr_sha1_from_string(std::string_view hex);
std::optional<tr_sha1_digest_t> tr_sha1_from_string(std::string_view hex);
/** @} */

View File

@ -5,6 +5,7 @@
#include <array>
#include <cstring>
#include <cctype> // isxdigit()
#include <string>
#include <string_view>
@ -21,10 +22,13 @@
using namespace std::literals;
/* this base32 code converted from code by Robert Kaye and Gordon Mohr
* and is public domain. see http://bitzi.com/publicdomain for more info */
namespace
{
auto constexpr Base32HashStrLen = size_t{ 32 };
/* this base32 code converted from code by Robert Kaye and Gordon Mohr
* and is public domain. see http://bitzi.com/publicdomain for more info */
namespace bitzi
{
@ -43,8 +47,6 @@ auto constexpr Base32Lookup = std::array<int, 80>{
void base32_to_sha1(uint8_t* out, char const* in, size_t const inlen)
{
TR_ASSERT(inlen == 32);
size_t const outlen = 20;
memset(out, 0, 20);
@ -105,6 +107,43 @@ void base32_to_sha1(uint8_t* out, char const* in, size_t const inlen)
}
} // namespace bitzi
std::optional<tr_sha1_digest_t> parseBase32Hash(std::string_view sv)
{
if (std::size(sv) != Base32HashStrLen)
{
return {};
}
if (!std::all_of(std::begin(sv), std::end(sv), [](unsigned char ch) { return bitzi::Base32Lookup[ch] - '0' != 0xFF; }))
{
return {};
}
auto digest = tr_sha1_digest_t{};
bitzi::base32_to_sha1(reinterpret_cast<uint8_t*>(std::data(digest)), std::data(sv), std::size(sv));
return digest;
}
std::optional<tr_sha1_digest_t> parseHash(std::string_view sv)
{
// http://bittorrent.org/beps/bep_0009.html
// Is the info-hash hex encoded, for a total of 40 characters.
// For compatability with existing links in the wild, clients
// should also support the 32 character base32 encoded info-hash.
if (auto const hash = tr_sha1_from_string(sv); hash)
{
return hash;
}
if (auto const hash = parseBase32Hash(sv); hash)
{
return hash;
}
return {};
}
} // namespace
/***
@ -141,6 +180,13 @@ std::string tr_magnet_metainfo::magnet() const
bool tr_magnet_metainfo::parseMagnet(std::string_view magnet_link, tr_error** error)
{
magnet_link = tr_strvStrip(magnet_link);
if (auto const hash = parseHash(magnet_link); hash)
{
return parseMagnet(tr_strvJoin("magnet:?xt=urn:btih:", tr_sha1_to_string(*hash)));
}
auto const parsed = tr_urlParse(magnet_link);
if (!parsed || parsed->scheme != "magnet"sv)
{
@ -148,7 +194,7 @@ bool tr_magnet_metainfo::parseMagnet(std::string_view magnet_link, tr_error** er
return false;
}
bool got_checksum = false;
bool got_hash = false;
for (auto const& [key, value] : tr_url_query_view{ parsed->query })
{
if (key == "dn"sv)
@ -174,24 +220,10 @@ bool tr_magnet_metainfo::parseMagnet(std::string_view magnet_link, tr_error** er
auto constexpr ValPrefix = "urn:btih:"sv;
if (tr_strvStartsWith(value, ValPrefix))
{
auto const hash = value.substr(std::size(ValPrefix));
switch (std::size(hash))
if (auto const hash = parseHash(value.substr(std::size(ValPrefix))); hash)
{
case TR_SHA1_DIGEST_STRLEN:
this->info_hash_ = tr_sha1_from_string(hash);
got_checksum = true;
break;
case 32:
bitzi::base32_to_sha1(
reinterpret_cast<uint8_t*>(std::data(this->info_hash_)),
std::data(hash),
std::size(hash));
got_checksum = true;
break;
default:
break;
this->info_hash_ = *hash;
got_hash = true;
}
}
}
@ -199,5 +231,10 @@ bool tr_magnet_metainfo::parseMagnet(std::string_view magnet_link, tr_error** er
info_hash_str_ = tr_sha1_to_string(this->infoHash());
return got_checksum;
if (std::empty(name()))
{
this->setName(info_hash_str_);
}
return got_hash;
}

View File

@ -20,6 +20,7 @@ class tr_magnet_metainfo
{
public:
bool parseMagnet(std::string_view magnet_link, tr_error** error = nullptr);
std::string magnet() const;
auto const& infoHash() const

View File

@ -1695,14 +1695,10 @@ static char const* torrentAdd(tr_session* session, tr_variant* args_in, tr_varia
// these two tr_ctorSet*() functions require zero-terminated strings
auto const filename_sz = std::string{ filename };
if (tr_strvStartsWith(filename, "magnet:?"sv))
if (!tr_ctorSetMetainfoFromFile(ctor, filename_sz.c_str(), nullptr))
{
tr_ctorSetMetainfoFromMagnetLink(ctor, filename_sz.c_str(), nullptr);
}
else
{
tr_ctorSetMetainfoFromFile(ctor, filename_sz.c_str(), nullptr);
}
}
addTorrentImpl(idle_data, ctor);

View File

@ -2783,7 +2783,6 @@ void tr_sessionRemoveTorrent(tr_session* session, tr_torrent* tor)
tr_torrent* tr_session::getTorrent(std::string_view info_dict_hash_string)
{
return std::size(info_dict_hash_string) == TR_SHA1_DIGEST_STRLEN ?
this->getTorrent(tr_sha1_from_string(info_dict_hash_string)) :
nullptr;
auto const info_hash = tr_sha1_from_string(info_dict_hash_string);
return info_hash ? this->getTorrent(*info_hash) : nullptr;
}

View File

@ -33,43 +33,34 @@ QString getNameFromMetainfo(QByteArray const& benc)
int AddData::set(QString const& key)
{
if (Utils::isMagnetLink(key))
if (auto const key_std = key.toStdString(); tr_urlIsValid(key_std))
{
magnet = key;
type = MAGNET;
}
else if (Utils::isUriWithSupportedScheme(key))
{
url = key;
this->url = key;
type = URL;
}
else if (QFile(key).exists())
{
filename = QDir::fromNativeSeparators(key);
this->filename = QDir::fromNativeSeparators(key);
type = FILENAME;
QFile file(key);
auto file = QFile{ key };
file.open(QIODevice::ReadOnly);
metainfo = file.readAll();
this->metainfo = file.readAll();
file.close();
}
else if (Utils::isHexHashcode(key))
else if (tr_magnet_metainfo{}.parseMagnet(key_std))
{
magnet = QStringLiteral("magnet:?xt=urn:btih:") + key;
type = MAGNET;
this->magnet = key;
this->type = MAGNET;
}
else if (auto const raw = QByteArray::fromBase64(key.toUtf8()); !raw.isEmpty())
{
this->metainfo.append(raw);
this->type = METAINFO;
}
else
{
auto raw = QByteArray::fromBase64(key.toUtf8());
if (!raw.isEmpty())
{
metainfo.append(raw);
type = METAINFO;
}
else
{
type = NONE;
}
this->type = NONE;
}
return type;

View File

@ -1545,7 +1545,7 @@ void MainWindow::dragEnterEvent(QDragEnterEvent* event)
if (mime->hasFormat(QStringLiteral("application/x-bittorrent")) || mime->hasUrls() ||
mime->text().trimmed().endsWith(QStringLiteral(".torrent"), Qt::CaseInsensitive) ||
mime->text().startsWith(QStringLiteral("magnet:"), Qt::CaseInsensitive))
tr_magnet_metainfo{}.parseMagnet(mime->text().toStdString()))
{
event->acceptProposedAction();
}
@ -1591,19 +1591,17 @@ bool MainWindow::event(QEvent* e)
}
if (auto const text = QGuiApplication::clipboard()->text().trimmed();
text.endsWith(QStringLiteral(".torrent"), Qt::CaseInsensitive) ||
text.startsWith(QStringLiteral("magnet:"), Qt::CaseInsensitive))
text.endsWith(QStringLiteral(".torrent"), Qt::CaseInsensitive) || tr_magnet_metainfo{}.parseMagnet(text.toStdString()))
{
for (QString const& entry : text.split(QLatin1Char('\n')))
for (auto const& entry : text.split(QLatin1Char('\n')))
{
QString key = entry.trimmed();
auto key = entry.trimmed();
if (key.isEmpty())
{
continue;
}
if (QUrl const url(key); url.isLocalFile())
if (auto const url = QUrl{ key }; url.isLocalFile())
{
key = url.toLocalFile();
}

View File

@ -121,12 +121,18 @@ public:
explicit TorrentHash(char const* str)
{
data_ = tr_sha1_from_string(str != nullptr ? str : "");
if (auto const hash = tr_sha1_from_string(str != nullptr ? str : ""); hash)
{
data_ = *hash;
}
}
explicit TorrentHash(QString const& str)
{
data_ = tr_sha1_from_string(str.toStdString());
if (auto const hash = tr_sha1_from_string(str.toStdString()); hash)
{
data_ = *hash;
}
}
bool operator==(TorrentHash const& that) const

View File

@ -74,37 +74,4 @@ public:
dialog->activateWindow();
}
}
///
/// URLs
///
static bool isMagnetLink(QString const& s)
{
return s.startsWith(QStringLiteral("magnet:?"));
}
static bool isHexHashcode(QString const& s)
{
if (s.length() != 40)
{
return false;
}
for (auto const& ch : s)
{
if (!isxdigit(ch.unicode()))
{
return false;
}
}
return true;
}
static bool isUriWithSupportedScheme(QString const& s)
{
return s.startsWith(QStringLiteral("ftp://")) || s.startsWith(QStringLiteral("http://")) ||
s.startsWith(QStringLiteral("https://"));
}
};

View File

@ -174,19 +174,26 @@ TEST(Crypto, ssha1)
EXPECT_TRUE(tr_ssha1_matches("{d209a21d3bc4f8fc4f8faf347e69f3def597eb170pySy4ai1ZPMjeU1", "test"));
}
TEST(Crypto, hex)
TEST(Crypto, sha1FromString)
{
auto constexpr Hex = std::array<std::string_view, 2>{
"a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"sv,
"d209a21d3bc4f8fc4f8faf347e69f3def597eb17"sv,
};
// bad lengths
EXPECT_FALSE(tr_sha1_from_string(""));
EXPECT_FALSE(tr_sha1_from_string("a94a8fe5ccb19ba61c4c0873d391e987982fbbd"sv));
EXPECT_FALSE(tr_sha1_from_string("a94a8fe5ccb19ba61c4c0873d391e987982fbbd33"sv));
// nonhex
EXPECT_FALSE(tr_sha1_from_string("a94a8fe5ccb19ba61c4cz873d391e987982fbbd3"sv));
EXPECT_FALSE(tr_sha1_from_string("a94a8fe5ccb19 61c4c0873d391e987982fbbd3"sv));
for (auto const& hex : Hex)
{
auto const digest = tr_sha1_from_string(hex);
auto const str = tr_sha1_to_string(digest);
EXPECT_EQ(hex, str);
}
// lowecase hex
auto const baseline = "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"sv;
auto const lc = tr_sha1_from_string(baseline);
EXPECT_TRUE(lc);
EXPECT_EQ(baseline, tr_sha1_to_string(*lc));
// uppercase hex should yield the same result
auto const uc = tr_sha1_from_string(tr_strupper(baseline));
EXPECT_TRUE(uc);
EXPECT_EQ(*lc, *uc);
}
TEST(Crypto, random)

View File

@ -78,4 +78,14 @@ TEST(MagnetMetainfo, magnetParse)
EXPECT_EQ("Display Name"sv, mm.name());
EXPECT_EQ(ExpectedHash, mm.infoHash());
}
for (auto const& uri : { "2I2UAEFDZJFN4W3UE65QSOTCUOEZ744B"sv, "d2354010a3ca4ade5b7427bb093a62a3899ff381"sv })
{
auto mm = tr_magnet_metainfo{};
EXPECT_TRUE(mm.parseMagnet(uri));
EXPECT_EQ(0, std::size(mm.announceList()));
EXPECT_EQ(0, mm.webseedCount());
EXPECT_EQ(ExpectedHash, mm.infoHash());
}
}