diff --git a/.clang-tidy b/.clang-tidy index be6c954..eb16075 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -13,10 +13,12 @@ Checks: > -cert-msc51-cpp, -cert-oop54-cpp, clang-analyzer-*, + -clang-analyzer-cplusplus.NewDeleteLeaks, concurrency-*, -concurrency-mt-unsafe, cppcoreguidelines-*, -cppcoreguidelines-avoid-c-arrays, + -cppcoreguidelines-avoid-do-while, -cppcoreguidelines-avoid-magic-numbers, -cppcoreguidelines-c-copy-assignment-signature, -cppcoreguidelines-non-private-member-variables-in-classes, @@ -41,6 +43,7 @@ Checks: > -misc-include-cleaner, -misc-non-private-member-variables-in-classes, -misc-unconventional-assign-operator, + -misc-use-anonymous-namespace, modernize-*, -modernize-avoid-c-arrays, -modernize-use-trailing-return-type, diff --git a/.gitignore b/.gitignore index 6219317..023b239 100644 --- a/.gitignore +++ b/.gitignore @@ -352,6 +352,9 @@ MigrationBackup/ doc/html doc/xml -build/* +build*/ _codeql_build_dir/ _codeql_detected_source_root +coverage*.info +coverage_html/ +coverage_full_html/ diff --git a/test/unit_tests.cpp b/test/unit_tests.cpp index d5c70ae..c6b7fe2 100644 --- a/test/unit_tests.cpp +++ b/test/unit_tests.cpp @@ -7,6 +7,222 @@ #include #include +TEST_CASE("HashT constructors and error paths") +{ + // Default constructor: all bytes zero + const merkle::Hash h0; + REQUIRE(h0.size() == 32); + REQUIRE(h0.serialised_size() == 32); + for (const auto& b : h0.bytes) + { + REQUIRE(b == 0); + } + + // uint8_t* constructor + std::array arr{}; + arr[0] = 0xAB; + arr[31] = 0xCD; + const merkle::Hash h_ptr(arr.data()); + REQUIRE(h_ptr.bytes[0] == 0xAB); + REQUIRE(h_ptr.bytes[31] == 0xCD); + + // std::array constructor + const merkle::Hash h_arr(arr); + REQUIRE(h_arr.bytes[0] == 0xAB); + REQUIRE(h_arr.bytes[31] == 0xCD); + + // String constructor: valid 64-char hex string + std::string valid_hex(64, '0'); + valid_hex[0] = 'a'; + valid_hex[1] = 'b'; + const merkle::Hash h_str(valid_hex); + REQUIRE(h_str.bytes[0] == 0xAB); + + // String constructor: invalid length throws + REQUIRE_THROWS(merkle::Hash(std::string(63, '0'))); + REQUIRE_THROWS(merkle::Hash(std::string(65, '0'))); + REQUIRE_THROWS(merkle::Hash(std::string())); + + // Vector constructor: valid + std::vector vec(32, 0); + vec[0] = 0xCA; + const merkle::Hash h_vec(vec); + REQUIRE(h_vec.bytes[0] == 0xCA); + + // Vector constructor: too short throws + const std::vector short_vec(31, 0); + REQUIRE_THROWS(merkle::Hash(short_vec)); + const std::vector empty_vec; + REQUIRE_THROWS(merkle::Hash(empty_vec)); + + // Vector+position constructor: valid + std::vector pos_vec(40, 0); + pos_vec[8] = 0xFE; + size_t pos = 8; + const merkle::Hash h_pos(pos_vec, pos); + REQUIRE(h_pos.bytes[0] == 0xFE); + REQUIRE(pos == 40); + + // Vector+position constructor: not enough bytes remaining throws + size_t pos2 = 10; + const std::vector too_short(41, 0); // only 31 bytes from position 10 + REQUIRE_THROWS(merkle::Hash(too_short, pos2)); +} + +TEST_CASE("HashT methods") +{ + // zero() clears all bytes + merkle::Hash h; + h.bytes[0] = 0xFF; + h.bytes[31] = 0xFF; + h.zero(); + for (const auto& b : h.bytes) + { + REQUIRE(b == 0); + } + + // to_string (lower case) + merkle::Hash h2; + h2.bytes[0] = 0xAB; + h2.bytes[1] = 0xCD; + const std::string s = h2.to_string(); + REQUIRE(s.size() == 64); + REQUIRE(s.substr(0, 4) == "abcd"); + + // to_string upper case + const std::string s_upper = h2.to_string(32, false); + REQUIRE(s_upper.substr(0, 4) == "ABCD"); + + // to_string with limited num_bytes + const std::string s2 = h2.to_string(2); + REQUIRE(s2.size() == 4); + REQUIRE(s2 == "abcd"); + + // operator== and operator!= + merkle::Hash ha; + merkle::Hash hb; + REQUIRE(ha == hb); + REQUIRE_FALSE(ha != hb); + hb.bytes[0] = 1; + REQUIRE_FALSE(ha == hb); + REQUIRE(ha != hb); + + // Assignment operator + merkle::Hash hc; + hc = hb; + REQUIRE(hc == hb); + + // serialise / deserialise round-trip + std::vector buf; + hb.serialise(buf); + REQUIRE(buf.size() == 32); + REQUIRE(buf[0] == 1); + size_t pos = 0; + merkle::Hash hd; + hd.deserialise(buf, pos); + REQUIRE(hd == hb); + REQUIRE(pos == 32); + + // operator std::vector() + merkle::Hash he; + he.bytes[5] = 0x42; + const std::vector converted = he; + REQUIRE(converted.size() == 32); + REQUIRE(converted[5] == 0x42); +} + +TEST_CASE("PathT equality") +{ + // Build a two-leaf tree and get paths to both leaves + const merkle::Tree::Hash h0; + merkle::Tree::Hash h1; + h1.bytes[31] = 1; + + merkle::Tree tree; + tree.insert(h0); + tree.insert(h1); + + const auto path0a = tree.path(0); + const auto path0b = tree.path(0); // same path extracted twice + const auto path1 = tree.path(1); // path to a different leaf + + // Two paths to the same leaf should be equal + REQUIRE(*path0a == *path0b); + REQUIRE_FALSE(*path0a != *path0b); + + // Paths to different leaves differ in leaf hash → first return false branch + REQUIRE_FALSE(*path0a == *path1); + REQUIRE(*path0a != *path1); + + // Same leaf, different element hash → second return false branch + merkle::Tree tree_diff; + tree_diff.insert(h0); + merkle::Tree::Hash h3; + h3.bytes[0] = 3; + tree_diff.insert(h3); + + const auto path_orig = tree.path(0); // h0 leaf, element has h1 + const auto path_diff = tree_diff.path(0); // h0 leaf, element has h3 + REQUIRE_FALSE(*path_orig == *path_diff); + REQUIRE(*path_orig != *path_diff); +} + +TEST_CASE("TreeT to_string") +{ + // Empty tree produces "" + merkle::Tree empty_tree; + const std::string empty_str = empty_tree.to_string(); + REQUIRE(empty_str.find("") != std::string::npos); + + // Non-empty tree produces normal output + merkle::Tree::Hash h; + h.bytes[0] = 1; + merkle::Tree tree; + tree.insert(h); + const std::string tree_str = tree.to_string(); + REQUIRE(tree_str.find("") == std::string::npos); + REQUIRE(!tree_str.empty()); +} + +TEST_CASE("TreeT leaf bounds and uninserted leaves") +{ + // leaf() out of bounds on empty tree + merkle::Tree empty_tree; + REQUIRE_THROWS(empty_tree.leaf(0)); + + // Access leaf before insertion is flushed (uninserted_leaf_nodes path) + merkle::Tree tree; + merkle::Tree::Hash h0; + merkle::Tree::Hash h1; + h1.bytes[0] = 1; + tree.insert(h0); + tree.insert(h1); + + // leaf() on valid indices before root is computed + REQUIRE(tree.leaf(0) == h0); + REQUIRE(tree.leaf(1) == h1); + + // leaf() out of bounds throws + REQUIRE_THROWS(tree.leaf(2)); + REQUIRE_THROWS(tree.leaf(100)); +} + +TEST_CASE("TreeT size with uninserted leaves") +{ + merkle::Tree tree; + // size() when tree has uninserted leaves triggers insert_leaves() + merkle::Tree::Hash h; + tree.insert(h); + // size() forces lazy insertion + const size_t sz = tree.size(); + REQUIRE(sz > 0); + + // Tree copy + merkle::Tree copy = tree; // NOLINT(misc-const-correctness) + REQUIRE(copy.size() == tree.size()); + REQUIRE(copy.root() == tree.root()); +} + TEST_CASE("Empty tree") { merkle::Tree tree; @@ -25,7 +241,7 @@ TEST_CASE("Empty tree") std::vector buffer; REQUIRE_NOTHROW(tree.serialise(buffer)); - REQUIRE_NOTHROW(merkle::Tree dt(buffer)); + REQUIRE_NOTHROW(merkle::Tree dt(buffer)); // NOLINT(misc-const-correctness) } TEST_CASE("One-node tree") @@ -49,13 +265,15 @@ TEST_CASE("One-node tree") std::vector buffer; REQUIRE_NOTHROW(tree.serialise(buffer)); - merkle::Tree dt(buffer); + merkle::Tree dt(buffer); // NOLINT(misc-const-correctness) REQUIRE(dt.root() == tree.root()); } TEST_CASE("Three-node tree") { - merkle::Tree::Hash h0, h1, hr; + merkle::Tree::Hash h0; + merkle::Tree::Hash h1; + merkle::Tree::Hash hr; h1.bytes[31] = 1; merkle::Tree tree; @@ -95,7 +313,7 @@ TEST_CASE("Three-node tree") std::vector buffer; REQUIRE_NOTHROW(tree.serialise(buffer)); - merkle::Tree dt(buffer); + merkle::Tree dt(buffer); // NOLINT(misc-const-correctness) REQUIRE(dt.root() == tree.root()); merkle::Tree copy = tree; @@ -132,7 +350,7 @@ TEST_CASE("SHA384 empty tree") std::vector buffer; REQUIRE_NOTHROW(tree.serialise(buffer)); - REQUIRE_NOTHROW(merkle::Tree384 dt(buffer)); + REQUIRE_NOTHROW(merkle::Tree384 dt(buffer)); // NOLINT(misc-const-correctness) } TEST_CASE("SHA384 one-node tree") @@ -156,13 +374,15 @@ TEST_CASE("SHA384 one-node tree") std::vector buffer; REQUIRE_NOTHROW(tree.serialise(buffer)); - merkle::Tree384 dt(buffer); + merkle::Tree384 dt(buffer); // NOLINT(misc-const-correctness) REQUIRE(dt.root() == tree.root()); } TEST_CASE("SHA384 three-node tree") { - merkle::Tree384::Hash h0, h1, hr; + merkle::Tree384::Hash h0; + merkle::Tree384::Hash h1; + merkle::Tree384::Hash hr; h1.bytes[47] = 1; merkle::Tree384 tree; @@ -194,7 +414,7 @@ TEST_CASE("SHA384 three-node tree") std::vector buffer; REQUIRE_NOTHROW(tree.serialise(buffer)); - merkle::Tree384 dt(buffer); + merkle::Tree384 dt(buffer); // NOLINT(misc-const-correctness) REQUIRE(dt.root() == tree.root()); merkle::Tree384 copy = tree; @@ -219,7 +439,9 @@ TEST_CASE("SHA384 paths") merkle::Tree384 tree; for (auto& h : hashes) + { tree.insert(h); + } auto root = tree.root(); for (size_t i = 0; i < num_leaves; i++) @@ -250,7 +472,7 @@ TEST_CASE("SHA512 empty tree") std::vector buffer; REQUIRE_NOTHROW(tree.serialise(buffer)); - REQUIRE_NOTHROW(merkle::Tree512 dt(buffer)); + REQUIRE_NOTHROW(merkle::Tree512 dt(buffer)); // NOLINT(misc-const-correctness) } TEST_CASE("SHA512 one-node tree") @@ -274,13 +496,15 @@ TEST_CASE("SHA512 one-node tree") std::vector buffer; REQUIRE_NOTHROW(tree.serialise(buffer)); - merkle::Tree512 dt(buffer); + merkle::Tree512 dt(buffer); // NOLINT(misc-const-correctness) REQUIRE(dt.root() == tree.root()); } TEST_CASE("SHA512 three-node tree") { - merkle::Tree512::Hash h0, h1, hr; + merkle::Tree512::Hash h0; + merkle::Tree512::Hash h1; + merkle::Tree512::Hash hr; h1.bytes[63] = 1; merkle::Tree512 tree; @@ -312,7 +536,7 @@ TEST_CASE("SHA512 three-node tree") std::vector buffer; REQUIRE_NOTHROW(tree.serialise(buffer)); - merkle::Tree512 dt(buffer); + merkle::Tree512 dt(buffer); // NOLINT(misc-const-correctness) REQUIRE(dt.root() == tree.root()); merkle::Tree512 copy = tree; @@ -337,7 +561,9 @@ TEST_CASE("SHA512 paths") merkle::Tree512 tree; for (auto& h : hashes) + { tree.insert(h); + } auto root = tree.root(); for (size_t i = 0; i < num_leaves; i++)