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
9 changes: 9 additions & 0 deletions include/json_lib.h
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,15 @@ int json_normalize(DYNAMIC_STRING *result,

int json_skip_array_and_count(json_engine_t *j, int* n_item);

/*
Compare two JSON string values semantically, taking Unicode escape
sequences into account. For example, "A" and "\u0041" are considered equal.
Returns 0 if the strings are equal, non-zero otherwise.
*/
int json_string_compare(CHARSET_INFO *cs,
const uchar *str1, int len1, int escaped1,
const uchar *str2, int len2, int escaped2);

inline static int json_scan_ended(json_engine_t *j)
{
return (j->state == JST_ARRAY_END && j->stack_p == 0);
Expand Down
129 changes: 129 additions & 0 deletions mysql-test/main/func_json_unicode_escape.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#
# MDEV-39995: JSON_CONTAINS and JSON_EQUALS do not compare strings
# based on semantic
#
#
# JSON string values with Unicode escape sequences should be treated
# as semantically equal to their literal equivalents.
# \u0041 is the Unicode escape for 'A'.
#
# JSON_CONTAINS: should return 1 for semantically equal strings
SELECT JSON_CONTAINS('"A"', '"\\u0041"');
JSON_CONTAINS('"A"', '"\\u0041"')
1
SELECT JSON_CONTAINS('"\\u0041"', '"A"');
JSON_CONTAINS('"\\u0041"', '"A"')
1
# JSON_OVERLAPS: should return 1 for semantically equal strings
SELECT JSON_OVERLAPS('"A"', '"\\u0041"');
JSON_OVERLAPS('"A"', '"\\u0041"')
1
# JSON_EQUALS: should return 1 for semantically equal strings
SELECT JSON_EQUALS('"A"', '"\\u0041"');
JSON_EQUALS('"A"', '"\\u0041"')
1
# JSON_UNQUOTE correctly resolves the escape (proving they are the same)
SELECT JSON_UNQUOTE('"A"') = JSON_UNQUOTE('"\\u0041"');
JSON_UNQUOTE('"A"') = JSON_UNQUOTE('"\\u0041"')
1
#
# Additional test from MDEV-39995 comment:
# Using hex literal that represents the bytes of '"\u0041"'
#
SELECT JSON_UNQUOTE('"A"');
JSON_UNQUOTE('"A"')
A
SELECT JSON_UNQUOTE(CAST(0x225C753030343122 AS CHAR));
JSON_UNQUOTE(CAST(0x225C753030343122 AS CHAR))
A
SELECT JSON_CONTAINS('"A"', CAST(0x225C753030343122 AS CHAR));
JSON_CONTAINS('"A"', CAST(0x225C753030343122 AS CHAR))
1
SELECT JSON_CONTAINS(JSON_QUOTE(JSON_UNQUOTE('"A"')),
JSON_QUOTE(JSON_UNQUOTE(CAST(0x225C753030343122 AS CHAR))));
JSON_CONTAINS(JSON_QUOTE(JSON_UNQUOTE('"A"')),
JSON_QUOTE(JSON_UNQUOTE(CAST(0x225C753030343122 AS CHAR))))
1
#
# More Unicode escape equivalences
#
# \u0048\u0065\u006C\u006C\u006F = "Hello"
SELECT JSON_CONTAINS('"Hello"', '"\\u0048\\u0065\\u006C\\u006C\\u006F"');
JSON_CONTAINS('"Hello"', '"\\u0048\\u0065\\u006C\\u006C\\u006F"')
1
SELECT JSON_EQUALS('"Hello"', '"\\u0048\\u0065\\u006C\\u006C\\u006F"');
JSON_EQUALS('"Hello"', '"\\u0048\\u0065\\u006C\\u006C\\u006F"')
1
SELECT JSON_OVERLAPS('"Hello"', '"\\u0048\\u0065\\u006C\\u006C\\u006F"');
JSON_OVERLAPS('"Hello"', '"\\u0048\\u0065\\u006C\\u006C\\u006F"')
1
# Mixed literal and escape in the same string: "H\u0065llo" = "Hello"
SELECT JSON_EQUALS('"Hello"', '"H\\u0065llo"');
JSON_EQUALS('"Hello"', '"H\\u0065llo"')
1
#
# Test within arrays and objects
#
SELECT JSON_CONTAINS('["A", "B"]', '["\\u0041"]');
JSON_CONTAINS('["A", "B"]', '["\\u0041"]')
1
SELECT JSON_CONTAINS('{"key": "A"}', '{"key": "\\u0041"}');
JSON_CONTAINS('{"key": "A"}', '{"key": "\\u0041"}')
1
SELECT JSON_EQUALS('["A", "B"]', '["\\u0041", "\\u0042"]');
JSON_EQUALS('["A", "B"]', '["\\u0041", "\\u0042"]')
1
SELECT JSON_EQUALS('{"key": "A"}', '{"key": "\\u0041"}');
JSON_EQUALS('{"key": "A"}', '{"key": "\\u0041"}')
1
#
# Surrogate pairs: characters above U+FFFF encoded as two \uXXXX escapes.
# U+1F600 (😀) = \uD83D\uDE00
# U+1F60A (😊) = \uD83D\uDE0A
#
SET NAMES utf8mb4;
SELECT JSON_EQUALS('"😀"', '"\\uD83D\\uDE00"');
JSON_EQUALS('"?"', '"\\uD83D\\uDE00"')
1
SELECT JSON_CONTAINS('"😀"', '"\\uD83D\\uDE00"');
JSON_CONTAINS('"?"', '"\\uD83D\\uDE00"')
1
SELECT JSON_OVERLAPS('"😀"', '"\\uD83D\\uDE00"');
JSON_OVERLAPS('"?"', '"\\uD83D\\uDE00"')
1
SELECT JSON_EQUALS('"😊"', '"\\uD83D\\uDE0A"');
JSON_EQUALS('"?"', '"\\uD83D\\uDE0A"')
1
SELECT JSON_CONTAINS('["😀", "hello"]', '["\\uD83D\\uDE00"]');
JSON_CONTAINS('["?", "hello"]', '["\\uD83D\\uDE00"]')
1
SELECT JSON_EQUALS('{"emoji": "😀"}', '{"emoji": "\\uD83D\\uDE00"}');
JSON_EQUALS('{"emoji": "?"}', '{"emoji": "\\uD83D\\uDE00"}')
1
#
# Escaped object keys: \u006B\u0065\u0079 = "key"
#
SELECT JSON_EQUALS('{"key":"A"}', '{"\\u006B\\u0065\\u0079":"A"}');
JSON_EQUALS('{"key":"A"}', '{"\\u006B\\u0065\\u0079":"A"}')
1
SELECT JSON_CONTAINS('{"key":"A"}', '{"\\u006B\\u0065\\u0079":"A"}');
JSON_CONTAINS('{"key":"A"}', '{"\\u006B\\u0065\\u0079":"A"}')
1
#
# BMP non-ASCII: é = U+00E9, literal UTF-8 vs escape
#
SELECT JSON_EQUALS('"é"', '"\\u00E9"');
JSON_EQUALS('"é"', '"\\u00E9"')
1
SELECT JSON_CONTAINS('"é"', '"\\u00E9"');
JSON_CONTAINS('"é"', '"\\u00E9"')
1
SELECT JSON_OVERLAPS('["é"]', '["\\u00E9"]');
JSON_OVERLAPS('["é"]', '["\\u00E9"]')
1
#
# CJK: 中 = U+4E2D
#
SELECT JSON_EQUALS('"中"', '"\\u4E2D"');
JSON_EQUALS('"中"', '"\\u4E2D"')
1
83 changes: 83 additions & 0 deletions mysql-test/main/func_json_unicode_escape.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
--echo #
--echo # MDEV-39995: JSON_CONTAINS and JSON_EQUALS do not compare strings
--echo # based on semantic
--echo #

--echo #
--echo # JSON string values with Unicode escape sequences should be treated
--echo # as semantically equal to their literal equivalents.
--echo # \u0041 is the Unicode escape for 'A'.
--echo #

--echo # JSON_CONTAINS: should return 1 for semantically equal strings
SELECT JSON_CONTAINS('"A"', '"\\u0041"');
SELECT JSON_CONTAINS('"\\u0041"', '"A"');

--echo # JSON_OVERLAPS: should return 1 for semantically equal strings
SELECT JSON_OVERLAPS('"A"', '"\\u0041"');

--echo # JSON_EQUALS: should return 1 for semantically equal strings
SELECT JSON_EQUALS('"A"', '"\\u0041"');

--echo # JSON_UNQUOTE correctly resolves the escape (proving they are the same)
SELECT JSON_UNQUOTE('"A"') = JSON_UNQUOTE('"\\u0041"');

--echo #
--echo # Additional test from MDEV-39995 comment:
--echo # Using hex literal that represents the bytes of '"\u0041"'
--echo #
SELECT JSON_UNQUOTE('"A"');
SELECT JSON_UNQUOTE(CAST(0x225C753030343122 AS CHAR));
SELECT JSON_CONTAINS('"A"', CAST(0x225C753030343122 AS CHAR));
SELECT JSON_CONTAINS(JSON_QUOTE(JSON_UNQUOTE('"A"')),
JSON_QUOTE(JSON_UNQUOTE(CAST(0x225C753030343122 AS CHAR))));

--echo #
--echo # More Unicode escape equivalences
--echo #
--echo # \u0048\u0065\u006C\u006C\u006F = "Hello"
SELECT JSON_CONTAINS('"Hello"', '"\\u0048\\u0065\\u006C\\u006C\\u006F"');
SELECT JSON_EQUALS('"Hello"', '"\\u0048\\u0065\\u006C\\u006C\\u006F"');
SELECT JSON_OVERLAPS('"Hello"', '"\\u0048\\u0065\\u006C\\u006C\\u006F"');

--echo # Mixed literal and escape in the same string: "H\u0065llo" = "Hello"
SELECT JSON_EQUALS('"Hello"', '"H\\u0065llo"');

--echo #
--echo # Test within arrays and objects
--echo #
SELECT JSON_CONTAINS('["A", "B"]', '["\\u0041"]');
SELECT JSON_CONTAINS('{"key": "A"}', '{"key": "\\u0041"}');
SELECT JSON_EQUALS('["A", "B"]', '["\\u0041", "\\u0042"]');
SELECT JSON_EQUALS('{"key": "A"}', '{"key": "\\u0041"}');

--echo #
--echo # Surrogate pairs: characters above U+FFFF encoded as two \uXXXX escapes.
--echo # U+1F600 (😀) = \uD83D\uDE00
--echo # U+1F60A (😊) = \uD83D\uDE0A
--echo #
SET NAMES utf8mb4;
SELECT JSON_EQUALS('"😀"', '"\\uD83D\\uDE00"');
SELECT JSON_CONTAINS('"😀"', '"\\uD83D\\uDE00"');
SELECT JSON_OVERLAPS('"😀"', '"\\uD83D\\uDE00"');
SELECT JSON_EQUALS('"😊"', '"\\uD83D\\uDE0A"');
SELECT JSON_CONTAINS('["😀", "hello"]', '["\\uD83D\\uDE00"]');
SELECT JSON_EQUALS('{"emoji": "😀"}', '{"emoji": "\\uD83D\\uDE00"}');

--echo #
--echo # Escaped object keys: \u006B\u0065\u0079 = "key"
--echo #
SELECT JSON_EQUALS('{"key":"A"}', '{"\\u006B\\u0065\\u0079":"A"}');
SELECT JSON_CONTAINS('{"key":"A"}', '{"\\u006B\\u0065\\u0079":"A"}');

--echo #
--echo # BMP non-ASCII: é = U+00E9, literal UTF-8 vs escape
--echo #
SELECT JSON_EQUALS('"é"', '"\\u00E9"');
SELECT JSON_CONTAINS('"é"', '"\\u00E9"');
SELECT JSON_OVERLAPS('["é"]', '["\\u00E9"]');

--echo #
--echo # CJK: 中 = U+4E2D
--echo #
SELECT JSON_EQUALS('"中"', '"\\u4E2D"');
16 changes: 8 additions & 8 deletions sql/item_jsonfunc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1704,12 +1704,10 @@ int Item_func_json_contains::check_contains(json_engine_t *js,
{
return FALSE;
}
/*
TODO: make proper json-json comparison here that takes excipient
into account.
*/
return value->value_len == js->value_len &&
memcmp(value->value, js->value, value->value_len) == 0;
return json_string_compare(js->s.cs,
js->value, js->value_len, js->value_escaped,
value->value, value->value_len,
value->value_escaped) == 0;
case JSON_VALUE_NUMBER:
if (value->value_type == JSON_VALUE_NUMBER)
{
Expand Down Expand Up @@ -5044,8 +5042,10 @@ static bool json_find_overlap_with_scalar(json_engine_t *js, json_engine_t *valu
}
else if (js->value_type == JSON_VALUE_STRING)
{
return value->value_len == js->value_len &&
memcmp(value->value, js->value, value->value_len) == 0;
return json_string_compare(js->s.cs,
js->value, js->value_len, js->value_escaped,
value->value, value->value_len,
value->value_escaped) == 0;
}
}
return value->value_type == js->value_type;
Expand Down
37 changes: 37 additions & 0 deletions strings/json_lib.c
Original file line number Diff line number Diff line change
Expand Up @@ -1818,6 +1818,43 @@ int json_unescape(CHARSET_INFO *json_cs,
}


/*
Compare two JSON string values semantically, resolving escape sequences.
If neither string has escapes, falls back to memcmp for speed.
Returns 0 if strings are equal, non-zero otherwise.
*/
int json_string_compare(CHARSET_INFO *cs,
const uchar *str1, int len1, int escaped1,
const uchar *str2, int len2, int escaped2)
{
json_string_t s1, s2;
int r1;

if (!escaped1 && !escaped2)
{
if (len1 != len2)
return 1;
return memcmp(str1, str2, len1);
}

json_string_setup(&s1, cs, str1, str1 + len1);
json_string_setup(&s2, cs, str2, str2 + len2);

for (r1= json_read_string_const_chr(&s1); r1 == 0;
r1= json_read_string_const_chr(&s1))
{
int r2= json_read_string_const_chr(&s2);
if (r2)
return 1;
if (s1.c_next != s2.c_next)
return 1;
}

return (json_read_string_const_chr(&s2) != 0 &&
s1.error == JE_EOS && s2.error == JE_EOS) ? 0 : 1;
}


/* When we need to replace a character with the escaping. */
enum json_esc_char_classes {
ESC_= 0, /* No need to escape. */
Expand Down
Loading