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
40 changes: 39 additions & 1 deletion src/core/jsonpointer/include/sourcemeta/core/jsonpointer_token.h
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,39 @@ template <typename PropertyT, typename Hash> class GenericToken {
return this->hash;
}

/// Check whether a JSON Pointer property token equals the given string,
/// 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 <sourcemeta/core/jsonpointer.h>
/// #include <cassert>
///
/// const sourcemeta::core::Pointer::Token token{"foo"};
/// assert(token.property_equals(
/// "foo", sourcemeta::core::JSON::Object::hash("foo")));
/// ```
[[nodiscard]] auto
property_equals(const JSON::StringView value,
const typename Hash::hash_type value_hash) const noexcept
-> bool {
assert(this->is_property());
assert(hasher(value.data(), value.size()) == value_hash);
if constexpr (requires { hasher.is_perfect(value_hash); }) {
// A perfect hash captures the property bytes but not its length, so
// two properties that differ only by trailing NUL bytes hash equal.
// Comparing sizes disambiguates them without the cost of a full
// string comparison
return this->hash == value_hash &&
(hasher.is_perfect(value_hash)
? this->to_property().size() == value.size()
: this->to_property() == value);
} else {
return this->hash == value_hash && this->to_property() == value;
}
}

/// Get the underlying value of a JSON Pointer object property token
/// (non-`const` overload). For example:
///
Expand Down Expand Up @@ -251,8 +284,13 @@ template <typename PropertyT, typename Hash> class GenericToken {
return false;
} else if (this->as_property) {
if constexpr (requires { hasher.is_perfect(this->hash); }) {
// A perfect hash captures the property bytes but not its length, so
// two properties that differ only by trailing NUL bytes hash equal.
// Comparing sizes disambiguates them without the cost of a full
// string comparison
if (hasher.is_perfect(this->hash) && hasher.is_perfect(other.hash)) {
return this->hash == other.hash;
return this->hash == other.hash &&
this->to_property().size() == other.to_property().size();
}
}

Expand Down
198 changes: 198 additions & 0 deletions test/jsonpointer/jsonpointer_token_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <sourcemeta/core/jsonpointer.h>
#include <sourcemeta/core/test.h>

#include <functional>
#include <type_traits>

TEST(general_traits) {
Expand Down Expand Up @@ -93,6 +94,49 @@ TEST(to_json_property) {
EXPECT_EQ(result.to_string(), "foo");
}

TEST(equality_short_property_tokens_equal) {
const sourcemeta::core::Pointer::Token first{"foo"};
const sourcemeta::core::Pointer::Token second{"foo"};
EXPECT_EQ(first, second);
EXPECT_EQ(second, first);
}

TEST(equality_short_property_tokens_not_equal) {
const sourcemeta::core::Pointer::Token first{"foo"};
const sourcemeta::core::Pointer::Token second{"bar"};
EXPECT_NE(first, second);
EXPECT_NE(second, first);
}

TEST(equality_long_property_tokens_equal) {
const sourcemeta::core::JSON::String property(40, 'a');
const sourcemeta::core::Pointer::Token first{property};
const sourcemeta::core::Pointer::Token second{property};
EXPECT_EQ(first, second);
EXPECT_EQ(second, first);
}

TEST(equality_property_tokens_differing_by_trailing_null_not_equal) {
sourcemeta::core::JSON::String property_with_null{"a"};
property_with_null.push_back('\x00');
const sourcemeta::core::Pointer::Token with_null{property_with_null};
const sourcemeta::core::Pointer::Token without_null{"a"};
EXPECT_NE(with_null, without_null);
EXPECT_NE(without_null, with_null);
}

TEST(equality_property_tokens_imperfect_hash_collision_not_equal) {
const sourcemeta::core::JSON::String stored{
sourcemeta::core::JSON::String(31, 'a') + "11111110" + "z"};
const sourcemeta::core::JSON::String other{
sourcemeta::core::JSON::String(31, 'a') + "22222220" + "z"};
const sourcemeta::core::Pointer::Token first{stored};
const sourcemeta::core::Pointer::Token second{other};
EXPECT_EQ(first.property_hash(), second.property_hash());
EXPECT_NE(first, second);
EXPECT_NE(second, first);
}

TEST(property_hash) {
const sourcemeta::core::Pointer::Token first{"foo"};
const sourcemeta::core::Pointer::Token second{"bar"};
Expand All @@ -102,6 +146,160 @@ TEST(property_hash) {
EXPECT_EQ(first.property_hash(), third.property_hash());
}

TEST(property_equals_perfect_match) {
const sourcemeta::core::Pointer::Token token{"foo"};
EXPECT_TRUE(token.property_equals(
"foo", sourcemeta::core::JSON::Object::hash("foo")));
}

TEST(property_equals_perfect_mismatch_same_size) {
const sourcemeta::core::Pointer::Token token{"foo"};
EXPECT_FALSE(token.property_equals(
"bar", sourcemeta::core::JSON::Object::hash("bar")));
}

TEST(property_equals_perfect_mismatch_different_size) {
const sourcemeta::core::Pointer::Token token{"foo"};
EXPECT_FALSE(token.property_equals(
"foobar", sourcemeta::core::JSON::Object::hash("foobar")));
}

TEST(property_equals_character_token_match) {
const sourcemeta::core::Pointer::Token token{'a'};
EXPECT_TRUE(
token.property_equals("a", sourcemeta::core::JSON::Object::hash("a")));
}

TEST(property_equals_empty_property_match) {
const sourcemeta::core::Pointer::Token token{""};
EXPECT_TRUE(
token.property_equals("", sourcemeta::core::JSON::Object::hash("")));
}

TEST(property_equals_empty_property_mismatch) {
const sourcemeta::core::Pointer::Token token{""};
EXPECT_FALSE(
token.property_equals("a", sourcemeta::core::JSON::Object::hash("a")));
}

TEST(property_equals_empty_value_mismatch) {
const sourcemeta::core::Pointer::Token token{"a"};
EXPECT_FALSE(
token.property_equals("", sourcemeta::core::JSON::Object::hash("")));
}

TEST(property_equals_property_with_trailing_null_not_matched_as_prefix) {
sourcemeta::core::JSON::String property_with_null{"a"};
property_with_null.push_back('\x00');
const sourcemeta::core::Pointer::Token token{property_with_null};
EXPECT_FALSE(
token.property_equals("a", sourcemeta::core::JSON::Object::hash("a")));
}

TEST(property_equals_value_with_trailing_null_not_matched_as_prefix) {
sourcemeta::core::JSON::String value_with_null{"a"};
value_with_null.push_back('\x00');
const sourcemeta::core::Pointer::Token token{"a"};
EXPECT_FALSE(token.property_equals(
value_with_null, sourcemeta::core::JSON::Object::hash(value_with_null)));
}

TEST(property_equals_property_with_trailing_null_match) {
sourcemeta::core::JSON::String property_with_null{"a"};
property_with_null.push_back('\x00');
const sourcemeta::core::Pointer::Token token{property_with_null};
EXPECT_TRUE(token.property_equals(
property_with_null,
sourcemeta::core::JSON::Object::hash(property_with_null)));
}

TEST(property_equals_longest_perfect_property_match) {
const sourcemeta::core::JSON::String property(31, 'a');
const sourcemeta::core::Pointer::Token token{property};
EXPECT_TRUE(token.property_equals(
property, sourcemeta::core::JSON::Object::hash(property)));
}

TEST(property_equals_shortest_imperfect_property_match) {
const sourcemeta::core::JSON::String property(32, 'a');
const sourcemeta::core::Pointer::Token token{property};
EXPECT_TRUE(token.property_equals(
property, sourcemeta::core::JSON::Object::hash(property)));
}

TEST(property_equals_perfect_property_against_imperfect_prefix_value) {
const sourcemeta::core::JSON::String property(31, 'a');
const sourcemeta::core::JSON::String value(32, 'a');
const sourcemeta::core::Pointer::Token token{property};
EXPECT_FALSE(token.property_equals(
value, sourcemeta::core::JSON::Object::hash(value)));
}

TEST(property_equals_imperfect_property_against_perfect_prefix_value) {
const sourcemeta::core::JSON::String property(32, 'a');
const sourcemeta::core::JSON::String value(31, 'a');
const sourcemeta::core::Pointer::Token token{property};
EXPECT_FALSE(token.property_equals(
value, sourcemeta::core::JSON::Object::hash(value)));
}

TEST(property_equals_long_property_match) {
const sourcemeta::core::JSON::String property(40, 'a');
const sourcemeta::core::Pointer::Token token{property};
EXPECT_TRUE(token.property_equals(
property, sourcemeta::core::JSON::Object::hash(property)));
}

TEST(property_equals_long_property_mismatch) {
const sourcemeta::core::JSON::String property(40, 'a');
const sourcemeta::core::JSON::String value(40, 'b');
const sourcemeta::core::Pointer::Token token{property};
EXPECT_FALSE(token.property_equals(
value, sourcemeta::core::JSON::Object::hash(value)));
}

TEST(property_equals_imperfect_hash_collision_rejected) {
const sourcemeta::core::JSON::String property{
sourcemeta::core::JSON::String(31, 'a') + "11111110" + "z"};
const sourcemeta::core::JSON::String value{
sourcemeta::core::JSON::String(31, 'a') + "22222220" + "z"};
const sourcemeta::core::Pointer::Token token{property};
EXPECT_EQ(sourcemeta::core::JSON::Object::hash(property),
sourcemeta::core::JSON::Object::hash(value));
EXPECT_FALSE(token.property_equals(
value, sourcemeta::core::JSON::Object::hash(value)));
}

TEST(property_equals_weak_token_perfect_match) {
const std::string property{"foo"};
const sourcemeta::core::WeakPointer::Token token{std::cref(property)};
EXPECT_TRUE(token.property_equals(
"foo", sourcemeta::core::JSON::Object::hash("foo")));
}

TEST(property_equals_weak_token_perfect_mismatch) {
const std::string property{"foo"};
const sourcemeta::core::WeakPointer::Token token{std::cref(property)};
EXPECT_FALSE(token.property_equals(
"bar", sourcemeta::core::JSON::Object::hash("bar")));
}

TEST(property_equals_weak_token_property_with_trailing_null_not_matched) {
std::string property_with_null{"a"};
property_with_null.push_back('\x00');
const sourcemeta::core::WeakPointer::Token token{
std::cref(property_with_null)};
EXPECT_FALSE(
token.property_equals("a", sourcemeta::core::JSON::Object::hash("a")));
}

TEST(property_equals_weak_token_long_property_match) {
const std::string property(40, 'a');
const sourcemeta::core::WeakPointer::Token token{std::cref(property)};
EXPECT_TRUE(token.property_equals(
property, sourcemeta::core::JSON::Object::hash(property)));
}

TEST(at_property_with_hash) {
const auto document = sourcemeta::core::parse_json(R"JSON({
"foo": 1,
Expand Down
Loading