commits
tags
#include "torlinkc/sources/magnet.hpp"
#include <algorithm>
#include <array>
#include <cctype>
#include <cstring>
#include <regex>
#include <unordered_set>
#include "torlinkc/util/url_encode.hpp"
namespace torlinkc {
namespace {
// CTAD deduces both element type and size from the initializer list, so a
// miscounted array-size template argument (previously 12 for an 11-entry
// list, leaving a null-pointer 12th element that crashed on first use) can't
// happen again.
constexpr std::array kTrackers = {
"udp://tracker.opentrackr.org:1337/announce",
"udp://open.demonii.com:1337/announce",
"udp://tracker.openbittorrent.com:6969/announce",
"udp://tracker.torrent.eu.org:451/announce",
"udp://exodus.desync.com:6969/announce",
"udp://open.stealth.si:80/announce",
"udp://tracker.dler.org:6969/announce",
// HTTP(S) endpoints so peer discovery still works where UDP is blocked or
// mangled (VPN exit nodes, strict NATs): DHT and udp:// are both UDP.
"http://tracker.opentrackr.org:1337/announce",
"http://tracker.openbittorrent.com:80/announce",
"http://tracker.dler.org:6969/announce",
"https://tracker.tamersunion.org:443/announce",
};
std::string trim(const std::string& s) {
auto first = s.find_first_not_of(" \t\r\n");
if (first == std::string::npos) return "";
auto last = s.find_last_not_of(" \t\r\n");
return s.substr(first, last - first + 1);
}
std::string toLower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); });
return s;
}
const char kBase32Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
std::optional<std::string> base32ToHex(const std::string& b32) {
int bits = 0;
unsigned long value = 0;
std::string out;
for (unsigned char raw : b32) {
char c = static_cast<char>(std::toupper(raw));
const char* pos = std::strchr(kBase32Alphabet, c);
if (!pos || c == '\0') return std::nullopt;
int idx = static_cast<int>(pos - kBase32Alphabet);
value = (value << 5) | static_cast<unsigned long>(idx);
bits += 5;
if (bits >= 8) {
bits -= 8;
unsigned int byte = (value >> bits) & 0xff;
static const char* hex = "0123456789abcdef";
out += hex[byte >> 4];
out += hex[byte & 0xF];
value &= (1u << bits) - 1;
}
}
return out.size() == 40 ? std::optional<std::string>(out) : std::nullopt;
}
// Extracts the `dn` query parameter the way `new URL(s).searchParams.get("dn")`
// would, without pulling in a full URL parser: split the query string on '&',
// find the first `dn=` pair, decode it.
std::optional<std::string> extractDn(const std::string& magnet) {
auto q = magnet.find('?');
if (q == std::string::npos) return std::nullopt;
std::string query = magnet.substr(q + 1);
std::size_t pos = 0;
while (pos <= query.size()) {
auto amp = query.find('&', pos);
std::string pair = query.substr(pos, amp == std::string::npos ? std::string::npos : amp - pos);
auto eq = pair.find('=');
if (eq != std::string::npos && pair.substr(0, eq) == "dn") {
return decodeURIComponent(pair.substr(eq + 1));
}
if (amp == std::string::npos) break;
pos = amp + 1;
}
return std::nullopt;
}
} // namespace
std::string buildMagnet(const std::string& infoHash, const std::string& name,
const std::vector<std::string>& extraTrackers) {
const std::string dn = encodeURIComponent(name);
std::unordered_set<std::string> seen;
std::vector<std::string> trackers;
auto addTracker = [&](const std::string& raw) {
const std::string url = trim(raw);
if (url.empty() || seen.count(url)) return;
seen.insert(url);
trackers.push_back(url);
};
for (const auto& t : extraTrackers) addTracker(t);
for (const char* t : kTrackers) addTracker(t);
std::string tr;
for (const auto& t : trackers) tr += "&tr=" + encodeURIComponent(t);
return "magnet:?xt=urn:btih:" + infoHash + "&dn=" + dn + tr;
}
std::string normalizeInfoHash(const std::string& raw) {
if (raw.size() == 32) {
if (auto hex = base32ToHex(raw)) return *hex;
return toLower(raw);
}
return toLower(raw);
}
std::optional<ParsedMagnet> parseMagnet(const std::string& input) {
const std::string s = trim(input);
static const std::regex kMagnetPrefix(R"(^magnet:\?)", std::regex::icase);
if (!std::regex_search(s, kMagnetPrefix)) return std::nullopt;
static const std::regex kMagnetRe(R"(xt=urn:btih:([a-f0-9]{40}|[a-z2-7]{32}))", std::regex::icase);
std::smatch m;
if (!std::regex_search(s, m, kMagnetRe)) return std::nullopt;
const std::string infoHash = normalizeInfoHash(m[1].str());
std::string name = infoHash;
if (auto dn = extractDn(s); dn && !dn->empty()) name = *dn;
return ParsedMagnet{infoHash, name, s};
}
bool isInfoHash(const std::string& input) {
const std::string s = trim(input);
static const std::regex kInfoHashRe(R"(^([a-f0-9]{40}|[a-z2-7]{32})$)", std::regex::icase);
return std::regex_match(s, kInfoHashRe);
}
std::optional<ParsedMagnet> parseInput(const std::string& input) {
const std::string s = trim(input);
if (auto magnet = parseMagnet(s)) return magnet;
if (!isInfoHash(s)) return std::nullopt;
const std::string infoHash = normalizeInfoHash(s);
return ParsedMagnet{infoHash, infoHash, buildMagnet(infoHash, infoHash)};
}
} // namespace torlinkc