mirror of
https://github.com/transmission/transmission
synced 2025-03-03 18:25:35 +00:00
feat: warn when creating torrents with nonportable filenames (#2695)
* feat: check new torrents for nonportable filenames * fix: parse torrents even if they have nonportable filenames in the info dict's 'file' string
This commit is contained in:
parent
706735ca88
commit
cfb92c47f0
8 changed files with 139 additions and 71 deletions
|
@ -55,6 +55,7 @@
|
|||
558699602570759F00F77A43 /* libcurl.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 55869925257074EC00F77A43 /* libcurl.tbd */; };
|
||||
5586996C2570759F00F77A43 /* libcurl.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 55869925257074EC00F77A43 /* libcurl.tbd */; };
|
||||
62F644738FE3D8788EBF73A9 /* block-info.cc in Sources */ = {isa = PBXBuildFile; fileRef = A54D44C6A7AAF131D9AE29F5 /* block-info.cc */; };
|
||||
ACBE7A956ED89682EC4460E0 /* file-info.cc in Sources */ = {isa = PBXBuildFile; fileRef = ACBE7A956ED89682EC4460E1 /* file-info.cc */; };
|
||||
66F977825E65AD498C028BB0 /* announce-list.cc in Sources */ = {isa = PBXBuildFile; fileRef = 66F977825E65AD498C028BB1 /* announce-list.cc */; };
|
||||
66F977825E65AD498C028BB2 /* announce-list.h in Headers */ = {isa = PBXBuildFile; fileRef = 66F977825E65AD498C028BB3 /* announce-list.h */; };
|
||||
8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; };
|
||||
|
@ -414,6 +415,7 @@
|
|||
ED8A16422735A8AA000D61F9 /* peer-mgr-wishlist.cc in Sources */ = {isa = PBXBuildFile; fileRef = ED8A163E2735A8AA000D61F9 /* peer-mgr-wishlist.cc */; };
|
||||
EDBDFA9E25AFCCA60093D9C1 /* evutil_time.c in Sources */ = {isa = PBXBuildFile; fileRef = EDBDFA9D25AFCCA60093D9C1 /* evutil_time.c */; };
|
||||
F11545ACA7C4D7A464F703AB /* block-info.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A044CBD8C049AFCBD4DB411 /* block-info.h */; settings = {ATTRIBUTES = (Project, ); }; };
|
||||
ACBE7A956ED89682EC4460E2 /* file-info.h in Headers */ = {isa = PBXBuildFile; fileRef = ACBE7A956ED89682EC4460E3 /* file-info.h */; settings = {ATTRIBUTES = (Project, ); }; };
|
||||
F63480631E1D7274005B9E09 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F63480621E1D7274005B9E09 /* Images.xcassets */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -610,6 +612,7 @@
|
|||
66F977825E65AD498C028BB1 /* announce-list.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "announce-list.cc"; sourceTree = "<group>"; };
|
||||
66F977825E65AD498C028BB3 /* announce-list.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "announce-list.h"; sourceTree = "<group>"; };
|
||||
6A044CBD8C049AFCBD4DB411 /* block-info.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "block-info.h"; sourceTree = SOURCE_ROOT; };
|
||||
ACBE7A956ED89682EC4460E3 /* file-info.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "file-info.h"; sourceTree = SOURCE_ROOT; };
|
||||
8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
8D1107320486CEB800E47090 /* Transmission.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Transmission.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A200B8390A2263BA007BBB1E /* InfoWindowController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InfoWindowController.h; sourceTree = "<group>"; };
|
||||
|
@ -997,6 +1000,7 @@
|
|||
A2FB701A0D95CAEA0001F331 /* GroupsController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GroupsController.h; sourceTree = "<group>"; };
|
||||
A2FB701B0D95CAEA0001F331 /* GroupsController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = GroupsController.mm; sourceTree = "<group>"; };
|
||||
A54D44C6A7AAF131D9AE29F5 /* block-info.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "block-info.cc"; sourceTree = "<group>"; };
|
||||
ACBE7A956ED89682EC4460E1 /* file-info.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "file-info.cc"; sourceTree = "<group>"; };
|
||||
BE1183480CE160960002D0F3 /* libminiupnp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libminiupnp.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BE11834E0CE160C50002D0F3 /* miniupnpc_declspec.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = miniupnpc_declspec.h; sourceTree = "<group>"; };
|
||||
BE11834F0CE160C50002D0F3 /* igd_desc_parse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = igd_desc_parse.h; sourceTree = "<group>"; };
|
||||
|
@ -1504,6 +1508,8 @@
|
|||
children = (
|
||||
A54D44C6A7AAF131D9AE29F5 /* block-info.cc */,
|
||||
6A044CBD8C049AFCBD4DB411 /* block-info.h */,
|
||||
ACBE7A956ED89682EC4460E1 /* file-info.cc */,
|
||||
ACBE7A956ED89682EC4460E3 /* file-info.h */,
|
||||
E23B55A5FC3B557F7746D511 /* interned-string.h */,
|
||||
C17740D3273A002C00E455D2 /* web-utils.cc */,
|
||||
C17740D4273A002C00E455D2 /* web-utils.h */,
|
||||
|
@ -2113,6 +2119,7 @@
|
|||
A2AF23C916B44FA0003BC59E /* log.h in Headers */,
|
||||
A23FAE55178BC2950053DC5B /* platform-quota.h in Headers */,
|
||||
F11545ACA7C4D7A464F703AB /* block-info.h in Headers */,
|
||||
ACBE7A956ED89682EC4460E2 /* file-info.h in Headers */,
|
||||
E23B55A5FC3B557F7746D510 /* interned-string.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -2816,6 +2823,7 @@
|
|||
A2AF23C816B44FA0003BC59E /* log.cc in Sources */,
|
||||
A23FAE54178BC2950053DC5B /* platform-quota.cc in Sources */,
|
||||
62F644738FE3D8788EBF73A9 /* block-info.cc in Sources */,
|
||||
ACBE7A956ED89682EC4460E0 /* file-info.cc in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ set(PROJECT_FILES
|
|||
crypto.cc
|
||||
error.cc
|
||||
fdlimit.cc
|
||||
file-info.cc
|
||||
file-piece-map.cc
|
||||
file-posix.cc
|
||||
file-win32.cc
|
||||
|
@ -165,6 +166,7 @@ set(${PROJECT_NAME}_PRIVATE_HEADERS
|
|||
crypto-utils.h
|
||||
crypto.h
|
||||
fdlimit.h
|
||||
file-info.h
|
||||
file-piece-map.h
|
||||
handshake.h
|
||||
history.h
|
||||
|
|
83
libtransmission/file-info.cc
Normal file
83
libtransmission/file-info.cc
Normal file
|
@ -0,0 +1,83 @@
|
|||
// This file Copyright © 2019-2022 Mnemosyne LLC.
|
||||
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
|
||||
// or any future license endorsed by Mnemosyne LLC.
|
||||
// License text can be found in the licenses/ folder.
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <event2/util.h> // evutil_ascii_strncasecmp
|
||||
|
||||
#include "file-info.h"
|
||||
#include "utils.h"
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
static void appendSanitizedComponent(std::string& out, std::string_view in)
|
||||
{
|
||||
// remove leading spaces
|
||||
auto constexpr leading_test = [](unsigned char 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 = [](unsigned char ch)
|
||||
{
|
||||
return (isspace(ch) != 0) || 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 = Banned.find(ch) != std::string_view::npos || (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;
|
||||
}
|
||||
}
|
||||
|
||||
std::string tr_file_info::sanitizePath(std::string_view in)
|
||||
{
|
||||
auto out = std::string{};
|
||||
|
||||
auto segment = std::string_view{};
|
||||
while (tr_strvSep(&in, &segment, '/'))
|
||||
{
|
||||
appendSanitizedComponent(out, segment);
|
||||
out += '/';
|
||||
}
|
||||
if (!std::empty(out)) // remove trailing slash
|
||||
{
|
||||
out.resize(std::size(out) - 1);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
19
libtransmission/file-info.h
Normal file
19
libtransmission/file-info.h
Normal file
|
@ -0,0 +1,19 @@
|
|||
// This file Copyright © 2021-2022 Mnemosyne LLC.
|
||||
// It may be used under GPLv2 (SPDX: GPL-2.0), GPLv3 (SPDX: GPL-3.0),
|
||||
// or any future license endorsed by Mnemosyne LLC.
|
||||
// License text can be found in the licenses/ folder.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
struct tr_file_info
|
||||
{
|
||||
[[nodiscard]] static std::string sanitizePath(std::string_view path);
|
||||
|
||||
[[nodiscard]] static bool isPortable(std::string_view path)
|
||||
{
|
||||
return sanitizePath(path) == path;
|
||||
}
|
||||
};
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
#include "crypto-utils.h"
|
||||
#include "error.h"
|
||||
#include "file-info.h"
|
||||
#include "file.h"
|
||||
#include "log.h"
|
||||
#include "makemeta.h"
|
||||
|
@ -154,7 +155,7 @@ tr_metainfo_builder* tr_metaInfoBuilderCreate(char const* topFileArg)
|
|||
tr_free(dir);
|
||||
}
|
||||
|
||||
for (struct FileList* walk = files; walk != nullptr; walk = walk->next)
|
||||
for (auto* walk = files; walk != nullptr; walk = walk->next)
|
||||
{
|
||||
++ret->fileCount;
|
||||
}
|
||||
|
@ -162,14 +163,16 @@ tr_metainfo_builder* tr_metaInfoBuilderCreate(char const* topFileArg)
|
|||
ret->files = tr_new0(tr_metainfo_builder_file, ret->fileCount);
|
||||
|
||||
int i = 0;
|
||||
auto const offset = strlen(ret->top);
|
||||
while (files != nullptr)
|
||||
{
|
||||
struct FileList* const tmp = files;
|
||||
files = files->next;
|
||||
|
||||
tr_metainfo_builder_file* const file = &ret->files[i++];
|
||||
auto* const file = &ret->files[i++];
|
||||
file->filename = tmp->filename;
|
||||
file->size = tmp->size;
|
||||
file->is_portable = tr_file_info::isPortable(file->filename + offset);
|
||||
|
||||
ret->totalSize += tmp->size;
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ struct tr_metainfo_builder_file
|
|||
{
|
||||
char* filename;
|
||||
uint64_t size;
|
||||
bool is_portable;
|
||||
};
|
||||
|
||||
enum class TrMakemetaResult
|
||||
|
|
|
@ -12,13 +12,12 @@
|
|||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <event2/util.h> // evutil_ascii_strncasecmp
|
||||
|
||||
#include "transmission.h"
|
||||
|
||||
#include "crypto-utils.h"
|
||||
#include "error-types.h"
|
||||
#include "error.h"
|
||||
#include "file-info.h"
|
||||
#include "file.h"
|
||||
#include "log.h"
|
||||
#include "quark.h"
|
||||
|
@ -179,62 +178,6 @@ void tr_torrent_metainfo::parseWebseeds(tr_torrent_metainfo& setme, tr_variant*
|
|||
}
|
||||
}
|
||||
|
||||
static bool appendSanitizedComponent(std::string& out, std::string_view in, bool* setme_is_adjusted)
|
||||
{
|
||||
auto const original_out_len = std::size(out);
|
||||
auto const original_in = in;
|
||||
*setme_is_adjusted = false;
|
||||
|
||||
// remove leading spaces
|
||||
auto constexpr leading_test = [](unsigned char 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 = [](unsigned char ch)
|
||||
{
|
||||
return (isspace(ch) != 0) || 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 = Banned.find(ch) != std::string_view::npos || (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;
|
||||
}
|
||||
|
||||
*setme_is_adjusted = original_in != std::string_view{ out.c_str() + original_out_len };
|
||||
return std::size(out) > original_out_len;
|
||||
}
|
||||
|
||||
bool tr_torrent_metainfo::parsePath(std::string_view root, tr_variant* path, std::string& setme)
|
||||
{
|
||||
if (!tr_variantIsList(path))
|
||||
|
@ -243,42 +186,43 @@ bool tr_torrent_metainfo::parsePath(std::string_view root, tr_variant* path, std
|
|||
}
|
||||
|
||||
setme = root;
|
||||
|
||||
for (size_t i = 0, n = tr_variantListSize(path); i < n; ++i)
|
||||
{
|
||||
auto raw = std::string_view{};
|
||||
|
||||
if (!tr_variantGetStrView(tr_variantListChild(path, i), &raw))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto is_component_adjusted = bool{};
|
||||
auto const pos = std::size(setme);
|
||||
if (!appendSanitizedComponent(setme, raw, &is_component_adjusted))
|
||||
if (!std::empty(raw))
|
||||
{
|
||||
continue;
|
||||
setme += TR_PATH_DELIMITER;
|
||||
setme += raw;
|
||||
}
|
||||
|
||||
setme.insert(std::begin(setme) + pos, TR_PATH_DELIMITER);
|
||||
}
|
||||
|
||||
if (std::size(setme) <= std::size(root))
|
||||
auto const sanitized = tr_file_info::sanitizePath(setme);
|
||||
|
||||
if (std::size(sanitized) <= std::size(root))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
tr_strvUtf8Clean(setme, setme);
|
||||
tr_strvUtf8Clean(sanitized, setme);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string_view tr_torrent_metainfo::parseFiles(tr_torrent_metainfo& setme, tr_variant* info_dict, uint64_t* setme_total_size)
|
||||
{
|
||||
auto is_root_adjusted = bool{ false };
|
||||
auto root_name = std::string{};
|
||||
auto total_size = uint64_t{ 0 };
|
||||
|
||||
setme.files_.clear();
|
||||
|
||||
if (!appendSanitizedComponent(root_name, setme.name_, &is_root_adjusted))
|
||||
auto const root_name = tr_file_info::sanitizePath(setme.name_);
|
||||
|
||||
if (std::empty(root_name))
|
||||
{
|
||||
return "invalid name"sv;
|
||||
}
|
||||
|
|
|
@ -208,6 +208,14 @@ int tr_main(int argc, char* argv[])
|
|||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < b->fileCount; ++i)
|
||||
{
|
||||
if (auto const& file = b->files[i]; !file.is_portable)
|
||||
{
|
||||
fprintf(stderr, "WARNING: consider renaming nonportable filename \"%s\".\n", file.filename);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.piecesize_kib != 0)
|
||||
{
|
||||
tr_metaInfoBuilderSetPieceSize(b, options.piecesize_kib * KiB);
|
||||
|
|
Loading…
Reference in a new issue