diff --git a/libtransmission/ip-cache.cc b/libtransmission/ip-cache.cc index 3fc649c21..783e6c6b8 100644 --- a/libtransmission/ip-cache.cc +++ b/libtransmission/ip-cache.cc @@ -170,7 +170,7 @@ tr_ip_cache::~tr_ip_cache() if (!std::all_of( std::begin(is_updating_), std::end(is_updating_), - [](is_updating_t const& v) { return v == is_updating_t::ABORT; })) + [](is_updating_t const& v) { return v == is_updating_t::Abort; })) { tr_logAddDebug("Destructed while some global IP queries were pending."); } @@ -185,11 +185,11 @@ bool tr_ip_cache::try_shutdown() noexcept for (std::size_t i = 0; i < NUM_TR_AF_INET_TYPES; ++i) { - if (is_updating_[i] == is_updating_t::YES) + if (is_updating_[i] == is_updating_t::Yes) { return false; } - is_updating_[i] = is_updating_t::ABORT; // Abort any future updates + is_updating_[i] = is_updating_t::Abort; // Abort any future updates } return true; } @@ -244,7 +244,7 @@ void tr_ip_cache::update_global_addr(tr_address_type type) noexcept { return; } - TR_ASSERT(is_updating_[type] == is_updating_t::YES); + TR_ASSERT(is_updating_[type] == is_updating_t::Yes); // Update global address static auto constexpr IPProtocolMap = std::array{ @@ -279,7 +279,7 @@ void tr_ip_cache::update_source_addr(tr_address_type type) noexcept { return; } - TR_ASSERT(is_updating_[type] == is_updating_t::YES); + TR_ASSERT(is_updating_[type] == is_updating_t::Yes); auto const protocol = tr_ip_protocol_to_sv(type); auto err = 0; @@ -321,7 +321,7 @@ void tr_ip_cache::update_source_addr(tr_address_type type) noexcept void tr_ip_cache::on_response_ip_query(tr_address_type type, tr_web::FetchResponse const& response) noexcept { auto& ix_service = ix_service_[type]; - TR_ASSERT(is_updating_[type] == is_updating_t::YES); + TR_ASSERT(is_updating_[type] == is_updating_t::Yes); TR_ASSERT(ix_service < std::size(IPQueryServices[type])); auto const protocol = tr_ip_protocol_to_sv(type); @@ -398,16 +398,16 @@ void tr_ip_cache::unset_addr(tr_address_type type) noexcept bool tr_ip_cache::set_is_updating(tr_address_type type) noexcept { - if (is_updating_[type] != is_updating_t::NO) + if (is_updating_[type] != is_updating_t::No) { return false; } - is_updating_[type] = is_updating_t::YES; + is_updating_[type] = is_updating_t::Yes; return true; } void tr_ip_cache::unset_is_updating(tr_address_type type) noexcept { - TR_ASSERT(is_updating_[type] == is_updating_t::YES); - is_updating_[type] = is_updating_t::NO; + TR_ASSERT(is_updating_[type] == is_updating_t::Yes); + is_updating_[type] = is_updating_t::No; } diff --git a/libtransmission/ip-cache.h b/libtransmission/ip-cache.h index bd71d81a2..02351a745 100644 --- a/libtransmission/ip-cache.h +++ b/libtransmission/ip-cache.h @@ -119,9 +119,9 @@ private: enum class is_updating_t : uint8_t { - NO = 0, - YES, - ABORT + No = 0, + Yes, + Abort }; array_ip_t is_updating_ = {}; diff --git a/libtransmission/magnet-metainfo.cc b/libtransmission/magnet-metainfo.cc index bcde0327d..6b4e06175 100644 --- a/libtransmission/magnet-metainfo.cc +++ b/libtransmission/magnet-metainfo.cc @@ -195,7 +195,7 @@ std::string tr_magnet_metainfo::magnet() const void tr_magnet_metainfo::set_name(std::string_view name) { - name_ = tr_strv_convert_utf8(name); + name_ = tr_strv_to_utf8_string(name); } void tr_magnet_metainfo::add_webseed(std::string_view webseed) diff --git a/libtransmission/peer-msgs.cc b/libtransmission/peer-msgs.cc index 6cf19e31f..d6e674ef5 100644 --- a/libtransmission/peer-msgs.cc +++ b/libtransmission/peer-msgs.cc @@ -1320,7 +1320,7 @@ void tr_peerMsgsImpl::parse_ltep_handshake(MessageReader& payload) // peer id encoding. if (auto const sv = map->value_if(TR_KEY_v)) { - set_user_agent(tr_interned_string{ tr_strv_convert_utf8(*sv) }); + set_user_agent(tr_interned_string{ tr_strv_to_utf8_string(*sv) }); } // https://www.bittorrent.org/beps/bep_0010.html diff --git a/libtransmission/torrent-metainfo.cc b/libtransmission/torrent-metainfo.cc index 46e397fa1..bf6c51769 100644 --- a/libtransmission/torrent-metainfo.cc +++ b/libtransmission/torrent-metainfo.cc @@ -329,11 +329,11 @@ struct MetainfoHandler final : public transmission::benc::BasicHandler + +#include +#include + +#include + +#include "gtest/gtest.h" +#include "test-fixtures.h" + +using UtilsTest = ::libtransmission::test::TransmissionTest; +using namespace std::literals; + +namespace +{ +[[nodiscard]] constexpr size_t count_replacement_char(std::string_view const sv) +{ + size_t count = 0; + constexpr auto needle = "\xEF\xBF\xBD"sv; // U+FFFD replacement + auto pos = std::string_view::size_type{}; + + while ((pos = sv.find(needle, pos)) != std::string_view::npos) + { + ++count; + pos += std::size(needle); + } + + return count; +} + +[[nodiscard]] constexpr bool has_non_ascii(std::string_view const sv) +{ + return std::any_of(std::begin(sv), std::end(sv), [](unsigned char ch) { return ch >= 0x80; }); +} +} // namespace + +TEST_F(UtilsTest, trStrvToUtf8NsstringValid) +{ + @autoreleasepool + { + NSString* str = tr_strv_to_utf8_nsstring("hello"sv); + EXPECT_TRUE([str isEqualToString:@"hello"]); + } +} + +TEST_F(UtilsTest, trStrvToUtf8NsstringInvalid) +{ + @autoreleasepool + { + constexpr auto bad = "\xF4\x33\x81\x82"sv; + NSString* str = tr_strv_to_utf8_nsstring(bad); + EXPECT_TRUE([str isEqualToString:@""]); + } +} + +TEST_F(UtilsTest, trStrvToUtf8NsstringFallback) +{ + @autoreleasepool + { + constexpr auto bad = "\xF4\x33\x81\x82"sv; + NSString* const key = @"tr.strv.to.utf8.fallback"; + NSString* const comment = @"fallback string for tests"; + NSString* str = tr_strv_to_utf8_nsstring(bad, key, comment); + EXPECT_TRUE([str isEqualToString:key]); + } +} + +TEST_F(UtilsTest, trStrvToUtf8StringMixedInvalid) +{ + constexpr auto input = "hello \xF0\x28\x8C\x28 world"sv; + auto const out = tr_strv_to_utf8_string(input); + EXPECT_FALSE(out.empty()); + EXPECT_EQ(out, tr_strv_replace_invalid(out)); + EXPECT_EQ(out, tr_strv_to_utf8_string(out)); +} + +TEST_F(UtilsTest, trStrvToUtf8StringAutodetectImproves) +{ + // Shift_JIS-encoded filename from a real-world report in + // https://github.com/transmission/transmission/pull/5244#issuecomment-1474442137 + constexpr auto input = + "\x93\xC1\x96\xBD\x8C\x57\x92\xB7\x81\x45\x91\xFC\x96\xEC\x90\x6D" + "\x83\x8A\x83\x5E\x81\x5B\x83\x93\x83\x59 (D-ABC 704x396 DivX511).avi"sv; + + auto const replace_only = tr_strv_replace_invalid(input); + auto const autodetect = tr_strv_to_utf8_string(input); + + EXPECT_FALSE(autodetect.empty()); + EXPECT_EQ(autodetect, tr_strv_replace_invalid(autodetect)); + + // Autodetect should preserve more readable text than replacement-only. + EXPECT_LT(count_replacement_char(autodetect), count_replacement_char(replace_only)); + + // If autodetect improves, it should yield valid UTF-8 with real non-ASCII characters. + if (count_replacement_char(autodetect) < count_replacement_char(replace_only)) + { + EXPECT_EQ(0U, count_replacement_char(autodetect)); + EXPECT_TRUE(has_non_ascii(autodetect)); + } +} diff --git a/tests/libtransmission/utils-test.cc b/tests/libtransmission/utils-test.cc index 4700a46fb..dfc375bef 100644 --- a/tests/libtransmission/utils-test.cc +++ b/tests/libtransmission/utils-test.cc @@ -210,8 +210,8 @@ TEST_F(UtilsTest, strvConvertUtf8Fuzz) { buf.resize(tr_rand_int(4096U)); tr_rand_buffer(std::data(buf), std::size(buf)); - auto const out = tr_strv_convert_utf8({ std::data(buf), std::size(buf) }); - EXPECT_EQ(out, tr_strv_convert_utf8(out)); + auto const out = tr_strv_to_utf8_string({ std::data(buf), std::size(buf) }); + EXPECT_EQ(out, tr_strv_to_utf8_string(out)); } }