diff --git a/merklecpp.h b/merklecpp.h index 5cc87ac..2ef483a 100644 --- a/merklecpp.h +++ b/merklecpp.h @@ -88,6 +88,26 @@ namespace merkle return r; } + static inline bool decode_hex_digit(char c, uint8_t& value) + { + if ('0' <= c && c <= '9') + { + value = static_cast(c - '0'); + return true; + } + if ('a' <= c && c <= 'f') + { + value = static_cast(c - 'a' + 10); + return true; + } + if ('A' <= c && c <= 'F') + { + value = static_cast(c - 'A' + 10); + return true; + } + return false; + } + /// @brief Template for fixed-size hashes /// @tparam SIZE Size of the hash in number of bytes template @@ -119,9 +139,14 @@ namespace merkle } for (size_t i = 0; i < SIZE; i++) { - int tmp = 0; - sscanf(s.c_str() + 2 * i, "%02x", &tmp); - bytes[i] = tmp; + uint8_t high = 0; + uint8_t low = 0; + if (!decode_hex_digit(s[2 * i], high) || + !decode_hex_digit(s[2 * i + 1], low)) + { + throw std::runtime_error("invalid hash string"); + } + bytes[i] = static_cast((high << 4) | low); } } diff --git a/test/coverage.cpp b/test/coverage.cpp index d431451..ef62271 100644 --- a/test/coverage.cpp +++ b/test/coverage.cpp @@ -108,6 +108,32 @@ namespace "deserialise_uint64_t should reject short buffers"); } + void test_hash_string_parsing() + { + const std::string valid_hex(64, 'a'); + const merkle::Hash valid_hash(valid_hex); + for (const auto byte : valid_hash.bytes) + { + require(byte == 0xAA, "valid hash string parsed incorrectly"); + } + + std::string mixed_case_hex(64, '0'); + mixed_case_hex[0] = 'A'; + mixed_case_hex[1] = 'f'; + const merkle::Hash mixed_case_hash(mixed_case_hex); + require(mixed_case_hash.bytes[0] == 0xAF, "mixed-case hash string parsed incorrectly"); + + require_throws( + [] { merkle::Hash(std::string(64, 'z')); }, + "hash string should reject non-hex digits"); + + std::string partially_invalid(64, '0'); + partially_invalid[1] = 'z'; + require_throws( + [&] { (void)merkle::Hash(partially_invalid); }, + "hash string should reject partially parsed hex bytes"); + } + void test_path_metadata_and_equality() { merkle::Tree tree; @@ -274,6 +300,7 @@ int main() try { test_serialisation_helpers(); + test_hash_string_parsing(); test_path_metadata_and_equality(); test_tree_partial_serialisation_bounds(); test_tree_assignment_and_moves(); diff --git a/test/unit_tests.cpp b/test/unit_tests.cpp index c6b7fe2..e6f6967 100644 --- a/test/unit_tests.cpp +++ b/test/unit_tests.cpp @@ -38,6 +38,11 @@ TEST_CASE("HashT constructors and error paths") const merkle::Hash h_str(valid_hex); REQUIRE(h_str.bytes[0] == 0xAB); + valid_hex[0] = 'A'; + valid_hex[1] = 'f'; + const merkle::Hash mixed_case_h_str(valid_hex); + REQUIRE(mixed_case_h_str.bytes[0] == 0xAF); + // String constructor: invalid length throws REQUIRE_THROWS(merkle::Hash(std::string(63, '0'))); REQUIRE_THROWS(merkle::Hash(std::string(65, '0')));