Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/core/json/include/sourcemeta/core/json_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,28 @@ template <typename Key, typename Value, typename Hash> class JSONObject {
key_type first;
mapped_type second;
hash_type hash;

/// Check whether this entry's key equals the given key, comparing the
/// precomputed hashes first and only falling back to a string comparison
/// when the hash is not perfect. For example:
///
/// ```cpp
/// #include <sourcemeta/core/json.h>
/// #include <cassert>
///
/// const sourcemeta::core::JSON document =
/// sourcemeta::core::parse_json("{ \"foo\": 1 }");
/// const auto &entry{*document.as_object().cbegin()};
/// assert(entry.key_equals(
/// "foo", sourcemeta::core::JSON::Object::hash("foo")));
/// ```
[[nodiscard]] inline auto key_equals(const KeyView key,
const hash_type key_hash) const
-> bool {
assert(JSONObject::hash(key) == key_hash);
return this->hash == key_hash &&

@augmentcode augmentcode Bot Jun 27, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/core/json/include/sourcemeta/core/json_object.h:58: key_equals can return a false positive if a caller accidentally passes a key_hash that doesn’t correspond to the provided key (especially in the is_perfect case where the string comparison is skipped). Other APIs that accept a precomputed hash typically assert(hash(key) == key_hash), so it may be worth adding a similar defensive check here to catch misuse in debug builds.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
(hasher.is_perfect(key_hash) || this->first == key);
}
};

using underlying_type = std::vector<Entry>;
Expand Down
36 changes: 36 additions & 0 deletions test/json/json_object_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1478,3 +1478,39 @@ TEST(JSON_object, assign_if_missing_with_string_view) {
EXPECT_EQ(document.at("foo").to_integer(), 1);
EXPECT_EQ(document.at("bar").to_integer(), 2);
}

TEST(JSON_object, entry_key_equals_perfect_match) {
const auto document{sourcemeta::core::parse_json(R"({ "foo": 1 })")};
const auto &entry{*document.as_object().cbegin()};
EXPECT_TRUE(
entry.key_equals("foo", sourcemeta::core::JSON::Object::hash("foo")));
}

TEST(JSON_object, entry_key_equals_perfect_mismatch) {
const auto document{sourcemeta::core::parse_json(R"({ "foo": 1 })")};
const auto &entry{*document.as_object().cbegin()};
EXPECT_FALSE(
entry.key_equals("bar", sourcemeta::core::JSON::Object::hash("bar")));
}

TEST(JSON_object, entry_key_equals_imperfect_match) {
const std::string key(40, 'a');
sourcemeta::core::JSON document{sourcemeta::core::JSON::make_object()};
document.assign(key, sourcemeta::core::JSON{1});
const auto &entry{*document.as_object().cbegin()};
EXPECT_FALSE(document.as_object().hash(key) ==
sourcemeta::core::JSON::Object::hash("a"));
EXPECT_TRUE(entry.key_equals(key, sourcemeta::core::JSON::Object::hash(key)));
}

TEST(JSON_object, entry_key_equals_imperfect_collision_is_rejected) {
const std::string stored{std::string(31, 'a') + "11111110" + "z"};
const std::string other{std::string(31, 'a') + "22222220" + "z"};
sourcemeta::core::JSON document{sourcemeta::core::JSON::make_object()};
document.assign(stored, sourcemeta::core::JSON{1});
const auto &entry{*document.as_object().cbegin()};
EXPECT_EQ(sourcemeta::core::JSON::Object::hash(stored),
sourcemeta::core::JSON::Object::hash(other));
EXPECT_FALSE(
entry.key_equals(other, sourcemeta::core::JSON::Object::hash(other)));
}
Loading