diff --git a/Transmission.xcodeproj/project.pbxproj b/Transmission.xcodeproj/project.pbxproj index e0e77440d..8129648e4 100644 --- a/Transmission.xcodeproj/project.pbxproj +++ b/Transmission.xcodeproj/project.pbxproj @@ -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 = ""; }; 66F977825E65AD498C028BB3 /* announce-list.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "announce-list.h"; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -997,6 +1000,7 @@ A2FB701A0D95CAEA0001F331 /* GroupsController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GroupsController.h; sourceTree = ""; }; A2FB701B0D95CAEA0001F331 /* GroupsController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = GroupsController.mm; sourceTree = ""; }; A54D44C6A7AAF131D9AE29F5 /* block-info.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "block-info.cc"; sourceTree = ""; }; + ACBE7A956ED89682EC4460E1 /* file-info.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "file-info.cc"; sourceTree = ""; }; 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 = ""; }; BE11834F0CE160C50002D0F3 /* igd_desc_parse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = igd_desc_parse.h; sourceTree = ""; }; @@ -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; }; diff --git a/libtransmission/CMakeLists.txt b/libtransmission/CMakeLists.txt index f02811106..e6d26f5fd 100644 --- a/libtransmission/CMakeLists.txt +++ b/libtransmission/CMakeLists.txt @@ -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 diff --git a/libtransmission/file-info.cc b/libtransmission/file-info.cc new file mode 100644 index 000000000..7750bb1ac --- /dev/null +++ b/libtransmission/file-info.cc @@ -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 +#include +#include +#include + +#include // 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{ + "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; +} diff --git a/libtransmission/file-info.h b/libtransmission/file-info.h new file mode 100644 index 000000000..3c1f804db --- /dev/null +++ b/libtransmission/file-info.h @@ -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 +#include + +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; + } +}; diff --git a/libtransmission/makemeta.cc b/libtransmission/makemeta.cc index 890728b79..8dbe8bfdb 100644 --- a/libtransmission/makemeta.cc +++ b/libtransmission/makemeta.cc @@ -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; diff --git a/libtransmission/makemeta.h b/libtransmission/makemeta.h index 68858d828..a39b1394f 100644 --- a/libtransmission/makemeta.h +++ b/libtransmission/makemeta.h @@ -11,6 +11,7 @@ struct tr_metainfo_builder_file { char* filename; uint64_t size; + bool is_portable; }; enum class TrMakemetaResult diff --git a/libtransmission/torrent-metainfo.cc b/libtransmission/torrent-metainfo.cc index 0aeb5fb64..440ebd225 100644 --- a/libtransmission/torrent-metainfo.cc +++ b/libtransmission/torrent-metainfo.cc @@ -12,13 +12,12 @@ #include #include -#include // 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{ - "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; } diff --git a/utils/create.cc b/utils/create.cc index c5eca082d..c156b8ab9 100644 --- a/utils/create.cc +++ b/utils/create.cc @@ -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);