From cd929bf1dc3ae5d5685af0adf509fd40e005401b Mon Sep 17 00:00:00 2001 From: Varun Deep Saini Date: Tue, 21 Apr 2026 15:59:28 +0530 Subject: [PATCH] MDEV-30124: Add format validation for JSON Schema JSON_SCHEMA_VALID() now validates the format keyword per JSON Schema Draft 2020-12 when the new session variable json_schema_format_validation is ON. By default it stays an annotation, matching the previous behaviour. The 18 Draft 2020-12 formats are supported: date-time, date, time, duration, email, idn-email, hostname, idn-hostname, ipv4, ipv6, uri, uri-reference, iri, iri-reference, uuid, json-pointer, relative-json-pointer, regex. Each format has a small syntactic validator, a private method of Json_schema_format. IP addresses are parsed with inet_pton, regex with pcre2_compile, and the rest with hand-written checks that follow the relevant RFC grammar. The format name is resolved through Json_schema_format::format_hash, built once at server startup (mirrors the existing all_keywords_hash used for the other schema keywords). Each hash entry carries a member pointer to the format's validator, set on Json_schema_format at schema parse time, so validate() dispatches with a single indirect call per document. An unknown format name raises ER_JSON_INVALID_VALUE_FOR_KEYWORD at schema parse time when json_schema_format_validation is ON (2020-12 7.2.2: assertion mode must refuse unknown formats). When OFF it stays a legal annotation (7.2.1), preserving the previous behaviour. --- mysql-test/main/func_json.result | 289 +++++++++- mysql-test/main/func_json.test | 151 ++++- mysql-test/main/mysqld--help.result | 5 + .../sys_vars/r/sysvars_server_embedded.result | 10 + .../r/sysvars_server_notembedded.result | 10 + sql/json_schema.cc | 530 +++++++++++++++++- sql/json_schema.h | 46 +- sql/mysqld.cc | 2 + sql/sql_class.h | 1 + sql/sys_vars.cc | 7 + 10 files changed, 1025 insertions(+), 26 deletions(-) diff --git a/mysql-test/main/func_json.result b/mysql-test/main/func_json.result index 900af17c0670b..e144bc8edb491 100644 --- a/mysql-test/main/func_json.result +++ b/mysql-test/main/func_json.result @@ -3107,7 +3107,7 @@ SELECT JSON_SCHEMA_VALID(@schema_number, '2'); JSON_SCHEMA_VALID(@schema_number, '2') 1 # Checking string -# checking format keyword. (not validating for now) +# checking format keyword (annotation only, validation disabled by default) SET @schema_string= '{ "type": "string", "format":"date-time" @@ -3234,6 +3234,293 @@ SET @schema_string= '{ SELECT JSON_SCHEMA_VALID(@schema_string, '"not_regex"'); JSON_SCHEMA_VALID(@schema_string, '"not_regex"') 1 +# Format validation when json_schema_format_validation is enabled (MDEV-30124) +SET @save_json_schema_format_validation= @@json_schema_format_validation; +SET json_schema_format_validation= ON; +# format: date-time +SET @schema= '{"type":"string","format":"date-time"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"2024-01-15T10:30:00Z"'); +JSON_SCHEMA_VALID(@schema, '"2024-01-15T10:30:00Z"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"2024-01-15T10:30:00+05:30"'); +JSON_SCHEMA_VALID(@schema, '"2024-01-15T10:30:00+05:30"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"2024-01-15T10:30:00.123Z"'); +JSON_SCHEMA_VALID(@schema, '"2024-01-15T10:30:00.123Z"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"not_date-time"'); +JSON_SCHEMA_VALID(@schema, '"not_date-time"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"2024-13-01T10:30:00Z"'); +JSON_SCHEMA_VALID(@schema, '"2024-13-01T10:30:00Z"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"2024-01-15 10:30:00Z"'); +JSON_SCHEMA_VALID(@schema, '"2024-01-15 10:30:00Z"') +0 +# format: date +SET @schema= '{"type":"string","format":"date"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"2024-01-15"'); +JSON_SCHEMA_VALID(@schema, '"2024-01-15"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"2024-02-29"'); +JSON_SCHEMA_VALID(@schema, '"2024-02-29"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"2023-02-29"'); +JSON_SCHEMA_VALID(@schema, '"2023-02-29"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"not_date"'); +JSON_SCHEMA_VALID(@schema, '"not_date"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"2024-13-01"'); +JSON_SCHEMA_VALID(@schema, '"2024-13-01"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"2024-00-15"'); +JSON_SCHEMA_VALID(@schema, '"2024-00-15"') +0 +# format: time +SET @schema= '{"type":"string","format":"time"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"10:30:00Z"'); +JSON_SCHEMA_VALID(@schema, '"10:30:00Z"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"23:59:59+05:30"'); +JSON_SCHEMA_VALID(@schema, '"23:59:59+05:30"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"10:30:00.123Z"'); +JSON_SCHEMA_VALID(@schema, '"10:30:00.123Z"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"not_time"'); +JSON_SCHEMA_VALID(@schema, '"not_time"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"25:00:00Z"'); +JSON_SCHEMA_VALID(@schema, '"25:00:00Z"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"10:30:00"'); +JSON_SCHEMA_VALID(@schema, '"10:30:00"') +0 +# format: duration +SET @schema= '{"type":"string","format":"duration"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"P1Y2M3D"'); +JSON_SCHEMA_VALID(@schema, '"P1Y2M3D"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"PT1H30M"'); +JSON_SCHEMA_VALID(@schema, '"PT1H30M"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"P1Y2M3DT4H5M6S"'); +JSON_SCHEMA_VALID(@schema, '"P1Y2M3DT4H5M6S"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"PT0.5S"'); +JSON_SCHEMA_VALID(@schema, '"PT0.5S"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"not_duration"'); +JSON_SCHEMA_VALID(@schema, '"not_duration"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"P"'); +JSON_SCHEMA_VALID(@schema, '"P"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"PT"'); +JSON_SCHEMA_VALID(@schema, '"PT"') +0 +# format: email +SET @schema= '{"type":"string","format":"email"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"user@example.com"'); +JSON_SCHEMA_VALID(@schema, '"user@example.com"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"a@b.c"'); +JSON_SCHEMA_VALID(@schema, '"a@b.c"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"not_email"'); +JSON_SCHEMA_VALID(@schema, '"not_email"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"@example.com"'); +JSON_SCHEMA_VALID(@schema, '"@example.com"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"user@"'); +JSON_SCHEMA_VALID(@schema, '"user@"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"user@@example.com"'); +JSON_SCHEMA_VALID(@schema, '"user@@example.com"') +0 +# format: idn-email +SET @schema= '{"type":"string","format":"idn-email"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"user@example.com"'); +JSON_SCHEMA_VALID(@schema, '"user@example.com"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"not_email"'); +JSON_SCHEMA_VALID(@schema, '"not_email"') +0 +# format: hostname +SET @schema= '{"type":"string","format":"hostname"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"example.com"'); +JSON_SCHEMA_VALID(@schema, '"example.com"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"sub.example.com"'); +JSON_SCHEMA_VALID(@schema, '"sub.example.com"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"-invalid.com"'); +JSON_SCHEMA_VALID(@schema, '"-invalid.com"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"invalid-.com"'); +JSON_SCHEMA_VALID(@schema, '"invalid-.com"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '""'); +JSON_SCHEMA_VALID(@schema, '""') +0 +# format: idn-hostname +SET @schema= '{"type":"string","format":"idn-hostname"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"example.com"'); +JSON_SCHEMA_VALID(@schema, '"example.com"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"-invalid.com"'); +JSON_SCHEMA_VALID(@schema, '"-invalid.com"') +0 +# format: ipv4 +SET @schema= '{"type":"string","format":"ipv4"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"192.168.1.1"'); +JSON_SCHEMA_VALID(@schema, '"192.168.1.1"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"0.0.0.0"'); +JSON_SCHEMA_VALID(@schema, '"0.0.0.0"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"255.255.255.255"'); +JSON_SCHEMA_VALID(@schema, '"255.255.255.255"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"not_ipv4"'); +JSON_SCHEMA_VALID(@schema, '"not_ipv4"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"256.1.1.1"'); +JSON_SCHEMA_VALID(@schema, '"256.1.1.1"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"1.2.3"'); +JSON_SCHEMA_VALID(@schema, '"1.2.3"') +0 +# format: ipv6 +SET @schema= '{"type":"string","format":"ipv6"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"::1"'); +JSON_SCHEMA_VALID(@schema, '"::1"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"2001:0db8:85a3:0000:0000:8a2e:0370:7334"'); +JSON_SCHEMA_VALID(@schema, '"2001:0db8:85a3:0000:0000:8a2e:0370:7334"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"not_ipv6"'); +JSON_SCHEMA_VALID(@schema, '"not_ipv6"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"12345::1"'); +JSON_SCHEMA_VALID(@schema, '"12345::1"') +0 +# format: uri +SET @schema= '{"type":"string","format":"uri"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"http://example.com"'); +JSON_SCHEMA_VALID(@schema, '"http://example.com"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"https://example.com/path?q=1#frag"'); +JSON_SCHEMA_VALID(@schema, '"https://example.com/path?q=1#frag"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"ftp://files.example.com"'); +JSON_SCHEMA_VALID(@schema, '"ftp://files.example.com"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"not a uri"'); +JSON_SCHEMA_VALID(@schema, '"not a uri"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"/relative/path"'); +JSON_SCHEMA_VALID(@schema, '"/relative/path"') +0 +# format: uri-reference +SET @schema= '{"type":"string","format":"uri-reference"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"http://example.com"'); +JSON_SCHEMA_VALID(@schema, '"http://example.com"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"/relative/path"'); +JSON_SCHEMA_VALID(@schema, '"/relative/path"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '""'); +JSON_SCHEMA_VALID(@schema, '""') +1 +# format: uuid +SET @schema= '{"type":"string","format":"uuid"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"550e8400-e29b-41d4-a716-446655440000"'); +JSON_SCHEMA_VALID(@schema, '"550e8400-e29b-41d4-a716-446655440000"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"not-a-uuid"'); +JSON_SCHEMA_VALID(@schema, '"not-a-uuid"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"550e8400-e29b-41d4-a716-44665544000g"'); +JSON_SCHEMA_VALID(@schema, '"550e8400-e29b-41d4-a716-44665544000g"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"550e8400e29b41d4a716446655440000"'); +JSON_SCHEMA_VALID(@schema, '"550e8400e29b41d4a716446655440000"') +0 +# format: json-pointer +SET @schema= '{"type":"string","format":"json-pointer"}'; +SELECT JSON_SCHEMA_VALID(@schema, '""'); +JSON_SCHEMA_VALID(@schema, '""') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"/foo/bar"'); +JSON_SCHEMA_VALID(@schema, '"/foo/bar"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"/foo/~0/~1"'); +JSON_SCHEMA_VALID(@schema, '"/foo/~0/~1"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"foo"'); +JSON_SCHEMA_VALID(@schema, '"foo"') +0 +SELECT JSON_SCHEMA_VALID(@schema, '"/foo/~2"'); +JSON_SCHEMA_VALID(@schema, '"/foo/~2"') +0 +# format: relative-json-pointer +SET @schema= '{"type":"string","format":"relative-json-pointer"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"0"'); +JSON_SCHEMA_VALID(@schema, '"0"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"1/foo"'); +JSON_SCHEMA_VALID(@schema, '"1/foo"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"0#"'); +JSON_SCHEMA_VALID(@schema, '"0#"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"/invalid"'); +JSON_SCHEMA_VALID(@schema, '"/invalid"') +0 +# format: regex +SET @schema= '{"type":"string","format":"regex"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"^[a-z]+$"'); +JSON_SCHEMA_VALID(@schema, '"^[a-z]+$"') +1 +SELECT JSON_SCHEMA_VALID(@schema, '"[invalid"'); +JSON_SCHEMA_VALID(@schema, '"[invalid"') +0 +# format only applies to strings, non-string values always pass +SET @schema= '{"format":"email"}'; +SELECT JSON_SCHEMA_VALID(@schema, '42'); +JSON_SCHEMA_VALID(@schema, '42') +1 +SELECT JSON_SCHEMA_VALID(@schema, 'true'); +JSON_SCHEMA_VALID(@schema, 'true') +1 +SELECT JSON_SCHEMA_VALID(@schema, 'null'); +JSON_SCHEMA_VALID(@schema, 'null') +1 +SELECT JSON_SCHEMA_VALID(@schema, '[1,2]'); +JSON_SCHEMA_VALID(@schema, '[1,2]') +1 +SELECT JSON_SCHEMA_VALID(@schema, '{"a":1}'); +JSON_SCHEMA_VALID(@schema, '{"a":1}') +1 +# unknown format values are rejected when validation is enabled +SET @schema= '{"type":"string","format":"unknown-format"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"anything"'); +ERROR HY000: Invalid value for keyword format +# verify annotation-only when disabled +SET json_schema_format_validation= OFF; +SET @schema= '{"type":"string","format":"email"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"not_email"'); +JSON_SCHEMA_VALID(@schema, '"not_email"') +1 +# unknown format values stay legal annotations when disabled +SET @schema= '{"type":"string","format":"unknown-format"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"anything"'); +JSON_SCHEMA_VALID(@schema, '"anything"') +1 +SET @@json_schema_format_validation= @save_json_schema_format_validation; # Validating other string keywords SET @schema_string= '{ "type": "string", diff --git a/mysql-test/main/func_json.test b/mysql-test/main/func_json.test index 39556f55c561e..0f53d9fbf3c00 100644 --- a/mysql-test/main/func_json.test +++ b/mysql-test/main/func_json.test @@ -2235,7 +2235,7 @@ SELECT JSON_SCHEMA_VALID(@schema_number, '2'); --echo # Checking string ---echo # checking format keyword. (not validating for now) +--echo # checking format keyword (annotation only, validation disabled by default) SET @schema_string= '{ "type": "string", @@ -2328,6 +2328,155 @@ SET @schema_string= '{ }'; SELECT JSON_SCHEMA_VALID(@schema_string, '"not_regex"'); +--echo # Format validation when json_schema_format_validation is enabled (MDEV-30124) + +SET @save_json_schema_format_validation= @@json_schema_format_validation; +SET json_schema_format_validation= ON; + +--echo # format: date-time +SET @schema= '{"type":"string","format":"date-time"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"2024-01-15T10:30:00Z"'); +SELECT JSON_SCHEMA_VALID(@schema, '"2024-01-15T10:30:00+05:30"'); +SELECT JSON_SCHEMA_VALID(@schema, '"2024-01-15T10:30:00.123Z"'); +SELECT JSON_SCHEMA_VALID(@schema, '"not_date-time"'); +SELECT JSON_SCHEMA_VALID(@schema, '"2024-13-01T10:30:00Z"'); +SELECT JSON_SCHEMA_VALID(@schema, '"2024-01-15 10:30:00Z"'); + +--echo # format: date +SET @schema= '{"type":"string","format":"date"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"2024-01-15"'); +SELECT JSON_SCHEMA_VALID(@schema, '"2024-02-29"'); +SELECT JSON_SCHEMA_VALID(@schema, '"2023-02-29"'); +SELECT JSON_SCHEMA_VALID(@schema, '"not_date"'); +SELECT JSON_SCHEMA_VALID(@schema, '"2024-13-01"'); +SELECT JSON_SCHEMA_VALID(@schema, '"2024-00-15"'); + +--echo # format: time +SET @schema= '{"type":"string","format":"time"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"10:30:00Z"'); +SELECT JSON_SCHEMA_VALID(@schema, '"23:59:59+05:30"'); +SELECT JSON_SCHEMA_VALID(@schema, '"10:30:00.123Z"'); +SELECT JSON_SCHEMA_VALID(@schema, '"not_time"'); +SELECT JSON_SCHEMA_VALID(@schema, '"25:00:00Z"'); +SELECT JSON_SCHEMA_VALID(@schema, '"10:30:00"'); + +--echo # format: duration +SET @schema= '{"type":"string","format":"duration"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"P1Y2M3D"'); +SELECT JSON_SCHEMA_VALID(@schema, '"PT1H30M"'); +SELECT JSON_SCHEMA_VALID(@schema, '"P1Y2M3DT4H5M6S"'); +SELECT JSON_SCHEMA_VALID(@schema, '"PT0.5S"'); +SELECT JSON_SCHEMA_VALID(@schema, '"not_duration"'); +SELECT JSON_SCHEMA_VALID(@schema, '"P"'); +SELECT JSON_SCHEMA_VALID(@schema, '"PT"'); + +--echo # format: email +SET @schema= '{"type":"string","format":"email"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"user@example.com"'); +SELECT JSON_SCHEMA_VALID(@schema, '"a@b.c"'); +SELECT JSON_SCHEMA_VALID(@schema, '"not_email"'); +SELECT JSON_SCHEMA_VALID(@schema, '"@example.com"'); +SELECT JSON_SCHEMA_VALID(@schema, '"user@"'); +SELECT JSON_SCHEMA_VALID(@schema, '"user@@example.com"'); + +--echo # format: idn-email +SET @schema= '{"type":"string","format":"idn-email"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"user@example.com"'); +SELECT JSON_SCHEMA_VALID(@schema, '"not_email"'); + +--echo # format: hostname +SET @schema= '{"type":"string","format":"hostname"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"example.com"'); +SELECT JSON_SCHEMA_VALID(@schema, '"sub.example.com"'); +SELECT JSON_SCHEMA_VALID(@schema, '"-invalid.com"'); +SELECT JSON_SCHEMA_VALID(@schema, '"invalid-.com"'); +SELECT JSON_SCHEMA_VALID(@schema, '""'); + +--echo # format: idn-hostname +SET @schema= '{"type":"string","format":"idn-hostname"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"example.com"'); +SELECT JSON_SCHEMA_VALID(@schema, '"-invalid.com"'); + +--echo # format: ipv4 +SET @schema= '{"type":"string","format":"ipv4"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"192.168.1.1"'); +SELECT JSON_SCHEMA_VALID(@schema, '"0.0.0.0"'); +SELECT JSON_SCHEMA_VALID(@schema, '"255.255.255.255"'); +SELECT JSON_SCHEMA_VALID(@schema, '"not_ipv4"'); +SELECT JSON_SCHEMA_VALID(@schema, '"256.1.1.1"'); +SELECT JSON_SCHEMA_VALID(@schema, '"1.2.3"'); + +--echo # format: ipv6 +SET @schema= '{"type":"string","format":"ipv6"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"::1"'); +SELECT JSON_SCHEMA_VALID(@schema, '"2001:0db8:85a3:0000:0000:8a2e:0370:7334"'); +SELECT JSON_SCHEMA_VALID(@schema, '"not_ipv6"'); +SELECT JSON_SCHEMA_VALID(@schema, '"12345::1"'); + +--echo # format: uri +SET @schema= '{"type":"string","format":"uri"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"http://example.com"'); +SELECT JSON_SCHEMA_VALID(@schema, '"https://example.com/path?q=1#frag"'); +SELECT JSON_SCHEMA_VALID(@schema, '"ftp://files.example.com"'); +SELECT JSON_SCHEMA_VALID(@schema, '"not a uri"'); +SELECT JSON_SCHEMA_VALID(@schema, '"/relative/path"'); + +--echo # format: uri-reference +SET @schema= '{"type":"string","format":"uri-reference"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"http://example.com"'); +SELECT JSON_SCHEMA_VALID(@schema, '"/relative/path"'); +SELECT JSON_SCHEMA_VALID(@schema, '""'); + +--echo # format: uuid +SET @schema= '{"type":"string","format":"uuid"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"550e8400-e29b-41d4-a716-446655440000"'); +SELECT JSON_SCHEMA_VALID(@schema, '"not-a-uuid"'); +SELECT JSON_SCHEMA_VALID(@schema, '"550e8400-e29b-41d4-a716-44665544000g"'); +SELECT JSON_SCHEMA_VALID(@schema, '"550e8400e29b41d4a716446655440000"'); + +--echo # format: json-pointer +SET @schema= '{"type":"string","format":"json-pointer"}'; +SELECT JSON_SCHEMA_VALID(@schema, '""'); +SELECT JSON_SCHEMA_VALID(@schema, '"/foo/bar"'); +SELECT JSON_SCHEMA_VALID(@schema, '"/foo/~0/~1"'); +SELECT JSON_SCHEMA_VALID(@schema, '"foo"'); +SELECT JSON_SCHEMA_VALID(@schema, '"/foo/~2"'); + +--echo # format: relative-json-pointer +SET @schema= '{"type":"string","format":"relative-json-pointer"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"0"'); +SELECT JSON_SCHEMA_VALID(@schema, '"1/foo"'); +SELECT JSON_SCHEMA_VALID(@schema, '"0#"'); +SELECT JSON_SCHEMA_VALID(@schema, '"/invalid"'); + +--echo # format: regex +SET @schema= '{"type":"string","format":"regex"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"^[a-z]+$"'); +SELECT JSON_SCHEMA_VALID(@schema, '"[invalid"'); + +--echo # format only applies to strings, non-string values always pass +SET @schema= '{"format":"email"}'; +SELECT JSON_SCHEMA_VALID(@schema, '42'); +SELECT JSON_SCHEMA_VALID(@schema, 'true'); +SELECT JSON_SCHEMA_VALID(@schema, 'null'); +SELECT JSON_SCHEMA_VALID(@schema, '[1,2]'); +SELECT JSON_SCHEMA_VALID(@schema, '{"a":1}'); + +--echo # unknown format values are rejected when validation is enabled +SET @schema= '{"type":"string","format":"unknown-format"}'; +--error ER_JSON_INVALID_VALUE_FOR_KEYWORD +SELECT JSON_SCHEMA_VALID(@schema, '"anything"'); + +--echo # verify annotation-only when disabled +SET json_schema_format_validation= OFF; +SET @schema= '{"type":"string","format":"email"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"not_email"'); +--echo # unknown format values stay legal annotations when disabled +SET @schema= '{"type":"string","format":"unknown-format"}'; +SELECT JSON_SCHEMA_VALID(@schema, '"anything"'); + +SET @@json_schema_format_validation= @save_json_schema_format_validation; + --echo # Validating other string keywords SET @schema_string= '{ diff --git a/mysql-test/main/mysqld--help.result b/mysql-test/main/mysqld--help.result index 82a85e0faae6c..89f6921b3ff43 100644 --- a/mysql-test/main/mysqld--help.result +++ b/mysql-test/main/mysqld--help.result @@ -481,6 +481,10 @@ The following specify which files/extra groups are read (specified before remain Controls what join operations can be executed with join buffers. Odd numbers are used for plain join buffers while even numbers are used for linked buffers + --json-schema-format-validation + Enable format validation for JSON_SCHEMA_VALID(). When + OFF (default), the format keyword is treated as an + annotation only --keep-files-on-create Don't overwrite stale .MYD and .MYI even if no directory is specified @@ -1852,6 +1856,7 @@ interactive-timeout 28800 join-buffer-size 262144 join-buffer-space-limit 2097152 join-cache-level 2 +json-schema-format-validation FALSE keep-files-on-create FALSE key-buffer-size 134217728 key-cache-age-threshold 300 diff --git a/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result b/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result index 900217485ef99..c5e7f95cc3523 100644 --- a/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result +++ b/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result @@ -1492,6 +1492,16 @@ NUMERIC_BLOCK_SIZE 1 ENUM_VALUE_LIST NULL READ_ONLY NO COMMAND_LINE_ARGUMENT REQUIRED +VARIABLE_NAME JSON_SCHEMA_FORMAT_VALIDATION +VARIABLE_SCOPE SESSION +VARIABLE_TYPE BOOLEAN +VARIABLE_COMMENT Enable format validation for JSON_SCHEMA_VALID(). When OFF (default), the format keyword is treated as an annotation only +NUMERIC_MIN_VALUE NULL +NUMERIC_MAX_VALUE NULL +NUMERIC_BLOCK_SIZE NULL +ENUM_VALUE_LIST OFF,ON +READ_ONLY NO +COMMAND_LINE_ARGUMENT OPTIONAL VARIABLE_NAME KEEP_FILES_ON_CREATE VARIABLE_SCOPE SESSION VARIABLE_TYPE BOOLEAN diff --git a/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result b/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result index 87972c6c8776d..1cec505133aed 100644 --- a/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result +++ b/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result @@ -1672,6 +1672,16 @@ NUMERIC_BLOCK_SIZE 1 ENUM_VALUE_LIST NULL READ_ONLY NO COMMAND_LINE_ARGUMENT REQUIRED +VARIABLE_NAME JSON_SCHEMA_FORMAT_VALIDATION +VARIABLE_SCOPE SESSION +VARIABLE_TYPE BOOLEAN +VARIABLE_COMMENT Enable format validation for JSON_SCHEMA_VALID(). When OFF (default), the format keyword is treated as an annotation only +NUMERIC_MIN_VALUE NULL +NUMERIC_MAX_VALUE NULL +NUMERIC_BLOCK_SIZE NULL +ENUM_VALUE_LIST OFF,ON +READ_ONLY NO +COMMAND_LINE_ARGUMENT OPTIONAL VARIABLE_NAME KEEP_FILES_ON_CREATE VARIABLE_SCOPE SESSION VARIABLE_TYPE BOOLEAN diff --git a/sql/json_schema.cc b/sql/json_schema.cc index 9aa052ca26185..e4e749e4abfd8 100644 --- a/sql/json_schema.cc +++ b/sql/json_schema.cc @@ -20,7 +20,13 @@ #include #include "json_schema.h" #include "json_schema_helper.h" +#define PCRE2_STATIC 1 #include "pcre2.h" +#ifdef _WIN32 +#include +#else +#include +#endif #ifndef DBUG_OFF int dbug_json_check_min_stack_requirement(); @@ -28,6 +34,74 @@ int dbug_json_check_min_stack_requirement(); static HASH all_keywords_hash; +HASH Json_schema_format::format_hash; + +struct st_json_schema_format_map +{ + LEX_CSTRING name; + Json_schema_format::Format_validator validate_fn; +}; + +static const uchar *get_key_name_for_format(const void *key_name, + size_t *length, my_bool) +{ + auto curr_format= static_cast(key_name); + *length= curr_format->name.length; + return reinterpret_cast(curr_format->name.str); +} + +bool Json_schema_format::setup_format_hash() +{ + /* static: the hash stores pointers into this array, no copies. */ + static st_json_schema_format_map format_map[]= + { + { { STRING_WITH_LEN("date-time") }, + &Json_schema_format::validate_datetime }, + { { STRING_WITH_LEN("date") }, &Json_schema_format::validate_date }, + { { STRING_WITH_LEN("time") }, &Json_schema_format::validate_time_part }, + { { STRING_WITH_LEN("duration") }, + &Json_schema_format::validate_duration }, + { { STRING_WITH_LEN("email") }, &Json_schema_format::validate_email }, + { { STRING_WITH_LEN("idn-email") }, + &Json_schema_format::validate_idn_email }, + { { STRING_WITH_LEN("hostname") }, + &Json_schema_format::validate_hostname }, + { { STRING_WITH_LEN("idn-hostname") }, + &Json_schema_format::validate_idn_hostname }, + { { STRING_WITH_LEN("ipv4") }, &Json_schema_format::validate_ipv4 }, + { { STRING_WITH_LEN("ipv6") }, &Json_schema_format::validate_ipv6 }, + { { STRING_WITH_LEN("uri") }, &Json_schema_format::validate_uri }, + { { STRING_WITH_LEN("uri-reference") }, + &Json_schema_format::validate_uri_reference }, + { { STRING_WITH_LEN("iri") }, &Json_schema_format::validate_iri }, + { { STRING_WITH_LEN("iri-reference") }, + &Json_schema_format::validate_iri_reference }, + { { STRING_WITH_LEN("uuid") }, &Json_schema_format::validate_uuid }, + { { STRING_WITH_LEN("json-pointer") }, + &Json_schema_format::validate_json_pointer }, + { { STRING_WITH_LEN("relative-json-pointer") }, + &Json_schema_format::validate_relative_json_pointer }, + { { STRING_WITH_LEN("regex") }, &Json_schema_format::validate_regex } + }; + + if (my_hash_init(PSI_INSTRUMENT_ME, &format_hash, &my_charset_bin, + array_elements(format_map), 0, 0, + get_key_name_for_format, NULL, 0)) + return true; + + for (size_t i= 0; i < array_elements(format_map); i++) + { + if (my_hash_insert(&format_hash, (uchar*)(&format_map[i]))) + return true; + } + return false; +} + +void Json_schema_format::cleanup_format_hash() +{ + my_hash_free(&format_hash); +} + static Json_schema_keyword *create_json_schema_keyword(THD *thd) { return new (thd->mem_root) Json_schema_keyword(); @@ -259,24 +333,7 @@ static Json_schema_keyword *create_json_schema_reference(THD *thd) {{ STRING_WITH_LEN("default") }, create_json_schema_annotation, JSON_SCHEMA_ANNOTATION_KEYWORD}, {{ STRING_WITH_LEN("$vocabulary") }, create_json_schema_annotation, JSON_SCHEMA_ANNOTATION_KEYWORD}, - {{ STRING_WITH_LEN("date-time") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("date") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("time") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("duration") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("email") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("idn-email") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("hostname") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("idn-hostname") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("ipv4") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("ipv6") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("uri") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("uri-reference") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("iri") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("iri-reference") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("uuid") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("json-pointer") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("relative-json-pointer") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, - {{ STRING_WITH_LEN("regex") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, + {{ STRING_WITH_LEN("format") }, create_json_schema_format, JSON_SCHEMA_FORMAT_KEYWORD}, {{ STRING_WITH_LEN("contentMediaType") }, create_json_schema_media_string, JSON_SCHEMA_MEDIA_KEYWORD}, {{ STRING_WITH_LEN("conentEncoding") }, create_json_schema_media_string, JSON_SCHEMA_MEDIA_KEYWORD}, @@ -377,10 +434,447 @@ bool Json_schema_format::handle_keyword(THD *thd, if (je->value_type != JSON_VALUE_STRING) { my_error(ER_JSON_INVALID_VALUE_FOR_KEYWORD, MYF(0), "format"); + return true; + } + + const char *val= (const char *) je->value; + size_t len= je->value_len; + st_json_schema_format_map *curr_format= + (st_json_schema_format_map*) my_hash_search(&format_hash, + (const uchar*)val, len); + if (!curr_format) + { + /* + Unknown formats are legal annotations (JSON Schema 2020-12 7.2.1), + but when format validation is enabled they must be refused (7.2.2). + */ + if (thd->variables.json_schema_format_validation) + { + my_error(ER_JSON_INVALID_VALUE_FOR_KEYWORD, MYF(0), "format"); + return true; + } + return false; } + validator= curr_format->validate_fn; + return false; } +static inline bool is_digit(char c) +{ + return my_isdigit(&my_charset_latin1, c); +} + +static inline bool is_hex(char c) +{ + return my_isxdigit(&my_charset_latin1, c); +} + +static inline bool is_alpha(char c) +{ + return my_isalpha(&my_charset_latin1, c); +} + +static int parse_2digits(const char *s) +{ + if (!is_digit(s[0]) || !is_digit(s[1])) + return -1; + return (s[0] - '0') * 10 + (s[1] - '0'); +} + +static int parse_4digits(const char *s) +{ + if (!is_digit(s[0]) || !is_digit(s[1]) || + !is_digit(s[2]) || !is_digit(s[3])) + return -1; + return (s[0] - '0') * 1000 + (s[1] - '0') * 100 + + (s[2] - '0') * 10 + (s[3] - '0'); +} + +bool Json_schema_format::validate_date(const char *val, int len) const +{ + if (len != 10) return false; + if (val[4] != '-' || val[7] != '-') return false; + int year= parse_4digits(val); + int month= parse_2digits(val + 5); + int day= parse_2digits(val + 8); + if (year < 0 || month < 1 || month > 12) return false; + if (day < 1 || day > (int) calc_days_in_month(year, month)) return false; + return true; +} + +/* Parse time: HH:MM:SS[.frac](Z|+HH:MM|-HH:MM) */ +bool Json_schema_format::validate_time_part(const char *val, int len) const +{ + if (len < 9) return false; + if (val[2] != ':' || val[5] != ':') return false; + int hour= parse_2digits(val); + int min= parse_2digits(val + 3); + int sec= parse_2digits(val + 6); + if (hour < 0 || hour > 23 || min < 0 || min > 59 || sec < 0 || sec > 60) + return false; + int pos= 8; + if (pos < len && val[pos] == '.') + { + pos++; + if (pos >= len || !is_digit(val[pos])) return false; + while (pos < len && is_digit(val[pos])) pos++; + } + if (pos >= len) return false; + if (val[pos] == 'Z' || val[pos] == 'z') + { + pos++; + return pos == len; + } + if (val[pos] == '+' || val[pos] == '-') + { + pos++; + if (pos + 5 != len) return false; + if (val[pos + 2] != ':') return false; + int tz_h= parse_2digits(val + pos); + int tz_m= parse_2digits(val + pos + 3); + if (tz_h < 0 || tz_h > 23 || tz_m < 0 || tz_m > 59) return false; + return true; + } + return false; +} + +bool Json_schema_format::validate_datetime(const char *val, int len) const +{ + if (len < 20) return false; + if (val[10] != 'T' && val[10] != 't') return false; + if (!validate_date(val, 10)) return false; + return validate_time_part(val + 11, len - 11); +} + +bool Json_schema_format::validate_duration(const char *val, int len) const +{ + if (len < 2 || val[0] != 'P') return false; + int pos= 1; + bool has_time= false, has_any= false; + + while (pos < len && val[pos] != 'T') + { + if (!is_digit(val[pos])) return false; + while (pos < len && is_digit(val[pos])) pos++; + if (pos >= len) return false; + if (val[pos] == 'Y' || val[pos] == 'M' || + val[pos] == 'W' || val[pos] == 'D') + { + has_any= true; + pos++; + } + else + return false; + } + if (pos < len && val[pos] == 'T') + { + has_time= true; + pos++; + if (pos >= len) return false; + bool has_time_component= false; + while (pos < len) + { + if (!is_digit(val[pos])) return false; + while (pos < len && is_digit(val[pos])) pos++; + if (pos >= len) return false; + if (val[pos] == '.') + { + pos++; + if (pos >= len || !is_digit(val[pos])) return false; + while (pos < len && is_digit(val[pos])) pos++; + if (pos >= len || val[pos] != 'S') return false; + } + if (val[pos] == 'H' || val[pos] == 'M' || val[pos] == 'S') + { + has_time_component= true; + pos++; + } + else + return false; + } + if (!has_time_component) return false; + } + return pos == len && (has_any || has_time); +} + +/* Position of '@' separating Local-part from Domain (RFC 5321 4.1.2). */ +static int find_email_split(const char *val, int len) +{ + const char *at= (const char *) memchr(val, '@', len); + if (!at) return -1; + int at_pos= (int) (at - val); + + if (val[0] == '"') + { + int i= 1; + while (i < len && val[i] != '"') + { + if (val[i] == '\\' && i + 1 < len) i+= 2; + else i++; + } + if (i >= len || val[i] != '"') return -1; + i++; + if (i >= len || val[i] != '@') return -1; + at_pos= i; + } + else if (memchr(at + 1, '@', len - at_pos - 1)) + return -1; + return at_pos; +} + +/* 'email' format: RFC 5321 Mailbox (ASCII only, RFC 5321 4.1.2). */ +bool Json_schema_format::validate_email(const char *val, int len) const +{ + int at_pos= find_email_split(val, len); + if (at_pos < 1 || at_pos > 64) return false; + int domain_len= len - at_pos - 1; + if (domain_len < 1 || domain_len > 253) return false; + const char *domain= val + at_pos + 1; + bool has_dot= false; + for (int i= 0; i < domain_len; i++) + { + if (domain[i] == '.') + { + has_dot= true; + if (i == 0 || i == domain_len - 1) return false; + if (domain[i - 1] == '.') return false; + } + else if (!is_alpha(domain[i]) && !is_digit(domain[i]) && domain[i] != '-') + return false; + } + return has_dot; +} + +/* 'idn-email' format: RFC 6531 extended Mailbox (UTF-8 permitted). */ +bool Json_schema_format::validate_idn_email(const char *val, int len) const +{ + int at_pos= find_email_split(val, len); + if (at_pos < 1) return false; + int domain_len= len - at_pos - 1; + if (domain_len < 1) return false; + const char *domain= val + at_pos + 1; + bool has_dot= false; + for (int i= 0; i < domain_len; i++) + { + if (domain[i] == '.') + { + has_dot= true; + if (i == 0 || i == domain_len - 1) return false; + if (domain[i - 1] == '.') return false; + } + } + return has_dot; +} + +/* RFC 1123 label lengths and hyphens; ascii_only also enforces LDH. */ +static bool validate_hostname_ex(const char *val, int len, bool ascii_only) +{ + if (len < 1 || len > 253) return false; + int label_start= 0; + for (int i= 0; i <= len; i++) + { + if (i == len || val[i] == '.') + { + int label_len= i - label_start; + if (label_len < 1 || label_len > 63) return false; + if (val[label_start] == '-' || val[i - 1] == '-') return false; + if (ascii_only) + { + for (int j= label_start; j < i; j++) + { + if (!is_alpha(val[j]) && !is_digit(val[j]) && val[j] != '-') + return false; + } + } + label_start= i + 1; + } + } + return true; +} + +bool Json_schema_format::validate_hostname(const char *val, int len) const +{ + return validate_hostname_ex(val, len, true); +} + +bool Json_schema_format::validate_idn_hostname(const char *val, int len) const +{ + return validate_hostname_ex(val, len, false); +} + +bool Json_schema_format::validate_ipv4(const char *val, int len) const +{ + if (len < 7 || len > 15) return false; + char buf[16]; + memcpy(buf, val, len); + buf[len]= '\0'; + struct in_addr addr; + return inet_pton(AF_INET, buf, &addr) == 1; +} + +bool Json_schema_format::validate_ipv6(const char *val, int len) const +{ + if (len < 2 || len > 45) return false; + char buf[46]; + memcpy(buf, val, len); + buf[len]= '\0'; + struct in6_addr addr; + return inet_pton(AF_INET6, buf, &addr) == 1; +} + +/* RFC 3986 unreserved + gen-delims + sub-delims (unescaped URI bytes). */ +static inline bool is_uri_char(char c) +{ + if (is_alpha(c) || is_digit(c)) return true; + if (c == '-' || c == '.' || c == '_' || c == '~') return true; + if (c == ':' || c == '/' || c == '?' || c == '#' || + c == '[' || c == ']' || c == '@') return true; + if (c == '!' || c == '$' || c == '&' || c == '\'' || + c == '(' || c == ')' || c == '*' || c == '+' || + c == ',' || c == ';' || c == '=') return true; + return false; +} + +/* URI body: valid char or pct-encoded. allow_non_ascii permits IRI bytes. */ +static bool validate_uri_tail(const char *val, int len, bool allow_non_ascii) +{ + int i= 0; + while (i < len) + { + unsigned char c= (unsigned char) val[i]; + if (c == '%') + { + if (i + 2 >= len) return false; + if (!is_hex(val[i + 1]) || !is_hex(val[i + 2])) return false; + i+= 3; + continue; + } + if (c >= 0x80) + { + if (!allow_non_ascii) return false; + i++; + continue; + } + if (!is_uri_char((char) c)) return false; + i++; + } + return true; +} + +/* scheme ":" then URI body (RFC 3986, syntactic check only). */ +static bool validate_uri_ex(const char *val, int len, + bool allow_non_ascii) +{ + if (len < 1 || !is_alpha(val[0])) return false; + int i= 1; + while (i < len && (is_alpha(val[i]) || is_digit(val[i]) || + val[i] == '+' || val[i] == '-' || val[i] == '.')) + i++; + if (i >= len || val[i] != ':') return false; + return validate_uri_tail(val + i + 1, len - i - 1, allow_non_ascii); +} + +bool Json_schema_format::validate_uri(const char *val, int len) const +{ + return validate_uri_ex(val, len, false); +} + +bool Json_schema_format::validate_iri(const char *val, int len) const +{ + return validate_uri_ex(val, len, true); +} + +/* URI-reference: absolute URI or relative-ref (RFC 3986 4.1). */ +bool Json_schema_format::validate_uri_reference(const char *val, int len) const +{ + return len == 0 || validate_uri_tail(val, len, false); +} + +bool Json_schema_format::validate_iri_reference(const char *val, int len) const +{ + return len == 0 || validate_uri_tail(val, len, true); +} + +bool Json_schema_format::validate_uuid(const char *val, int len) const +{ + if (len != 36) return false; + for (int i= 0; i < 36; i++) + { + if (i == 8 || i == 13 || i == 18 || i == 23) + { + if (val[i] != '-') return false; + } + else + { + if (!is_hex(val[i])) return false; + } + } + return true; +} + +bool Json_schema_format::validate_json_pointer(const char *val, int len) const +{ + if (len == 0) return true; + if (val[0] != '/') return false; + for (int i= 1; i < len; i++) + { + if (val[i] == '~') + { + if (i + 1 >= len) return false; + if (val[i + 1] != '0' && val[i + 1] != '1') return false; + i++; + } + } + return true; +} + +bool Json_schema_format::validate_relative_json_pointer(const char *val, int len) const +{ + if (len < 1) return false; + if (!is_digit(val[0])) return false; + int pos= 0; + while (pos < len && is_digit(val[pos])) pos++; + if (pos == len) return true; + if (val[pos] == '#') return pos + 1 == len; + return validate_json_pointer(val + pos, len - pos); +} + +/* + Unlike Json_schema_pattern (which compiles the schema's own fixed + pattern once), format:"regex" checks the document's value, which + varies per row, so it can't be precompiled at schema-parse time. +*/ +bool Json_schema_format::validate_regex(const char *val, int len) const +{ + int errcode; + PCRE2_SIZE erroffset; + pcre2_code *re= pcre2_compile((PCRE2_SPTR) val, (PCRE2_SIZE) len, + 0, &errcode, &erroffset, NULL); + if (!re) + return false; + pcre2_code_free(re); + return true; +} + +bool Json_schema_format::validate(const json_engine_t *je, + MEM_ROOT *current_mem_root, + const uchar *k_start, + const uchar *k_end) +{ + if (je->value_type != JSON_VALUE_STRING) + return false; + if (!current_thd->variables.json_schema_format_validation) + return false; + if (!validator) + return false; + + const char *val= (const char *) je->value; + int len= (int) je->value_len; + + return !(this->*validator)(val, len); +} + bool Json_schema_type::validate(const json_engine_t *je, MEM_ROOT *current_mem_root, const uchar *k_start, diff --git a/sql/json_schema.h b/sql/json_schema.h index d3bc1e7c13e43..45836cf1adc97 100644 --- a/sql/json_schema.h +++ b/sql/json_schema.h @@ -136,12 +136,46 @@ class Json_schema_annotation : public Json_schema_keyword class Json_schema_format : public Json_schema_keyword { - public: - bool handle_keyword(THD *thd, MEM_ROOT *current_mem_root, - json_engine_t *je, - const char* key_start, - const char* key_end, - List *all_keywords) override; +public: + /* + Resolved once at schema-parse time from the format-name hash, so + validate() dispatches with a single call on the per-document path. + */ + typedef bool (Json_schema_format::*Format_validator)(const char *val, + int len) const; +private: + Format_validator validator; + static HASH format_hash; + + bool validate_date(const char *val, int len) const; + bool validate_time_part(const char *val, int len) const; + bool validate_datetime(const char *val, int len) const; + bool validate_duration(const char *val, int len) const; + bool validate_email(const char *val, int len) const; + bool validate_idn_email(const char *val, int len) const; + bool validate_hostname(const char *val, int len) const; + bool validate_idn_hostname(const char *val, int len) const; + bool validate_ipv4(const char *val, int len) const; + bool validate_ipv6(const char *val, int len) const; + bool validate_uri(const char *val, int len) const; + bool validate_iri(const char *val, int len) const; + bool validate_uri_reference(const char *val, int len) const; + bool validate_iri_reference(const char *val, int len) const; + bool validate_uuid(const char *val, int len) const; + bool validate_json_pointer(const char *val, int len) const; + bool validate_relative_json_pointer(const char *val, int len) const; + bool validate_regex(const char *val, int len) const; +public: + Json_schema_format() : validator(NULL) {} + bool validate(const json_engine_t *je, MEM_ROOT *current_mem_root, + const uchar *k_start= NULL, + const uchar *k_end= NULL) override; + bool handle_keyword(THD *thd, MEM_ROOT *current_mem_root, + json_engine_t *je, const char* key_start, + const char* key_end, + List *all_keywords) override; + static bool setup_format_hash(); + static void cleanup_format_hash(); }; typedef List List_schema_keyword; diff --git a/sql/mysqld.cc b/sql/mysqld.cc index 07a597b3e1106..7867d387baef7 100644 --- a/sql/mysqld.cc +++ b/sql/mysqld.cc @@ -2017,6 +2017,7 @@ static void clean_up(bool print_message, bool use_dummy_thd) lex_free(); /* Free some memory */ item_create_cleanup(); cleanup_json_schema_keyword_hash(); + Json_schema_format::cleanup_format_hash(); tdc_start_shutdown(use_dummy_thd); #ifdef HAVE_REPLICATION semi_sync_master_deinit(); @@ -4341,6 +4342,7 @@ static int init_common_variables() return 1; item_init(); setup_json_schema_keyword_hash(); + Json_schema_format::setup_format_hash(); /* Process a comma-separated character set list and choose the first available character set. This is mostly for diff --git a/sql/sql_class.h b/sql/sql_class.h index 64fae69773ca0..3bd9a8e9d0961 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -927,6 +927,7 @@ typedef struct system_variables my_bool session_track_user_variables; #endif // USER_VAR_TRACKING my_bool tcp_nodelay; + my_bool json_schema_format_validation; my_bool optimizer_record_context; plugin_ref table_plugin; plugin_ref tmp_table_plugin; diff --git a/sql/sys_vars.cc b/sql/sys_vars.cc index 7e321359b1c7e..3ce1834ea2257 100644 --- a/sql/sys_vars.cc +++ b/sql/sys_vars.cc @@ -1530,6 +1530,13 @@ static Sys_var_ulonglong Sys_join_buffer_size( SESSION_VAR(join_buff_size), CMD_LINE(REQUIRED_ARG), VALID_RANGE(128, SIZE_T_MAX), DEFAULT(256*1024), BLOCK_SIZE(128)); +static Sys_var_mybool Sys_json_schema_format_validation( + "json_schema_format_validation", + "Enable format validation for JSON_SCHEMA_VALID(). " + "When OFF (default), the format keyword is treated as an annotation only", + SESSION_VAR(json_schema_format_validation), CMD_LINE(OPT_ARG), + DEFAULT(FALSE)); + static Sys_var_keycache Sys_key_buffer_size( "key_buffer_size", "The size of the buffer used for " "index blocks for MyISAM tables. Increase this to get faster index "