]> git.ipfire.org Git - thirdparty/ccache.git/commitdiff
enhance: Add util::parse_base16 function
authorJoel Rosdahl <joel@rosdahl.net>
Sun, 26 Oct 2025 08:40:43 +0000 (09:40 +0100)
committerJoel Rosdahl <joel@rosdahl.net>
Sun, 26 Oct 2025 17:24:21 +0000 (18:24 +0100)
src/ccache/util/string.cpp
src/ccache/util/string.hpp
unittest/test_util_string.cpp

index 950ba92babfd42548da2f0f56ddf4ded0a2abc66..e7dea93b92e5b7d778e85db19a80fddba9c9c8e2 100644 (file)
@@ -19,6 +19,7 @@
 #include "string.hpp"
 
 #include <ccache/util/assertions.hpp>
+#include <ccache/util/bytes.hpp>
 #include <ccache/util/expected.hpp>
 #include <ccache/util/filesystem.hpp>
 #include <ccache/util/format.hpp>
@@ -220,6 +221,48 @@ join_path_list(const std::vector<std::filesystem::path>& path_list)
   return join(path_list, k_path_delimiter);
 }
 
+tl::expected<Bytes, std::string>
+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<uint8_t> {
+    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<uint8_t>((*high << 4) | *low));
+  }
+
+  return result;
+}
+
 tl::expected<double, std::string>
 parse_double(const std::string& value)
 {
index 8b17efc05f0d6f830ffcba7814aa77926f20afcd..5585c86c8523b2fcab0677b06b4c929946e653ee 100644 (file)
@@ -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<std::filesystem::path>& 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<Bytes, std::string> 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.
index 2859ba34015a570c78d3765c572125e53a01d19f..5bcf210b345a344d1df745bcb69cdb39643f567f 100644 (file)
@@ -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 <ccache/util/bytes.hpp>
 #include <ccache/util/string.hpp>
 
 #include <doctest/doctest.h>
@@ -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.