From: Joel Rosdahl Date: Sun, 26 Oct 2025 08:40:43 +0000 (+0100) Subject: enhance: Add util::parse_base16 function X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a8a3a2555e81240f18256258ae42a136f2c89054;p=thirdparty%2Fccache.git enhance: Add util::parse_base16 function --- diff --git a/src/ccache/util/string.cpp b/src/ccache/util/string.cpp index 950ba92b..e7dea93b 100644 --- a/src/ccache/util/string.cpp +++ b/src/ccache/util/string.cpp @@ -19,6 +19,7 @@ #include "string.hpp" #include +#include #include #include #include @@ -220,6 +221,48 @@ join_path_list(const std::vector& path_list) return join(path_list, k_path_delimiter); } +tl::expected +parse_base16(std::string_view hex_string) +{ + if (hex_string.size() % 2 != 0) { + return tl::unexpected( + FMT("invalid hex string (odd length): \"{}\"", hex_string)); + } + + const auto from_hex_digit = [](char ch) -> std::optional { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } else if (ch >= 'a' && ch <= 'f') { + return ch - 'a' + 10; + } else if (ch >= 'A' && ch <= 'F') { + return ch - 'A' + 10; + } else { + return std::nullopt; + } + }; + + Bytes result; + result.reserve(hex_string.size() / 2); + + for (size_t i = 0; i < hex_string.size(); i += 2) { + auto high = from_hex_digit(hex_string[i]); + auto low = from_hex_digit(hex_string[i + 1]); + + if (!high) { + return tl::unexpected( + FMT("invalid hex character at position {}: \"{}\"", i, hex_string)); + } + if (!low) { + return tl::unexpected( + FMT("invalid hex character at position {}: \"{}\"", i + 1, hex_string)); + } + + result.push_back(static_cast((*high << 4) | *low)); + } + + return result; +} + tl::expected parse_double(const std::string& value) { diff --git a/src/ccache/util/string.hpp b/src/ccache/util/string.hpp index 8b17efc0..5585c86c 100644 --- a/src/ccache/util/string.hpp +++ b/src/ccache/util/string.hpp @@ -41,6 +41,8 @@ namespace util { // --- Interface --- +class Bytes; + enum class SizeUnitPrefixType { binary, decimal }; enum class TimeZone { local, utc }; @@ -118,6 +120,12 @@ join(const T& begin, const T& end, const std::string_view delimiter); // Join paths into a string with system-dependent delimiter. std::string join_path_list(const std::vector& path_list); +// Parse a hexadecimal string into bytes. The input string must have an even +// length and contain only hexadecimal digits (0-9, a-f, A-F). +// +// Returns an error string if the input is not a valid hexadecimal string. +tl::expected parse_base16(std::string_view hex_string); + // Parse a string into a double. // // Returns an error string if `value` cannot be parsed as a double. diff --git a/unittest/test_util_string.cpp b/unittest/test_util_string.cpp index 2859ba34..5bcf210b 100644 --- a/unittest/test_util_string.cpp +++ b/unittest/test_util_string.cpp @@ -16,6 +16,7 @@ // this program; if not, write to the Free Software Foundation, Inc., 51 // Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +#include #include #include @@ -104,6 +105,88 @@ TEST_CASE("util::format_base16") CHECK(util::format_base16({data, sizeof(data)}) == "00010203"); } +TEST_CASE("util::parse_base16") +{ + SUBCASE("empty string") + { + auto result = util::parse_base16(""); + REQUIRE(result); + CHECK(result->empty()); + } + + SUBCASE("valid hex strings") + { + auto result1 = util::parse_base16("666f6f00"); + REQUIRE(result1); + CHECK(result1->size() == 4); + CHECK(result1->at(0) == 0x66); + CHECK(result1->at(1) == 0x6f); + CHECK(result1->at(2) == 0x6f); + CHECK(result1->at(3) == 0x00); + + auto result2 = util::parse_base16("00010203"); + REQUIRE(result2); + CHECK(result2->size() == 4); + CHECK(result2->at(0) == 0x00); + CHECK(result2->at(1) == 0x01); + CHECK(result2->at(2) == 0x02); + CHECK(result2->at(3) == 0x03); + } + + SUBCASE("uppercase hex") + { + auto result = util::parse_base16("DEADBEEF"); + REQUIRE(result); + CHECK(result->size() == 4); + CHECK(result->at(0) == 0xde); + CHECK(result->at(1) == 0xad); + CHECK(result->at(2) == 0xbe); + CHECK(result->at(3) == 0xef); + } + + SUBCASE("mixed case hex") + { + auto result = util::parse_base16("DeAdBeEf"); + REQUIRE(result); + CHECK(result->size() == 4); + CHECK(result->at(0) == 0xde); + CHECK(result->at(1) == 0xad); + CHECK(result->at(2) == 0xbe); + CHECK(result->at(3) == 0xef); + } + + SUBCASE("odd length string") + { + auto result = util::parse_base16("abc"); + REQUIRE(!result); + CHECK(result.error().find("odd length") != std::string::npos); + } + + SUBCASE("invalid characters") + { + auto result1 = util::parse_base16("xyz!"); + REQUIRE(!result1); + CHECK(result1.error() == "invalid hex character at position 0: \"xyz!\""); + + auto result2 = util::parse_base16("12!4"); + REQUIRE(!result2); + CHECK(result2.error() == "invalid hex character at position 2: \"12!4\""); + + auto result3 = util::parse_base16("abcg"); + REQUIRE(!result3); + CHECK(result3.error() == "invalid hex character at position 3: \"abcg\""); + } + + SUBCASE("round trip") + { + util::Bytes original = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}; + auto hex = util::format_base16(original); + auto result = util::parse_base16(hex); + REQUIRE(result); + CHECK(*result == original); + } +} + TEST_CASE("util::format_base32hex") { // Test vectors (without padding) from RFC 4648.