Repository: bitcoin/bitcoin
File: src/util/strencodings.h (C/C++)

Code before:
        requires(N % 2 == 1)
    {
        if (hex_str[N - 1]) throw "null terminator required";
        for (std::size_t i = 0; i < bytes.size(); ++i) {
            bytes[i] = static_cast<std::byte>(
                (ConstevalHexDigit(hex_str[2 * i]) << 4) |
                 ConstevalHexDigit(hex_str[2 * i + 1]));
        }
    }
};
} // namespace detail

struct AsciiCaseInsensitiveKeyEqual {
    bool operator()(const std::string& s1, const std::string& s2) const
    {
        return ToLower(s1) == ToLower(s2);
    }
};

struct AsciiCaseInsensitiveHash {
    size_t operator()(const std::string_view& s) const {
        std::string lowered;
        lowered.resize(s.size());
        std::ranges::transform(s, lowered.begin(),
            [](char c) {
                // Avoid implicit-integer-sign-change UB by only
                // processing ASCII.
                auto uc = static_cast<unsigned char>(c);
                if (uc >= 128) return c;
                return ToLower(c);
            });
        return std::hash<std::string>{}(lowered);
    }
};

/**
 * ""_hex is a compile-time user-defined literal returning a
 * `std::array<std::byte>`, equivalent to ParseHex(). Variants provided:
 *
 * - ""_hex_v: Returns `std::vector<std::byte>`, useful for heap allocation or
 *   variable-length serialization.
 *
 * - ""_hex_u8: Returns `std::array<uint8_t>`, for cases where `std::byte` is
 *   incompatible.
 *
 * - ""_hex_v_u8: Returns `std::vector<uint8_t>`, combining heap allocation with
 *   `uint8_t`.
 *
 * @warning It could be necessary to use vector instead of array variants when
 *   serializing, or vice versa, because vectors are assumed to be variable-
 *   length and serialized with a size prefix, while arrays are considered fixed

Review comment (bug):
I don't understand this. sign-change is not UB. Also, i fail to see how it could happen with ToLower? Also, no need to create two copies, when one is enough. The following diff passes isan for me:

```diff
diff --git a/src/test/util_string_tests.cpp b/src/test/util_string_tests.cpp
index 82ddd209c2..4a49e5d090 100644
--- a/src/test/util_string_tests.cpp
+++ b/src/test/util_string_tests.cpp
@@ -178,6 +178,7 @@ BOOST_AUTO_TEST_CASE(ascii_case_insensitive_hash_test)
     BOOST_CHECK_NE(hsh("AA"), hsh("A"));
     BOOST_CHECK_EQUAL(hsh("A"), hsh("a"));
     BOOST_CHECK_EQUAL(hsh("Ab"), hsh("aB"));
+    BOOST_CHECK_EQUAL(hsh("A\xfe"), hsh("a\xfe"));
 }
 
 BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/util/strencodings.h b/src/util/strencodings.h
index b005628f24..3b49eafd6e 100644
--- a/src/util/strencodings.h
+++ b/src/util/strencodings.h
@@ -355,25 +355,16 @@ struct Hex {
 } // namespace detail
 
 struct AsciiCaseInsensitiveKeyEqual {
-    bool operator()(const std::string& s1, const std::string& s2) const
+    bool operator()(std::string_view s1, std::string_view s2) const
     {
         return ToLower(s1) == ToLower(s2);
     }
 };
 
 struct AsciiCaseInsensitiveHash {
-    size_t operator()(const std::string_view& s) const {
-        std::string lowered;
-        lowered.resize(s.size());
-        std::ranges::transform(s, lowered.begin(),
-            [](char c) {
-                // Avoid implicit-integer-sign-change UB by only
-                // processing ASCII.
-                auto uc = static_cast<unsigned char>(c);
-                if (uc >= 128) return c;
-                return ToLower(c);
-            });
-        return std::hash<std::string>{}(lowered);
+    size_t operator()(std::string_view s) const
+    {
+        return std::hash<std::string>{}(ToLower(s));
     }
 };
 
```

```
$ UBSAN_OPTIONS="suppressions=$(pwd)/test/sanitizer_suppressions/ubsan:print_stacktrace=1:halt_on_error=1:report_error_type=1" ./bld-cmake/bin/test_bitcoin -t util_string_tests
Running 3 test cases...

*** No errors detected


Code after:
        requires(N % 2 == 1)
    {
        if (hex_str[N - 1]) throw "null terminator required";
        for (std::size_t i = 0; i < bytes.size(); ++i) {
            bytes[i] = static_cast<std::byte>(
                (ConstevalHexDigit(hex_str[2 * i]) << 4) |
                 ConstevalHexDigit(hex_str[2 * i + 1]));
        }
    }
};
} // namespace detail

struct AsciiCaseInsensitiveKeyEqual {
    bool operator()(std::string_view s1, std::string_view s2) const
    {
        return ToLower(s1) == ToLower(s2);
    }
};

struct AsciiCaseInsensitiveHash {
    size_t operator()(std::string_view s) const
    {
        return std::hash<std::string>{}(ToLower(s));
    }
};

/**
 * ""_hex is a compile-time user-defined literal returning a
 * `std::array<std::byte>`, equivalent to ParseHex(). Variants provided:
 *
 * - ""_hex_v: Returns `std::vector<std::byte>`, useful for heap allocation or
 *   variable-length serialization.
 *
 * - ""_hex_u8: Returns `std::array<uint8_t>`, for cases where `std::byte` is
 *   incompatible.
 *
 * - ""_hex_v_u8: Returns `std::vector<uint8_t>`, combining heap allocation with
 *   `uint8_t`.
 *
 * @warning It could be necessary to use vector instead of array variants when
 *   serializing, or vice versa, because vectors are assumed to be variable-
 *   length and serialized with a size prefix, while arrays are considered fixed
 *   length and serialized with no prefix.
 *
 * @warning It may be preferable to use vector variants to save stack space when
 *   declaring local variables if hex strings are large. Alternatively variables
 *   could be declared constexpr to avoid using stack space.
 *
 * @warning Avoid `uint8_t` variants when not necessary, as the codebase
 *   migrates to use `std::byte` instead of `unsigned char` and `uint8_t`.
 *