transmission/libtransmission/metainfo.cc

757 lines
20 KiB
C++
Raw Normal View History

/*
* This file Copyright (C) 2007-2014 Mnemosyne LLC
2006-07-16 19:39:23 +00:00
*
* It may be used under the GNU GPL versions 2 or 3
* or any future license endorsed by Mnemosyne LLC.
2006-07-16 19:39:23 +00:00
*
*/
2006-07-16 19:39:23 +00:00
#include <algorithm>
#include <array>
#include <cstring> /* strlen() */
#include <iterator>
#include <string_view>
2006-07-16 19:39:23 +00:00
#include "transmission.h"
#include "crypto-utils.h" /* tr_sha1 */
#include "file.h"
#include "log.h"
2007-07-09 20:10:42 +00:00
#include "metainfo.h"
#include "platform.h" /* tr_getTorrentDir() */
#include "session.h"
#include "tr-assert.h"
#include "utils.h"
#include "variant.h"
#include "web-utils.h"
2006-07-16 19:39:23 +00:00
using namespace std::literals;
2007-10-20 15:17:36 +00:00
/***
****
***/
#ifdef _WIN32
auto constexpr PATH_DELIMITER_CHARS = std::array<char, 2>{ '/', '\\' };
#else
auto constexpr PATH_DELIMITER_CHARS = std::array<char, 1>{ '/' };
#endif
static constexpr bool char_is_path_separator(char c)
{
for (auto ch : PATH_DELIMITER_CHARS)
{
if (c == ch)
{
return true;
}
}
return false;
}
static char* metainfoGetBasenameNameAndPartialHash(tr_info const* inf)
{
char const* name = inf->originalName;
size_t const name_len = strlen(name);
char* ret = tr_strdup_printf("%s.%16.16s", name, inf->hashString);
2017-05-13 22:38:31 +00:00
for (size_t i = 0; i < name_len; ++i)
{
if (char_is_path_separator(ret[i]))
{
ret[i] = '_';
}
}
return ret;
}
static char* metainfoGetBasenameHashOnly(tr_info const* inf)
2008-04-14 11:52:50 +00:00
{
return tr_strdup(inf->hashString);
}
char* tr_metainfoGetBasename(tr_info const* inf, enum tr_metainfo_basename_format format)
{
switch (format)
{
case TR_METAINFO_BASENAME_NAME_AND_PARTIAL_HASH:
return metainfoGetBasenameNameAndPartialHash(inf);
case TR_METAINFO_BASENAME_HASH:
return metainfoGetBasenameHashOnly(inf);
default:
TR_ASSERT_MSG(false, "unknown metainfo basename format %d", (int)format);
return nullptr;
}
}
static char* getTorrentFilename(tr_session const* session, tr_info const* inf, enum tr_metainfo_basename_format format)
{
char* base = tr_metainfoGetBasename(inf, format);
char* filename = tr_strdup_printf("%s" TR_PATH_DELIMITER_STR "%s.torrent", tr_getTorrentDir(session), base);
tr_free(base);
return filename;
2008-04-14 11:52:50 +00:00
}
2008-08-20 21:01:17 +00:00
/***
****
***/
bool tr_metainfoAppendSanitizedPathComponent(std::string& out, std::string_view in, bool* is_adjusted)
{
auto const original_out_len = std::size(out);
auto const original_in = in;
*is_adjusted = false;
// remove leading spaces
auto constexpr leading_test = [](auto ch)
{
return isspace(ch);
};
auto const it = std::find_if_not(std::begin(in), std::end(in), leading_test);
in.remove_prefix(std::distance(std::begin(in), it));
// remove trailing spaces and '.'
auto constexpr trailing_test = [](auto ch)
{
return isspace(ch) || ch == '.';
};
auto const rit = std::find_if_not(std::rbegin(in), std::rend(in), trailing_test);
in.remove_suffix(std::distance(std::rbegin(in), rit));
// munge banned characters
// https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
auto constexpr ensure_legal_char = [](auto ch)
{
auto constexpr Banned = std::string_view{ "<>:\"/\\|?*" };
auto const banned = tr_strvContains(Banned, ch) || (unsigned char)ch < 0x20;
return banned ? '_' : ch;
};
auto const old_out_len = std::size(out);
std::transform(std::begin(in), std::end(in), std::back_inserter(out), ensure_legal_char);
// munge banned filenames
// https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
auto constexpr ReservedNames = std::array<std::string_view, 22>{
"CON"sv, "PRN"sv, "AUX"sv, "NUL"sv, "COM1"sv, "COM2"sv, "COM3"sv, "COM4"sv, "COM5"sv, "COM6"sv, "COM7"sv,
"COM8"sv, "COM9"sv, "LPT1"sv, "LPT2"sv, "LPT3"sv, "LPT4"sv, "LPT5"sv, "LPT6"sv, "LPT7"sv, "LPT8"sv, "LPT9"sv,
};
for (auto const& name : ReservedNames)
{
size_t const name_len = std::size(name);
if (evutil_ascii_strncasecmp(out.c_str() + old_out_len, std::data(name), name_len) != 0 ||
(out[old_out_len + name_len] != '\0' && out[old_out_len + name_len] != '.'))
{
continue;
}
out.insert(std::begin(out) + old_out_len + name_len, '_');
break;
}
*is_adjusted = original_in != std::string_view{ out.c_str() + original_out_len };
return std::size(out) > original_out_len;
}
static bool getfile(char** setme, bool* is_adjusted, std::string_view root, tr_variant* path, std::string& buf)
2008-08-20 21:01:17 +00:00
{
bool success = false;
2008-08-20 21:01:17 +00:00
*setme = nullptr;
*is_adjusted = false;
if (tr_variantIsList(path))
2008-08-20 21:01:17 +00:00
{
success = true;
buf = root;
2017-05-13 22:38:31 +00:00
for (int i = 0, n = tr_variantListSize(path); i < n; i++)
{
auto raw = std::string_view{};
if (!tr_variantGetStrView(tr_variantListChild(path, i), &raw))
{
success = false;
break;
2008-08-20 21:01:17 +00:00
}
auto is_component_adjusted = bool{};
auto const pos = std::size(buf);
if (!tr_metainfoAppendSanitizedPathComponent(buf, raw, &is_component_adjusted))
{
continue;
}
buf.insert(std::begin(buf) + pos, TR_PATH_DELIMITER);
*is_adjusted |= is_component_adjusted;
}
2008-08-20 21:01:17 +00:00
}
if (success && std::size(buf) <= std::size(root))
{
success = false;
}
if (success)
{
*setme = tr_utf8clean(buf);
*is_adjusted |= buf != *setme;
}
return success;
2008-08-20 21:01:17 +00:00
}
static char const* parseFiles(tr_info* inf, tr_variant* files, tr_variant const* length)
2008-08-20 21:01:17 +00:00
{
int64_t len = 0;
inf->totalSize = 0;
2008-08-20 21:01:17 +00:00
bool is_root_adjusted = false;
auto root_name = std::string{};
if (!tr_metainfoAppendSanitizedPathComponent(root_name, inf->name, &is_root_adjusted))
{
return "path";
}
char const* errstr = nullptr;
if (tr_variantIsList(files)) /* multi-file mode */
2008-08-20 21:01:17 +00:00
{
auto buf = std::string{};
errstr = nullptr;
2008-08-21 16:12:17 +00:00
inf->isFolder = true;
inf->fileCount = tr_variantListSize(files);
inf->files = tr_new0(tr_file, inf->fileCount);
2008-08-20 21:01:17 +00:00
2017-05-13 22:38:31 +00:00
for (tr_file_index_t i = 0; i < inf->fileCount; i++)
2008-08-20 21:01:17 +00:00
{
auto* const file = tr_variantListChild(files, i);
if (!tr_variantIsDict(file))
{
errstr = "files";
break;
}
2008-08-20 21:01:17 +00:00
tr_variant* path = nullptr;
if (!tr_variantDictFindList(file, TR_KEY_path_utf_8, &path) && !tr_variantDictFindList(file, TR_KEY_path, &path))
{
errstr = "path";
break;
}
2008-08-20 21:01:17 +00:00
bool is_file_adjusted = false;
if (!getfile(&inf->files[i].name, &is_file_adjusted, root_name, path, buf))
{
errstr = "path";
break;
}
2008-08-20 21:01:17 +00:00
if (!tr_variantDictFindInt(file, TR_KEY_length, &len))
{
errstr = "length";
break;
}
2008-08-20 21:01:17 +00:00
inf->files[i].length = len;
inf->files[i].is_renamed = is_root_adjusted || is_file_adjusted;
inf->totalSize += len;
2008-08-20 21:01:17 +00:00
}
}
else if (tr_variantGetInt(length, &len)) /* single-file mode */
2008-08-20 21:01:17 +00:00
{
inf->isFolder = false;
inf->fileCount = 1;
inf->files = tr_new0(tr_file, 1);
inf->files[0].name = tr_strndup(root_name.c_str(), std::size(root_name));
inf->files[0].length = len;
inf->files[0].is_renamed = is_root_adjusted;
inf->totalSize += len;
2008-08-20 21:01:17 +00:00
}
else
2008-08-20 21:01:17 +00:00
{
errstr = "length";
2008-08-20 21:01:17 +00:00
}
return errstr;
2008-08-20 21:01:17 +00:00
}
static char* tr_convertAnnounceToScrape(std::string_view url)
2008-04-24 03:26:36 +00:00
{
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);
2008-04-24 03:26:36 +00:00
}
// some torrents with UDP announce URLs don't have /announce
else if (url.find("udp:"sv) == 0)
{
scrape = tr_strvDup(url);
}
2008-04-24 03:26:36 +00:00
return scrape;
2008-04-24 03:26:36 +00:00
}
static char const* getannounce(tr_info* inf, tr_variant* meta)
2008-04-24 03:26:36 +00:00
{
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))
2008-04-24 03:26:36 +00:00
{
int const numTiers = tr_variantListSize(tiers);
int n = 0;
2008-04-24 03:26:36 +00:00
2017-05-13 22:38:31 +00:00
for (int i = 0; i < numTiers; i++)
{
n += tr_variantListSize(tr_variantListChild(tiers, i));
}
2008-04-24 03:26:36 +00:00
trackers = tr_new0(tr_tracker_info, n);
2008-04-24 03:26:36 +00:00
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;
2017-05-13 22:38:31 +00:00
for (int j = 0; j < tierSize; j++)
{
if (tr_variantGetStrView(tr_variantListChild(tier, 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;
}
2008-04-24 03:26:36 +00:00
}
}
2009-08-10 20:04:08 +00:00
if (anyAdded)
{
++validTiers;
}
2008-04-24 03:26:36 +00:00
}
/* did we use any of the tiers? */
if (trackerCount == 0)
{
tr_free(trackers);
trackers = nullptr;
2008-04-24 03:26:36 +00:00
}
}
/* Regular announce value */
if (trackerCount == 0 && tr_variantDictFindStrView(meta, TR_KEY_announce, &url))
2008-04-24 03:26:36 +00:00
{
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++;
}
2008-04-24 03:26:36 +00:00
}
inf->trackers = trackers;
inf->trackerCount = trackerCount;
2008-04-24 03:26:36 +00:00
return nullptr;
2008-08-20 21:01:17 +00:00
}
/**
* @brief Ensure that the URLs for multfile torrents end in a slash.
*
* See http://bittorrent.org/beps/bep_0019.html#metadata-extension
* for background on how the trailing slash is used for "url-list"
* fields.
*
* This function is to workaround some .torrent generators, such as
* mktorrent and very old versions of utorrent, that don't add the
* trailing slash for multifile torrents if omitted by the end user.
*/
static char* fix_webseed_url(tr_info const* inf, std::string_view url)
{
url = tr_strvStrip(url);
if (!tr_urlIsValid(url))
{
return nullptr;
}
if (inf->fileCount > 1 && !std::empty(url) && url.back() != '/')
{
return tr_strdup_printf("%" TR_PRIsv "/", TR_PRIsv_ARG(url));
}
return tr_strvDup(url);
}
static void geturllist(tr_info* inf, tr_variant* meta)
2008-08-20 21:01:17 +00:00
{
tr_variant* urls = nullptr;
auto url = std::string_view{};
2008-08-20 21:01:17 +00:00
if (tr_variantDictFindList(meta, TR_KEY_url_list, &urls))
2008-08-20 21:01:17 +00:00
{
int const n = tr_variantListSize(urls);
2008-08-20 21:01:17 +00:00
inf->webseedCount = 0;
inf->webseeds = tr_new0(char*, n);
2017-05-13 22:38:31 +00:00
for (int i = 0; i < n; i++)
{
if (tr_variantGetStrView(tr_variantListChild(urls, i), &url))
{
char* const fixed_url = fix_webseed_url(inf, url);
if (fixed_url != nullptr)
{
inf->webseeds[inf->webseedCount++] = fixed_url;
}
}
}
2008-08-20 21:01:17 +00:00
}
else if (tr_variantDictFindStrView(meta, TR_KEY_url_list, &url)) /* handle single items in webseeds */
{
char* const fixed_url = fix_webseed_url(inf, url);
if (fixed_url != nullptr)
{
inf->webseedCount = 1;
inf->webseeds = tr_new0(char*, 1);
inf->webseeds[0] = fixed_url;
}
}
2008-04-24 03:26:36 +00:00
}
static char const* tr_metainfoParseImpl(
tr_session const* session,
tr_info* inf,
bool* hasInfoDict,
size_t* infoDictLength,
tr_variant const* meta_in)
2007-12-21 22:18:40 +00:00
{
int64_t i = 0;
auto sv = std::string_view{};
tr_variant* const meta = const_cast<tr_variant*>(meta_in);
bool isMagnet = false;
/* info_hash: urlencoded 20-byte SHA1 hash of the value of the info key
* from the Metainfo file. Note that the value will be a bencoded
* dictionary, given the definition of the info key above. */
tr_variant* infoDict = nullptr;
bool b = tr_variantDictFindDict(meta, TR_KEY_info, &infoDict);
if (hasInfoDict != nullptr)
{
*hasInfoDict = b;
}
if (!b)
{
/* no info dictionary... is this a magnet link? */
tr_variant* d = nullptr;
if (tr_variantDictFindDict(meta, TR_KEY_magnet_info, &d))
{
isMagnet = true;
// get the info-hash
if (!tr_variantDictFindStrView(d, TR_KEY_info_hash, &sv))
{
return "info_hash";
}
if (std::size(sv) != SHA_DIGEST_LENGTH)
{
return "info_hash";
}
std::copy(std::begin(sv), std::end(sv), inf->hash);
tr_sha1_to_hex(inf->hashString, inf->hash);
// maybe get the display name
if (tr_variantDictFindStrView(d, TR_KEY_display_name, &sv))
{
tr_free(inf->name);
tr_free(inf->originalName);
inf->name = tr_strvDup(sv);
inf->originalName = tr_strvDup(sv);
}
if (inf->name == nullptr)
{
inf->name = tr_strdup(inf->hashString);
}
if (inf->originalName == nullptr)
{
inf->originalName = tr_strdup(inf->hashString);
}
}
else // not a magnet link and has no info dict...
{
return "info";
}
}
else
{
size_t blen = 0;
char* bstr = tr_variantToStr(infoDict, TR_VARIANT_FMT_BENC, &blen);
tr_sha1(inf->hash, bstr, (int)blen, nullptr);
tr_sha1_to_hex(inf->hashString, inf->hash);
if (infoDictLength != nullptr)
{
*infoDictLength = blen;
}
tr_free(bstr);
}
/* name */
if (!isMagnet)
{
if (!tr_variantDictFindStrView(infoDict, TR_KEY_name_utf_8, &sv) &&
!tr_variantDictFindStrView(infoDict, TR_KEY_name, &sv))
{
sv = ""sv;
}
if (std::empty(sv))
{
return "name";
}
tr_free(inf->name);
tr_free(inf->originalName);
inf->name = tr_utf8clean(sv);
inf->originalName = tr_strdup(inf->name);
}
/* comment */
if (!tr_variantDictFindStrView(meta, TR_KEY_comment_utf_8, &sv) && !tr_variantDictFindStrView(meta, TR_KEY_comment, &sv))
{
sv = ""sv;
}
tr_free(inf->comment);
inf->comment = tr_utf8clean(sv);
/* created by */
if (!tr_variantDictFindStrView(meta, TR_KEY_created_by_utf_8, &sv) &&
!tr_variantDictFindStrView(meta, TR_KEY_created_by, &sv))
{
sv = ""sv;
}
2009-11-24 02:16:31 +00:00
tr_free(inf->creator);
inf->creator = tr_utf8clean(sv);
2009-11-24 02:16:31 +00:00
/* creation date */
i = 0;
(void)!tr_variantDictFindInt(meta, TR_KEY_creation_date, &i);
inf->dateCreated = i;
/* private */
if (!tr_variantDictFindInt(infoDict, TR_KEY_private, &i) && !tr_variantDictFindInt(meta, TR_KEY_private, &i))
{
i = 0;
}
2006-07-16 19:39:23 +00:00
inf->isPrivate = i != 0;
/* source */
if (!tr_variantDictFindStrView(infoDict, TR_KEY_source, &sv) && !tr_variantDictFindStrView(meta, TR_KEY_source, &sv))
{
sv = ""sv;
}
tr_free(inf->source);
inf->source = tr_utf8clean(sv);
/* piece length */
if (!isMagnet)
{
if (!tr_variantDictFindInt(infoDict, TR_KEY_piece_length, &i) || (i < 1))
{
return "piece length";
}
inf->pieceSize = i;
}
2008-08-20 21:01:17 +00:00
/* pieces and files */
if (!isMagnet)
{
if (!tr_variantDictFindStrView(infoDict, TR_KEY_pieces, &sv))
{
return "pieces";
}
if (std::size(sv) % SHA_DIGEST_LENGTH != 0)
{
return "pieces";
}
inf->pieceCount = std::size(sv) / SHA_DIGEST_LENGTH;
inf->pieces = tr_new0(tr_sha1_digest_t, inf->pieceCount);
std::copy_n(std::data(sv), std::size(sv), (uint8_t*)(inf->pieces));
2008-08-20 19:21:57 +00:00
auto const* const errstr = parseFiles(
inf,
tr_variantDictFind(infoDict, TR_KEY_files),
tr_variantDictFind(infoDict, TR_KEY_length));
if (errstr != nullptr)
{
return errstr;
}
if (inf->fileCount == 0 || inf->totalSize == 0)
{
return "files";
}
if ((uint64_t)inf->pieceCount != (inf->totalSize + inf->pieceSize - 1) / inf->pieceSize)
{
return "files";
}
}
/* get announce or announce-list */
auto const* const errstr = getannounce(inf, meta);
if (errstr != nullptr)
{
return errstr;
}
2006-07-16 19:39:23 +00:00
/* get the url-list */
geturllist(inf, meta);
/* filename of Transmission's copy */
tr_free(inf->torrent);
inf->torrent = session != nullptr ? getTorrentFilename(session, inf, TR_METAINFO_BASENAME_HASH) : nullptr;
2008-04-14 11:52:50 +00:00
return nullptr;
2008-08-20 21:01:17 +00:00
}
bool tr_metainfoParse(
tr_session const* session,
tr_variant const* meta_in,
tr_info* inf,
bool* hasInfoDict,
size_t* infoDictLength)
2008-08-20 21:01:17 +00:00
{
char const* badTag = tr_metainfoParseImpl(session, inf, hasInfoDict, infoDictLength, meta_in);
bool const success = badTag == nullptr;
if (badTag != nullptr)
2008-08-20 21:01:17 +00:00
{
tr_logAddNamedError(inf->name, _("Invalid metadata entry \"%s\""), badTag);
tr_metainfoFree(inf);
2008-08-20 21:01:17 +00:00
}
return success;
2006-07-16 19:39:23 +00:00
}
void tr_metainfoFree(tr_info* inf)
{
2017-05-13 22:38:31 +00:00
for (unsigned int i = 0; i < inf->webseedCount; i++)
{
tr_free(inf->webseeds[i]);
}
2017-05-13 22:38:31 +00:00
for (tr_file_index_t ff = 0; ff < inf->fileCount; ff++)
{
tr_free(inf->files[ff].name);
}
tr_free(inf->webseeds);
tr_free(inf->pieces);
tr_free(inf->files);
tr_free(inf->comment);
tr_free(inf->creator);
tr_free(inf->source);
tr_free(inf->torrent);
tr_free(inf->originalName);
tr_free(inf->name);
2017-05-13 22:38:31 +00:00
for (unsigned int i = 0; i < inf->trackerCount; i++)
{
tr_free(inf->trackers[i].announce);
tr_free(inf->trackers[i].scrape);
}
tr_free(inf->trackers);
memset(inf, '\0', sizeof(tr_info));
}
void tr_metainfoRemoveSaved(tr_session const* session, tr_info const* inf)
2006-07-16 19:39:23 +00:00
{
char* filename = getTorrentFilename(session, inf, TR_METAINFO_BASENAME_HASH);
tr_sys_path_remove(filename, nullptr);
tr_free(filename);
filename = getTorrentFilename(session, inf, TR_METAINFO_BASENAME_NAME_AND_PARTIAL_HASH);
tr_sys_path_remove(filename, nullptr);
tr_free(filename);
}
void tr_metainfoMigrateFile(
tr_session const* session,
tr_info const* info,
enum tr_metainfo_basename_format old_format,
enum tr_metainfo_basename_format new_format)
{
char* old_filename = getTorrentFilename(session, info, old_format);
char* new_filename = getTorrentFilename(session, info, new_format);
if (tr_sys_path_rename(old_filename, new_filename, nullptr))
{
tr_logAddNamedError(info->name, "Migrated torrent file from \"%s\" to \"%s\"", old_filename, new_filename);
}
tr_free(new_filename);
tr_free(old_filename);
}