From 62dd6a4925774a77e96d51d53e4682a2d7f68d7c Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 13 Oct 2026 13:28:31 -0300 Subject: [PATCH 01/11] Upgrade all Sourcemeta dependencies Signed-off-by: Juan Cruz Viotti --- DEPENDENCIES | 6 +- vendor/blaze/CMakeLists.txt | 2 + vendor/blaze/DEPENDENCIES | 2 +- vendor/blaze/ports/javascript/describe.mjs | 47 + vendor/blaze/ports/javascript/index.mjs | 375 ++-- vendor/blaze/ports/javascript/opcodes.mjs | 126 +- vendor/blaze/schemas/canonical-draft3.json | 67 +- vendor/blaze/src/alterschema/schema_helpers.h | 1 + vendor/blaze/src/alterschema/wrap.cc | 2 +- vendor/blaze/src/canonicalizer/CMakeLists.txt | 2 + .../blaze/src/canonicalizer/canonicalize.cc | 112 +- vendor/blaze/src/canonicalizer/helpers.h | 229 ++- .../rules/additional_items_implicit.h | 6 +- .../rules/dependencies_to_extends_disallow.h | 4 +- .../rules/disallow_to_array_of_schemas.h | 19 +- .../rules/divisible_by_implicit.h | 10 +- .../src/canonicalizer/rules/draft3_type_any.h | 4 +- .../rules/empty_dependencies_drop.h | 1 + .../rules/enum_drop_redundant_validation.h | 9 +- .../canonicalizer/rules/enum_filter_by_type.h | 14 +- .../canonicalizer/rules/enum_split_by_type.h | 3 +- .../src/canonicalizer/rules/enum_with_type.h | 49 +- .../rules/equal_numeric_bounds_to_enum.h | 13 +- .../exclusive_maximum_boolean_integer_fold.h | 1 + .../exclusive_minimum_boolean_integer_fold.h | 1 + .../canonicalizer/rules/extends_to_array.h | 14 +- .../rules/hyper_schema_keywords_drop.h | 59 + .../rules/implicit_contains_keywords.h | 20 +- .../rules/implicit_object_keywords.h | 8 +- .../src/canonicalizer/rules/items_implicit.h | 24 +- .../rules/metadata_keywords_drop.h | 57 + .../canonicalizer/rules/min_length_implicit.h | 1 + .../rules/required_property_implicit.h | 4 +- .../rules/type_boolean_as_enum.h | 1 + .../rules/type_inherit_in_place.h | 1 + .../canonicalizer/rules/type_null_as_enum.h | 1 + .../canonicalizer/rules/type_union_implicit.h | 15 +- .../rules/type_union_to_schemas.h | 14 +- .../rules/type_with_applicator_to_allof.h | 16 + .../rules/type_with_applicator_to_extends.h | 14 +- .../rules/unknown_keywords_drop.h | 50 + .../rules/unknown_keywords_prefix.h | 9 +- .../rules/unsatisfiable_empty_enum.h | 1 + .../unsatisfiable_exclusive_equal_bounds.h | 1 + .../rules/unsatisfiable_type_and_enum.h | 15 +- .../blaze/src/canonicalizer/schema_helpers.h | 1 + vendor/blaze/src/compiler/CMakeLists.txt | 5 +- vendor/blaze/src/compiler/compile.cc | 122 +- vendor/blaze/src/compiler/compile_helpers.h | 77 +- vendor/blaze/src/compiler/default_compiler.cc | 222 ++- .../src/compiler/default_compiler_2019_09.h | 45 +- .../src/compiler/default_compiler_draft3.h | 520 +++++- .../src/compiler/default_compiler_draft4.h | 82 +- .../src/compiler/default_compiler_draft6.h | 18 +- .../include/sourcemeta/blaze/compiler.h | 19 +- .../sourcemeta/blaze/compiler_unevaluated.h | 50 - vendor/blaze/src/compiler/postprocess.h | 32 +- vendor/blaze/src/compiler/schema_helpers.h | 1 + vendor/blaze/src/compiler/unevaluated.cc | 311 ---- vendor/blaze/src/compiler/unevaluated.h | 351 ++++ vendor/blaze/src/convert/convert.cc | 56 +- vendor/blaze/src/convert/helpers.h | 171 +- .../dialect_override_becomes_dollar_schema.h | 59 +- .../prefix_promoted_draft_2019_09_keywords.h | 34 +- .../rules/prefix_promoted_draft_7_keywords.h | 34 +- .../prefix_promoted_openapi_3_1_keywords.h | 44 +- .../rules/upgrade_openapi_3_0_to_draft_4.h | 524 ++++++ .../blaze/src/evaluator/evaluator_describe.cc | 57 + vendor/blaze/src/evaluator/evaluator_json.cc | 1 + .../include/sourcemeta/blaze/evaluator.h | 2 +- .../sourcemeta/blaze/evaluator_dispatch.h | 84 +- .../sourcemeta/blaze/evaluator_instruction.h | 4 + .../sourcemeta/blaze/evaluator_value.h | 23 +- vendor/core/CMakeLists.txt | 18 +- vendor/core/config.cmake.in | 18 +- .../core/src/core/crypto/crypto_aes_block.h | 31 +- vendor/core/src/core/crypto/crypto_base64.cc | 102 +- vendor/core/src/core/crypto/crypto_bignum.h | 16 +- vendor/core/src/core/crypto/crypto_ecc.h | 19 +- vendor/core/src/core/crypto/crypto_eddsa.h | 37 +- vendor/core/src/core/crypto/crypto_pkcs8.h | 97 +- .../src/core/crypto/crypto_random_other.cc | 38 +- .../core/src/core/crypto/crypto_sign_apple.cc | 8 +- .../core/src/core/crypto/crypto_sign_other.cc | 107 +- .../src/core/crypto/crypto_sign_windows.cc | 8 +- .../src/core/crypto/crypto_verify_other.cc | 21 +- .../include/sourcemeta/core/crypto_base64.h | 30 + vendor/core/src/core/diff/myers.h | 10 +- vendor/core/src/core/dns/idn_hostname.cc | 9 +- vendor/core/src/core/email/email.cc | 5 +- vendor/core/src/core/email/helpers.h | 8 +- vendor/core/src/core/gzip/deflate.h | 3 - .../include/sourcemeta/core/html_escape.h | 8 +- .../include/sourcemeta/core/html_writer.h | 1 - vendor/core/src/core/http/CMakeLists.txt | 7 +- vendor/core/src/core/http/client_curl.cc | 8 + vendor/core/src/core/http/client_darwin.mm | 12 + vendor/core/src/core/http/client_windows.cc | 37 +- .../src/core/http/continue_traceparent.cc | 52 + vendor/core/src/core/http/entropy.h | 33 + vendor/core/src/core/http/helpers.h | 14 +- .../core/http/include/sourcemeta/core/http.h | 2 + .../include/sourcemeta/core/http_baggage.h | 232 +++ .../http/include/sourcemeta/core/http_error.h | 34 + .../include/sourcemeta/core/http_method.h | 4 + .../include/sourcemeta/core/http_syntax.h | 34 +- .../sourcemeta/core/http_tracecontext.h | 298 ++++ vendor/core/src/core/http/is_baggage.cc | 84 + vendor/core/src/core/http/is_baggage_value.cc | 47 + vendor/core/src/core/http/is_traceparent.cc | 72 + vendor/core/src/core/http/is_tracestate.cc | 146 ++ vendor/core/src/core/http/make_traceparent.cc | 37 + vendor/core/src/core/http/match_accept.cc | 11 +- .../src/core/http/match_accept_language.cc | 11 +- vendor/core/src/core/http/parse_media_type.cc | 8 +- .../core/src/core/http/parse_traceparent.cc | 39 + .../core/src/core/http/scan_quoted_string.cc | 21 +- .../src/core/http/serialize_cache_control.cc | 4 + vendor/core/src/core/http/serialize_cookie.cc | 4 + vendor/core/src/core/ip/ipv6.cc | 9 +- .../core/src/core/jose/jose_jwks_provider.cc | 2 +- vendor/core/src/core/json/construct.h | 429 ++--- .../json/include/sourcemeta/core/json_value.h | 60 + vendor/core/src/core/json/json.cc | 51 +- vendor/core/src/core/json/json_value.cc | 11 +- vendor/core/src/core/json/parser.h | 298 ++-- vendor/core/src/core/json/stringify.h | 33 +- vendor/core/src/core/jsonl/iterator.cc | 4 + vendor/core/src/core/jsonld/jsonld.cc | 21 +- .../core/src/core/jsonld/jsonld_algorithms.h | 133 +- .../core/src/core/jsonld/jsonld_compaction.cc | 6 +- .../core/jsonld/jsonld_context_processing.cc | 243 ++- .../jsonld/jsonld_create_term_definition.cc | 286 +-- .../core/src/core/jsonld/jsonld_expansion.cc | 728 ++++++-- vendor/core/src/core/jsonld/jsonld_flatten.cc | 5 +- .../src/core/jsonld/jsonld_iri_expansion.cc | 29 +- .../src/core/jsonld/jsonld_is_expanded.cc | 6 +- .../src/core/jsonld/jsonld_materialize.cc | 31 +- .../core/src/core/jsonld/jsonld_serialise.h | 11 +- .../core/jsonld/jsonld_value_compaction.cc | 6 +- .../src/core/jsonld/jsonld_value_expansion.cc | 10 +- vendor/core/src/core/jsonpath/jsonpath.cc | 20 +- vendor/core/src/core/jsonpath/parser.h | 19 +- vendor/core/src/core/jsonpointer/parser.h | 19 +- vendor/core/src/core/jsonpointer/position.cc | 6 - .../jsonrpc/include/sourcemeta/core/jsonrpc.h | 19 + vendor/core/src/core/jsonrpc/jsonrpc.cc | 13 + .../core/src/core/jsonschema/CMakeLists.txt | 2 +- vendor/core/src/core/jsonschema/bundle.cc | 579 +++++- vendor/core/src/core/jsonschema/format.cc | 30 +- vendor/core/src/core/jsonschema/frame.cc | 238 ++- vendor/core/src/core/jsonschema/hasher.h | 87 + vendor/core/src/core/jsonschema/helpers.h | 141 +- .../include/sourcemeta/core/jsonschema.h | 7 +- .../sourcemeta/core/jsonschema_error.h | 70 + .../sourcemeta/core/jsonschema_frame.h | 9 +- .../sourcemeta/core/jsonschema_vocabularies.h | 17 +- vendor/core/src/core/jsonschema/iterator.h | 150 +- vendor/core/src/core/jsonschema/jsonschema.cc | 88 +- .../src/core/jsonschema/known_resolver.cmake | 5 + .../src/core/jsonschema/known_resolver.in.cc | 55 + .../core/src/core/jsonschema/known_walker.cc | 485 +++-- .../core/src/core/jsonschema/vocabularies.cc | 45 +- vendor/core/src/core/markdown/blocks.h | 138 +- vendor/core/src/core/markdown/characters.h | 16 +- vendor/core/src/core/markdown/document.h | 7 +- vendor/core/src/core/markdown/inlines.h | 23 +- vendor/core/src/core/markdown/postprocess.h | 8 +- vendor/core/src/core/markdown/references.h | 15 +- vendor/core/src/core/markdown/render.h | 3 +- vendor/core/src/core/markdown/scanners.h | 177 +- vendor/core/src/core/mcp/CMakeLists.txt | 9 +- vendor/core/src/core/mcp/capabilities.cc | 396 +++++ vendor/core/src/core/mcp/error.cc | 183 ++ vendor/core/src/core/mcp/headers.cc | 540 ++++++ vendor/core/src/core/mcp/helpers.h | 334 ++++ .../core/mcp/include/sourcemeta/core/mcp.h | 533 +----- .../sourcemeta/core/mcp_capabilities.h | 262 +++ .../mcp/include/sourcemeta/core/mcp_error.h | 424 +++++ .../mcp/include/sourcemeta/core/mcp_headers.h | 490 +++++ .../mcp/include/sourcemeta/core/mcp_logging.h | 141 ++ .../include/sourcemeta/core/mcp_protocol.h | 1307 ++++++++++++++ .../mcp/include/sourcemeta/core/mcp_results.h | 1569 +++++++++++++++++ vendor/core/src/core/mcp/mcp.cc | 419 ----- vendor/core/src/core/mcp/notifications.cc | 356 ++++ vendor/core/src/core/mcp/parameter_headers.cc | 187 ++ vendor/core/src/core/mcp/results.cc | 1174 ++++++++++++ vendor/core/src/core/mcp/validation.h | 1190 +++++++++++++ vendor/core/src/core/oauth/oauth_assertion.cc | 10 +- vendor/core/src/core/oauth/oauth_par.cc | 7 +- .../core/src/core/oauth/oauth_registration.cc | 8 +- .../core/src/core/oidc/oidc_authentication.cc | 17 +- vendor/core/src/core/oidc/oidc_id_token.cc | 12 +- vendor/core/src/core/oidc/oidc_metadata.cc | 6 +- .../core/src/core/oidc/oidc_registration.cc | 71 +- vendor/core/src/core/openapi/bundle.cc | 553 +++++- vendor/core/src/core/openapi/components.h | 88 +- vendor/core/src/core/openapi/content.h | 10 +- vendor/core/src/core/openapi/discriminator.h | 19 +- vendor/core/src/core/openapi/document.h | 116 +- vendor/core/src/core/openapi/format.cc | 19 +- vendor/core/src/core/openapi/frame.cc | 54 +- vendor/core/src/core/openapi/helpers.h | 165 +- .../openapi/include/sourcemeta/core/openapi.h | 99 +- vendor/core/src/core/openapi/info.h | 23 + vendor/core/src/core/openapi/parameter.h | 11 +- vendor/core/src/core/openapi/path_item.h | 8 + vendor/core/src/core/openapi/paths.h | 23 +- vendor/core/src/core/openapi/reference.h | 20 +- vendor/core/src/core/openapi/response.h | 2 +- vendor/core/src/core/openapi/security.h | 45 +- vendor/core/src/core/openapi/server.h | 138 +- vendor/core/src/core/openapi/version.cc | 35 +- vendor/core/src/core/punycode/punycode.cc | 11 +- vendor/core/src/core/regex/ecma262.h | 8 +- vendor/core/src/core/regex/iregexp.h | 8 +- vendor/core/src/core/regex/permissive.h | 104 +- vendor/core/src/core/regex/regex.cc | 4 + vendor/core/src/core/semver/semver.cc | 71 +- vendor/core/src/core/terminal/terminal.cc | 25 +- vendor/core/src/core/time/rfc3339_duration.cc | 8 +- vendor/core/src/core/time/rfc3339_fulltime.cc | 5 +- vendor/core/src/core/unicode/codegen.cc | 19 +- vendor/core/src/core/uri/canonicalize.cc | 2 +- vendor/core/src/core/uri/escaping.h | 9 - vendor/core/src/core/uri/filesystem.cc | 15 +- .../core/uri/include/sourcemeta/core/uri.h | 3 +- vendor/core/src/core/uri/normalize.h | 9 +- vendor/core/src/core/uri/parse.cc | 13 +- vendor/core/src/core/uri/path.cc | 5 +- vendor/core/src/core/uri/resolution.cc | 4 +- vendor/core/src/core/uri/setters.cc | 22 +- vendor/core/src/core/uritemplate/helpers.h | 23 +- .../core/src/core/uritemplate/uritemplate.cc | 41 +- .../core/uritemplate/uritemplate_router.cc | 14 +- .../uritemplate/uritemplate_router_view.cc | 5 + vendor/core/src/core/yaml/lexer.h | 271 +-- vendor/core/src/core/yaml/parser.h | 1132 ++++++------ vendor/core/src/core/yaml/stringify.h | 65 +- vendor/core/src/core/yaml/yaml.cc | 14 +- vendor/core/src/lang/io/io.cc | 8 +- vendor/core/src/lang/io/io_fileview.cc | 50 +- vendor/core/src/lang/io/io_temporary.cc | 11 +- .../core/src/lang/numeric/big_coefficient.h | 47 +- vendor/core/src/lang/numeric/decimal.cc | 626 +++++-- .../include/sourcemeta/core/numeric_decimal.h | 4 +- .../include/sourcemeta/core/numeric_parse.h | 20 + .../include/sourcemeta/core/numeric_uint128.h | 5 +- .../include/sourcemeta/core/numeric_util.h | 83 +- .../include/sourcemeta/core/numeric_zigzag.h | 4 +- vendor/core/src/lang/numeric/parse.cc | 93 +- vendor/core/src/lang/options/options.cc | 2 +- .../sourcemeta/core/parallel_for_each.h | 24 +- vendor/core/src/lang/process/spawn.cc | 15 +- .../core/src/lang/stacktrace/CMakeLists.txt | 11 + .../src/lang/stacktrace/stacktrace_posix.h | 5 +- .../src/lang/stacktrace/stacktrace_windows.h | 4 +- vendor/core/src/lang/test/clitest/clitest.h | 12 +- .../lang/text/include/sourcemeta/core/text.h | 26 + vendor/core/src/lang/text/text.cc | 32 +- vendor/jsonbinpack/DEPENDENCIES | 4 +- .../src/compiler/mapper/number_arbitrary.h | 2 +- vendor/jsonbinpack/src/runtime/decoder_any.cc | 2 + .../jsonbinpack/src/runtime/decoder_common.cc | 1 + .../jsonbinpack/src/runtime/decoder_number.cc | 37 + vendor/jsonbinpack/src/runtime/encoder_any.cc | 4 + .../jsonbinpack/src/runtime/encoder_common.cc | 1 + .../jsonbinpack/src/runtime/encoder_number.cc | 97 +- .../sourcemeta/jsonbinpack/runtime_decoder.h | 1 + .../sourcemeta/jsonbinpack/runtime_encoder.h | 1 + .../sourcemeta/jsonbinpack/runtime_encoding.h | 57 +- vendor/jsonbinpack/src/runtime/loader.cc | 1 + .../src/runtime/loader_v1_number.h | 5 + 273 files changed, 20324 insertions(+), 5081 deletions(-) create mode 100644 vendor/blaze/src/canonicalizer/rules/hyper_schema_keywords_drop.h create mode 100644 vendor/blaze/src/canonicalizer/rules/metadata_keywords_drop.h create mode 100644 vendor/blaze/src/canonicalizer/rules/unknown_keywords_drop.h delete mode 100644 vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler_unevaluated.h delete mode 100644 vendor/blaze/src/compiler/unevaluated.cc create mode 100644 vendor/blaze/src/compiler/unevaluated.h create mode 100644 vendor/blaze/src/convert/rules/upgrade_openapi_3_0_to_draft_4.h create mode 100644 vendor/core/src/core/http/continue_traceparent.cc create mode 100644 vendor/core/src/core/http/entropy.h create mode 100644 vendor/core/src/core/http/include/sourcemeta/core/http_baggage.h create mode 100644 vendor/core/src/core/http/include/sourcemeta/core/http_tracecontext.h create mode 100644 vendor/core/src/core/http/is_baggage.cc create mode 100644 vendor/core/src/core/http/is_baggage_value.cc create mode 100644 vendor/core/src/core/http/is_traceparent.cc create mode 100644 vendor/core/src/core/http/is_tracestate.cc create mode 100644 vendor/core/src/core/http/make_traceparent.cc create mode 100644 vendor/core/src/core/http/parse_traceparent.cc create mode 100644 vendor/core/src/core/jsonschema/hasher.h create mode 100644 vendor/core/src/core/mcp/capabilities.cc create mode 100644 vendor/core/src/core/mcp/error.cc create mode 100644 vendor/core/src/core/mcp/headers.cc create mode 100644 vendor/core/src/core/mcp/helpers.h create mode 100644 vendor/core/src/core/mcp/include/sourcemeta/core/mcp_capabilities.h create mode 100644 vendor/core/src/core/mcp/include/sourcemeta/core/mcp_error.h create mode 100644 vendor/core/src/core/mcp/include/sourcemeta/core/mcp_headers.h create mode 100644 vendor/core/src/core/mcp/include/sourcemeta/core/mcp_logging.h create mode 100644 vendor/core/src/core/mcp/include/sourcemeta/core/mcp_protocol.h create mode 100644 vendor/core/src/core/mcp/include/sourcemeta/core/mcp_results.h delete mode 100644 vendor/core/src/core/mcp/mcp.cc create mode 100644 vendor/core/src/core/mcp/notifications.cc create mode 100644 vendor/core/src/core/mcp/parameter_headers.cc create mode 100644 vendor/core/src/core/mcp/results.cc create mode 100644 vendor/core/src/core/mcp/validation.h diff --git a/DEPENDENCIES b/DEPENDENCIES index c5a3fb83c..72c73fb3e 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,5 +1,5 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core 783d6646d159d0862755aca042dda0c426e918b2 -jsonbinpack https://github.com/sourcemeta/jsonbinpack 9c6be797ea4f2a586deff6f871506e7f729f3e8e -blaze https://github.com/sourcemeta/blaze 049a75b2a45c1178e6d484e349c449fe4b36ae4d +core https://github.com/sourcemeta/core af2b932ef6284c880f47012749b78bb75526f7a4 +jsonbinpack https://github.com/sourcemeta/jsonbinpack 147050385dce7086a28ce93e18a4c3baa9719136 +blaze https://github.com/sourcemeta/blaze a5a019aa49139e7cd21accd52ad2c65f66afff16 ctrf https://github.com/ctrf-io/ctrf 93ea827d951390190171d37443bff169cf47c808 diff --git a/vendor/blaze/CMakeLists.txt b/vendor/blaze/CMakeLists.txt index 0c07dcffd..b6723e97e 100644 --- a/vendor/blaze/CMakeLists.txt +++ b/vendor/blaze/CMakeLists.txt @@ -132,6 +132,8 @@ if(PROJECT_IS_TOP_LEVEL) "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_2019_09_invalid.json" "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_2020_12.json" "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_2020_12_invalid.json" + "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_openapi_3_0.json" + "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_openapi_3_0_invalid.json" "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_openapi_3_1.json" "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_openapi_3_2.json" "${PROJECT_SOURCE_DIR}/test/output/output_standard_basic.json" diff --git a/vendor/blaze/DEPENDENCIES b/vendor/blaze/DEPENDENCIES index c0a910cc6..aa4dbae86 100644 --- a/vendor/blaze/DEPENDENCIES +++ b/vendor/blaze/DEPENDENCIES @@ -1,3 +1,3 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core 783d6646d159d0862755aca042dda0c426e918b2 +core https://github.com/sourcemeta/core af2b932ef6284c880f47012749b78bb75526f7a4 jsonschema-test-suite https://github.com/json-schema-org/JSON-Schema-Test-Suite 6648e8194c69697b2e1a15fe76a06a480b183a51 diff --git a/vendor/blaze/ports/javascript/describe.mjs b/vendor/blaze/ports/javascript/describe.mjs index 00609784e..4d2718df0 100644 --- a/vendor/blaze/ports/javascript/describe.mjs +++ b/vendor/blaze/ports/javascript/describe.mjs @@ -18,6 +18,8 @@ import { ASSERTION_UNIQUE, ASSERTION_DIVISIBLE, ASSERTION_TYPE_INTEGER_BOUNDED, ASSERTION_TYPE_INTEGER_BOUNDED_STRICT, ASSERTION_TYPE_INTEGER_LOWER_BOUND, ASSERTION_TYPE_INTEGER_LOWER_BOUND_STRICT, + ASSERTION_NUMBER_INTEGER_BOUNDED, + ASSERTION_NUMBER_TYPE, ASSERTION_STRING_TYPE, ASSERTION_PROPERTY_TYPE, ASSERTION_PROPERTY_TYPE_EVALUATE, ASSERTION_PROPERTY_TYPE_STRICT, ASSERTION_PROPERTY_TYPE_STRICT_EVALUATE, @@ -702,6 +704,12 @@ export function describe(valid, instruction, evaluatePath, return message; } + if (keyword === 'x-format-assertion' && typeof annotation === 'boolean') { + return annotation + ? 'A sibling `format` keyword was expected to be enforced as an assertion' + : 'A sibling `format` keyword was expected to be collected as an annotation'; + } + return 'The unrecognized keyword ' + escapeString(keyword) + ' was collected as the annotation ' + stringifyValue(annotation); } @@ -832,6 +840,21 @@ export function describe(valid, instruction, evaluatePath, return 'The value was expected to be an integer above the given minimum'; } + if (opcode === ASSERTION_NUMBER_TYPE) { + return 'The ' + valueTypeName(target) + ' value ' + + stringifyValue(target) + + ' was expected to be exactly representable as an IEEE 754 ' + + (value === 0 ? 'single' : 'double') + + ' precision floating point number'; + } + + if (opcode === ASSERTION_NUMBER_INTEGER_BOUNDED) { + return 'The ' + valueTypeName(target) + ' value ' + + stringifyValue(target) + + ' was expected to be an integer between ' + value[0] + + ' and ' + value[1]; + } + if (opcode === ASSERTION_OBJECT_PROPERTIES_SIMPLE) { return 'The object value was expected to validate against the defined property subschemas'; } @@ -1049,6 +1072,30 @@ export function describe(valid, instruction, evaluatePath, 'string is short enough to satisfy a negative maximum length'; } + // The lower bounds below compile the same way when the bound is too large + // to hold, as nothing can be big enough to satisfy one + if (keyword === 'minLength') { + return 'The value was expected to be of a type other than string, as no ' + + 'string is long enough to satisfy a minimum length that large'; + } + + if (keyword === 'minItems') { + return 'The value was expected to be of a type other than array, as no ' + + 'array has enough items to satisfy a minimum size that large'; + } + + if (keyword === 'minProperties') { + return 'The value was expected to be of a type other than object, as no ' + + 'object has enough properties to satisfy a minimum size that large'; + } + + // `contains` compiles the same way when its `minContains` is too large to + // hold, as no array has that many items for the subschema to match + if (keyword === 'contains') { + return 'The value was expected to be of a type other than array, as no ' + + 'array has enough matching items to satisfy a minimum count that large'; + } + return describeNotTypesCheck(valid, targetType, value); } diff --git a/vendor/blaze/ports/javascript/index.mjs b/vendor/blaze/ports/javascript/index.mjs index 3a1d47e47..e804a445e 100644 --- a/vendor/blaze/ports/javascript/index.mjs +++ b/vendor/blaze/ports/javascript/index.mjs @@ -7,7 +7,7 @@ import { CONTROL_JUMP, CONTROL_DYNAMIC_ANCHOR_JUMP } from './opcodes.mjs'; -const JSON_VERSION = 7; +const JSON_VERSION = 9; const DEPTH_LIMIT = 300; const URI_REGEX = /^[a-zA-Z][a-zA-Z0-9+\-.]*:[^\s]*$/; @@ -300,48 +300,50 @@ function compileInstructionToCode(instruction, captures, visited, budget) { case 34: return fb(34); case 35: return fb(35); case 36: return fb(36); case 37: return fb(37); case 38: return fb(38); case 39: return fb(39); case 40: return fb(40); - case 41: { var r=R('t'); return r?IO+r+'if(t===void 0)return true;var a=_jt(t);return a==='+value+'||('+value+'===2&&_ii(t));':null; } + case 41: return fb(41); case 42: return fb(42); - case 43: { var r=R('t'); return r?IO+r+'if(t===void 0)return true;return _es(t)==='+value+';':null; } + case 43: { var r=R('t'); return r?IO+r+'if(t===void 0)return true;var a=_jt(t);return a==='+value+'||('+value+'===2&&_ii(t));':null; } case 44: return fb(44); - case 45: { var r=R('t'); return r?IO+r+'if(t===void 0)return true;return('+value+'&(1<<_es(t)))!==0;':null; } - case 46: return fb(46); case 47: return fb(47); case 48: return fb(48); - case 49: return fb(49); - case 50: case 51: case 52: case 53: case 54: return 'return true;'; - case 55: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return 'return false;'; var c=r; for(var j=0;j0)c+=seq(children,'t'); return c+'return true;'; } - case 62: { var r=R('t'); if(!r)return null; var c=r+TO+'return true;if(!Object.hasOwn(t,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'t'); return c+'return true;'; } - case 63: { var r=R('t'); if(!r)return null; var c=r+'if(!Array.isArray(t)||t.length<='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'t'); return c+'return true;'; } - case 64: return fb(64); case 65: return fb(65); - case 66: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; var mi=captures.length; captures.push(value); var gf=''; for(var gi=0;gi0){gf+='function(i,d,_t,_v){'+lb(gc,'i')+'return true;},';}else{gf+='null,';}} return r+TO+'return true;var __cg=['+gf+'];for(var k in t){var __mi=_c['+mi+'][k];if(__mi!==void 0){var __cf=__cg[__mi];if(__cf&&!__cf(t,d,_t,_v))return false;}}return true;'; } - case 67: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; var mi=captures.length; captures.push(value); var gf=''; for(var gi=0;gi0){gf+='function(i,d,_t,_v){'+lb(gc,'i')+'return true;},';}else{gf+='null,';}} return r+TO+'return true;var __cg=['+gf+'];for(var k in t){var __mi=_c['+mi+'][k];if(__mi===void 0)return false;var __cf=__cg[__mi];if(__cf&&!__cf(t,d,_t,_v))return false;}return true;'; } - case 68: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; return r+TO+'return true;for(var k in t){'+lb(children,'t[k]')+'}return true;'; } - case 69: return fb(69); case 70: return fb(70); case 71: return fb(71); case 72: return fb(72); case 73: return fb(73); - case 74: return fb(74); case 75: return fb(75); case 76: return fb(76); - case 77: return fb(77); - case 78: { var r=R('t'); return r?r+TO+'return true;for(var k in t){if(_es(t[k])!=='+value+')return false;}return true;':null; } - case 79: return fb(79); case 80: return fb(80); case 81: return fb(81); - case 82: return fb(82); - case 83: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; return r+'if(!Array.isArray(t))return true;for(var j=0;j=t.length)return true;for(var j='+value+';j0)c+=seq(children,'i'); return c+'return true;'; } - case 96: { var c=IO+'if(!Object.hasOwn(i,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } - case 97: { var c='if(_jt(i)!=='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } - case 98: return 'return true;'; - case 99: return fb(99); - case 100: { if(!value)return 'return true;'; if(visited&&visited.has(instruction))return fb(100); if(!visited)visited=new Set(); visited.add(instruction); var r=R('t'); if(!r)return fb(100); var c=r; for(var j=0;j0)c+=seq(children,'t'); return c+'return true;'; } + case 64: { var r=R('t'); if(!r)return null; var c=r+TO+'return true;if(!Object.hasOwn(t,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'t'); return c+'return true;'; } + case 65: { var r=R('t'); if(!r)return null; var c=r+'if(!Array.isArray(t)||t.length<='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'t'); return c+'return true;'; } + case 66: return fb(66); case 67: return fb(67); + case 68: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; var mi=captures.length; captures.push(value); var gf=''; for(var gi=0;gi0){gf+='function(i,d,_t,_v){'+lb(gc,'i')+'return true;},';}else{gf+='null,';}} return r+TO+'return true;var __cg=['+gf+'];for(var k in t){var __mi=_c['+mi+'][k];if(__mi!==void 0){var __cf=__cg[__mi];if(__cf&&!__cf(t,d,_t,_v))return false;}}return true;'; } + case 69: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; var mi=captures.length; captures.push(value); var gf=''; for(var gi=0;gi0){gf+='function(i,d,_t,_v){'+lb(gc,'i')+'return true;},';}else{gf+='null,';}} return r+TO+'return true;var __cg=['+gf+'];for(var k in t){var __mi=_c['+mi+'][k];if(__mi===void 0)return false;var __cf=__cg[__mi];if(__cf&&!__cf(t,d,_t,_v))return false;}return true;'; } + case 70: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; return r+TO+'return true;for(var k in t){'+lb(children,'t[k]')+'}return true;'; } + case 71: return fb(71); case 72: return fb(72); case 73: return fb(73); case 74: return fb(74); case 75: return fb(75); + case 76: return fb(76); case 77: return fb(77); case 78: return fb(78); + case 79: return fb(79); + case 80: { var r=R('t'); return r?r+TO+'return true;for(var k in t){if(_es(t[k])!=='+value+')return false;}return true;':null; } + case 81: return fb(81); case 82: return fb(82); case 83: return fb(83); + case 84: return fb(84); + case 85: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; return r+'if(!Array.isArray(t))return true;for(var j=0;j=t.length)return true;for(var j='+value+';j0)c+=seq(children,'i'); return c+'return true;'; } + case 98: { var c=IO+'if(!Object.hasOwn(i,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } + case 99: { var c='if(_jt(i)!=='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } + case 100: return 'return true;'; + case 101: return fb(101); + case 102: { if(!value)return 'return true;'; if(visited&&visited.has(instruction))return fb(102); if(!visited)visited=new Set(); visited.add(instruction); var r=R('t'); if(!r)return fb(102); var c=r; for(var j=0;j= range[0] && target <= range[1]; + if (evaluator.callbackMode) evaluator.callbackPop(instruction, __result); + return __result; +}; + +function AssertionNumberType(instruction, instance, depth, template, evaluator) { + const target = resolveInstance(instance, instruction[2]); + if (typeof target !== 'number' && typeof target !== 'bigint') return true; + if (evaluator.callbackMode) evaluator.callbackPush(instruction); + const __result = numberTypeMatches(target, instruction[6]); + if (evaluator.callbackMode) evaluator.callbackPop(instruction, __result); + return __result; +}; + function AssertionTypeIntegerBoundedStrict(instruction, instance, depth, template, evaluator) { const target = resolveInstance(instance, instruction[2]); if (evaluator.callbackMode) evaluator.callbackPush(instruction); @@ -2433,8 +2469,8 @@ function LoopItemsPropertiesExactlyTypeStrictHash(instruction, instance, depth, if (evaluator.callbackMode) evaluator.callbackPush(instruction); const target = resolveInstance(instance, instruction[2]); if (!Array.isArray(target)) { - if (evaluator.callbackMode) evaluator.callbackPop(instruction, false); - return false; + if (evaluator.callbackMode) evaluator.callbackPop(instruction, true); + return true; } const expectedType = instruction[6][0]; const entries = instruction[6][1][0]; @@ -2702,67 +2738,69 @@ const handlers = [ AssertionTypeIntegerBoundedStrict, // 37 AssertionTypeIntegerLowerBound, // 38 AssertionTypeIntegerLowerBoundStrict, // 39 - AssertionStringType, // 40 - AssertionPropertyType, // 41 - AssertionPropertyTypeEvaluate, // 42 - AssertionPropertyTypeStrict, // 43 - AssertionPropertyTypeStrictEvaluate, // 44 - AssertionPropertyTypeStrictAny, // 45 - AssertionPropertyTypeStrictAnyEvaluate, // 46 - AssertionArrayPrefix, // 47 - AssertionArrayPrefixEvaluate, // 48 - AssertionObjectPropertiesSimple, // 49 - AnnotationEmit, // 50 - AnnotationEmitWrapped, // 51 - AnnotationToParent, // 52 - AnnotationBasenameToParent, // 53 - Evaluate, // 54 - LogicalNot, // 55 - LogicalNotEvaluate, // 56 - LogicalOr, // 57 - LogicalAnd, // 58 - LogicalXor, // 59 - LogicalCondition, // 60 - LogicalWhenType, // 61 - LogicalWhenDefines, // 62 - LogicalWhenArraySizeGreater, // 63 - LoopPropertiesUnevaluated, // 64 - LoopPropertiesUnevaluatedExcept, // 65 - LoopPropertiesMatch, // 66 - LoopPropertiesMatchClosed, // 67 - LoopProperties, // 68 - LoopPropertiesEvaluate, // 69 - LoopPropertiesRegex, // 70 - LoopPropertiesRegexClosed, // 71 - LoopPropertiesStartsWith, // 72 - LoopPropertiesExcept, // 73 - LoopPropertiesType, // 74 - LoopPropertiesTypeEvaluate, // 75 - LoopPropertiesExactlyTypeStrict, // 76 - LoopPropertiesExactlyTypeStrictHash, // 77 - LoopPropertiesTypeStrict, // 78 - LoopPropertiesTypeStrictEvaluate, // 79 - LoopPropertiesTypeStrictAny, // 80 - LoopPropertiesTypeStrictAnyEvaluate, // 81 - LoopKeys, // 82 - LoopItems, // 83 - LoopItemsFrom, // 84 - LoopItemsUnevaluated, // 85 - LoopItemsType, // 86 - LoopItemsTypeStrict, // 87 - LoopItemsTypeStrictAny, // 88 - LoopItemsPropertiesExactlyTypeStrictHash, // 89 - LoopItemsPropertiesExactlyTypeStrictHash, // 90 - LoopItemsIntegerBounded, // 91 - LoopItemsIntegerBoundedSized, // 92 - LoopContains, // 93 - ControlGroup, // 94 - ControlGroupWhenDefines, // 95 - ControlGroupWhenDefinesDirect, // 96 - ControlGroupWhenType, // 97 - ControlEvaluate, // 98 - ControlDynamicAnchorJump, // 99 - ControlJump // 100 + AssertionNumberIntegerBounded, // 40 + AssertionNumberType, // 41 + AssertionStringType, // 42 + AssertionPropertyType, // 43 + AssertionPropertyTypeEvaluate, // 44 + AssertionPropertyTypeStrict, // 45 + AssertionPropertyTypeStrictEvaluate, // 46 + AssertionPropertyTypeStrictAny, // 47 + AssertionPropertyTypeStrictAnyEvaluate, // 48 + AssertionArrayPrefix, // 49 + AssertionArrayPrefixEvaluate, // 50 + AssertionObjectPropertiesSimple, // 51 + AnnotationEmit, // 52 + AnnotationEmitWrapped, // 53 + AnnotationToParent, // 54 + AnnotationBasenameToParent, // 55 + Evaluate, // 56 + LogicalNot, // 57 + LogicalNotEvaluate, // 58 + LogicalOr, // 59 + LogicalAnd, // 60 + LogicalXor, // 61 + LogicalCondition, // 62 + LogicalWhenType, // 63 + LogicalWhenDefines, // 64 + LogicalWhenArraySizeGreater, // 65 + LoopPropertiesUnevaluated, // 66 + LoopPropertiesUnevaluatedExcept, // 67 + LoopPropertiesMatch, // 68 + LoopPropertiesMatchClosed, // 69 + LoopProperties, // 70 + LoopPropertiesEvaluate, // 71 + LoopPropertiesRegex, // 72 + LoopPropertiesRegexClosed, // 73 + LoopPropertiesStartsWith, // 74 + LoopPropertiesExcept, // 75 + LoopPropertiesType, // 76 + LoopPropertiesTypeEvaluate, // 77 + LoopPropertiesExactlyTypeStrict, // 78 + LoopPropertiesExactlyTypeStrictHash, // 79 + LoopPropertiesTypeStrict, // 80 + LoopPropertiesTypeStrictEvaluate, // 81 + LoopPropertiesTypeStrictAny, // 82 + LoopPropertiesTypeStrictAnyEvaluate, // 83 + LoopKeys, // 84 + LoopItems, // 85 + LoopItemsFrom, // 86 + LoopItemsUnevaluated, // 87 + LoopItemsType, // 88 + LoopItemsTypeStrict, // 89 + LoopItemsTypeStrictAny, // 90 + LoopItemsPropertiesExactlyTypeStrictHash, // 91 + LoopItemsPropertiesExactlyTypeStrictHash, // 92 + LoopItemsIntegerBounded, // 93 + LoopItemsIntegerBoundedSized, // 94 + LoopContains, // 95 + ControlGroup, // 96 + ControlGroupWhenDefines, // 97 + ControlGroupWhenDefinesDirect, // 98 + ControlGroupWhenType, // 99 + ControlEvaluate, // 100 + ControlDynamicAnchorJump, // 101 + ControlJump // 102 ]; function AssertionTypeArrayBounded_fast(instruction, instance, depth, template, evaluator) { @@ -3873,7 +3911,7 @@ function LoopItemsUnevaluated_fast(instruction, instance, depth, template, evalu function LoopItemsPropertiesExactlyTypeStrictHash_fast(instruction, instance, depth, template, evaluator) { const target = resolveInstance(instance, instruction[2]); - if (!Array.isArray(target)) return false; + if (!Array.isArray(target)) return true; const expectedType = instruction[6][0]; const entries = instruction[6][1][0]; const expectedCount = entries.length; @@ -3946,6 +3984,19 @@ function AssertionTypeIntegerBounded_fast(instruction, instance, depth, template return (typeof target === 'bigint' || Number.isInteger(target)) && target >= range[0] && target <= range[1]; } +function AssertionNumberIntegerBounded_fast(instruction, instance, depth, template, evaluator) { + const target = resolveInstance(instance, instruction[2]); + if (typeof target !== 'number' && typeof target !== 'bigint') return true; + const range = instruction[6]; + return (typeof target === 'bigint' || Number.isInteger(target)) && target >= range[0] && target <= range[1]; +} + +function AssertionNumberType_fast(instruction, instance, depth, template, evaluator) { + const target = resolveInstance(instance, instruction[2]); + if (typeof target !== 'number' && typeof target !== 'bigint') return true; + return numberTypeMatches(target, instruction[6]); +} + function AssertionTypeIntegerBoundedStrict_fast(instruction, instance, depth, template, evaluator) { const target = resolveInstance(instance, instruction[2]); const range = instruction[6]; @@ -3966,35 +4017,35 @@ function AssertionTypeIntegerLowerBoundStrict_fast(instruction, instance, depth, const fastHandlers = handlers.slice(); fastHandlers[16] = AssertionTypeArrayBounded_fast; -fastHandlers[88] = LoopItemsTypeStrictAny_fast; -fastHandlers[43] = AssertionPropertyTypeStrict_fast; +fastHandlers[90] = LoopItemsTypeStrictAny_fast; +fastHandlers[45] = AssertionPropertyTypeStrict_fast; fastHandlers[11] = AssertionTypeStrict_fast; fastHandlers[4] = AssertionDefinesAllStrict_fast; fastHandlers[27] = AssertionEqual_fast; -fastHandlers[66] = LoopPropertiesMatch_fast; -fastHandlers[57] = LogicalOr_fast; -fastHandlers[100] = ControlJump_fast; +fastHandlers[68] = LoopPropertiesMatch_fast; +fastHandlers[59] = LogicalOr_fast; +fastHandlers[102] = ControlJump_fast; fastHandlers[29] = AssertionEqualsAnyStringHash_fast; -fastHandlers[59] = LogicalXor_fast; +fastHandlers[61] = LogicalXor_fast; fastHandlers[2] = AssertionDefinesStrict_fast; -fastHandlers[83] = LoopItems_fast; -fastHandlers[67] = LoopPropertiesMatchClosed_fast; +fastHandlers[85] = LoopItems_fast; +fastHandlers[69] = LoopPropertiesMatchClosed_fast; fastHandlers[14] = AssertionTypeStringBounded_fast; -fastHandlers[58] = LogicalAnd_fast; +fastHandlers[60] = LogicalAnd_fast; fastHandlers[8] = AssertionPropertyDependencies_fast; fastHandlers[10] = AssertionTypeAny_fast; -fastHandlers[60] = LogicalCondition_fast; -fastHandlers[73] = LoopPropertiesExcept_fast; +fastHandlers[62] = LogicalCondition_fast; +fastHandlers[75] = LoopPropertiesExcept_fast; fastHandlers[20] = AssertionRegex_fast; -fastHandlers[68] = LoopProperties_fast; +fastHandlers[70] = LoopProperties_fast; fastHandlers[1] = AssertionDefines_fast; -fastHandlers[61] = LogicalWhenType_fast; -fastHandlers[62] = LogicalWhenDefines_fast; +fastHandlers[63] = LogicalWhenType_fast; +fastHandlers[64] = LogicalWhenDefines_fast; fastHandlers[0] = AssertionFail_fast; -fastHandlers[93] = LoopContains_fast; -fastHandlers[55] = LogicalNot_fast; -fastHandlers[86] = LoopItemsType_fast; -fastHandlers[87] = LoopItemsTypeStrict_fast; +fastHandlers[95] = LoopContains_fast; +fastHandlers[57] = LogicalNot_fast; +fastHandlers[88] = LoopItemsType_fast; +fastHandlers[89] = LoopItemsTypeStrict_fast; fastHandlers[28] = AssertionEqualsAny_fast; fastHandlers[3] = AssertionDefinesAll_fast; fastHandlers[5] = AssertionDefinesExactly_fast; @@ -4023,44 +4074,46 @@ fastHandlers[36] = AssertionTypeIntegerBounded_fast; fastHandlers[37] = AssertionTypeIntegerBoundedStrict_fast; fastHandlers[38] = AssertionTypeIntegerLowerBound_fast; fastHandlers[39] = AssertionTypeIntegerLowerBoundStrict_fast; -fastHandlers[40] = AssertionStringType_fast; -fastHandlers[41] = AssertionPropertyType_fast; -fastHandlers[42] = AssertionPropertyTypeEvaluate_fast; -fastHandlers[44] = AssertionPropertyTypeStrictEvaluate_fast; -fastHandlers[45] = AssertionPropertyTypeStrictAny_fast; -fastHandlers[46] = AssertionPropertyTypeStrictAnyEvaluate_fast; -fastHandlers[47] = AssertionArrayPrefix_fast; -fastHandlers[48] = AssertionArrayPrefixEvaluate_fast; -fastHandlers[49] = AssertionObjectPropertiesSimple_fast; -fastHandlers[50] = AnnotationEmit_fast; -fastHandlers[51] = AnnotationEmitWrapped_fast; -fastHandlers[52] = AnnotationToParent_fast; -fastHandlers[53] = AnnotationBasenameToParent_fast; -fastHandlers[54] = Evaluate_fast; -fastHandlers[56] = LogicalNotEvaluate_fast; -fastHandlers[63] = LogicalWhenArraySizeGreater_fast; -fastHandlers[64] = LoopPropertiesUnevaluated_fast; -fastHandlers[65] = LoopPropertiesUnevaluatedExcept_fast; -fastHandlers[69] = LoopPropertiesEvaluate_fast; -fastHandlers[70] = LoopPropertiesRegex_fast; -fastHandlers[71] = LoopPropertiesRegexClosed_fast; -fastHandlers[72] = LoopPropertiesStartsWith_fast; -fastHandlers[74] = LoopPropertiesType_fast; -fastHandlers[75] = LoopPropertiesTypeEvaluate_fast; -fastHandlers[76] = LoopPropertiesExactlyTypeStrict_fast; -fastHandlers[77] = LoopPropertiesExactlyTypeStrictHash_fast; -fastHandlers[78] = LoopPropertiesTypeStrict_fast; -fastHandlers[79] = LoopPropertiesTypeStrictEvaluate_fast; -fastHandlers[80] = LoopPropertiesTypeStrictAny_fast; -fastHandlers[81] = LoopPropertiesTypeStrictAnyEvaluate_fast; -fastHandlers[82] = LoopKeys_fast; -fastHandlers[84] = LoopItemsFrom_fast; -fastHandlers[85] = LoopItemsUnevaluated_fast; -fastHandlers[89] = LoopItemsPropertiesExactlyTypeStrictHash_fast; -fastHandlers[90] = LoopItemsPropertiesExactlyTypeStrictHash_fast; -fastHandlers[91] = LoopItemsIntegerBounded_fast; -fastHandlers[92] = LoopItemsIntegerBoundedSized_fast; -fastHandlers[99] = ControlDynamicAnchorJump_fast; +fastHandlers[40] = AssertionNumberIntegerBounded_fast; +fastHandlers[41] = AssertionNumberType_fast; +fastHandlers[42] = AssertionStringType_fast; +fastHandlers[43] = AssertionPropertyType_fast; +fastHandlers[44] = AssertionPropertyTypeEvaluate_fast; +fastHandlers[46] = AssertionPropertyTypeStrictEvaluate_fast; +fastHandlers[47] = AssertionPropertyTypeStrictAny_fast; +fastHandlers[48] = AssertionPropertyTypeStrictAnyEvaluate_fast; +fastHandlers[49] = AssertionArrayPrefix_fast; +fastHandlers[50] = AssertionArrayPrefixEvaluate_fast; +fastHandlers[51] = AssertionObjectPropertiesSimple_fast; +fastHandlers[52] = AnnotationEmit_fast; +fastHandlers[53] = AnnotationEmitWrapped_fast; +fastHandlers[54] = AnnotationToParent_fast; +fastHandlers[55] = AnnotationBasenameToParent_fast; +fastHandlers[56] = Evaluate_fast; +fastHandlers[58] = LogicalNotEvaluate_fast; +fastHandlers[65] = LogicalWhenArraySizeGreater_fast; +fastHandlers[66] = LoopPropertiesUnevaluated_fast; +fastHandlers[67] = LoopPropertiesUnevaluatedExcept_fast; +fastHandlers[71] = LoopPropertiesEvaluate_fast; +fastHandlers[72] = LoopPropertiesRegex_fast; +fastHandlers[73] = LoopPropertiesRegexClosed_fast; +fastHandlers[74] = LoopPropertiesStartsWith_fast; +fastHandlers[76] = LoopPropertiesType_fast; +fastHandlers[77] = LoopPropertiesTypeEvaluate_fast; +fastHandlers[78] = LoopPropertiesExactlyTypeStrict_fast; +fastHandlers[79] = LoopPropertiesExactlyTypeStrictHash_fast; +fastHandlers[80] = LoopPropertiesTypeStrict_fast; +fastHandlers[81] = LoopPropertiesTypeStrictEvaluate_fast; +fastHandlers[82] = LoopPropertiesTypeStrictAny_fast; +fastHandlers[83] = LoopPropertiesTypeStrictAnyEvaluate_fast; +fastHandlers[84] = LoopKeys_fast; +fastHandlers[86] = LoopItemsFrom_fast; +fastHandlers[87] = LoopItemsUnevaluated_fast; +fastHandlers[91] = LoopItemsPropertiesExactlyTypeStrictHash_fast; +fastHandlers[92] = LoopItemsPropertiesExactlyTypeStrictHash_fast; +fastHandlers[93] = LoopItemsIntegerBounded_fast; +fastHandlers[94] = LoopItemsIntegerBoundedSized_fast; +fastHandlers[101] = ControlDynamicAnchorJump_fast; import { describe } from './describe.mjs'; diff --git a/vendor/blaze/ports/javascript/opcodes.mjs b/vendor/blaze/ports/javascript/opcodes.mjs index 9fdfa858b..54a1eccf4 100644 --- a/vendor/blaze/ports/javascript/opcodes.mjs +++ b/vendor/blaze/ports/javascript/opcodes.mjs @@ -38,67 +38,69 @@ export const ASSERTION_TYPE_INTEGER_BOUNDED = 36; export const ASSERTION_TYPE_INTEGER_BOUNDED_STRICT = 37; export const ASSERTION_TYPE_INTEGER_LOWER_BOUND = 38; export const ASSERTION_TYPE_INTEGER_LOWER_BOUND_STRICT = 39; -export const ASSERTION_STRING_TYPE = 40; -export const ASSERTION_PROPERTY_TYPE = 41; -export const ASSERTION_PROPERTY_TYPE_EVALUATE = 42; -export const ASSERTION_PROPERTY_TYPE_STRICT = 43; -export const ASSERTION_PROPERTY_TYPE_STRICT_EVALUATE = 44; -export const ASSERTION_PROPERTY_TYPE_STRICT_ANY = 45; -export const ASSERTION_PROPERTY_TYPE_STRICT_ANY_EVALUATE = 46; -export const ASSERTION_ARRAY_PREFIX = 47; -export const ASSERTION_ARRAY_PREFIX_EVALUATE = 48; -export const ASSERTION_OBJECT_PROPERTIES_SIMPLE = 49; -export const ANNOTATION_EMIT = 50; -export const ANNOTATION_EMIT_WRAPPED = 51; -export const ANNOTATION_TO_PARENT = 52; -export const ANNOTATION_BASENAME_TO_PARENT = 53; -export const EVALUATE = 54; -export const LOGICAL_NOT = 55; -export const LOGICAL_NOT_EVALUATE = 56; -export const LOGICAL_OR = 57; -export const LOGICAL_AND = 58; -export const LOGICAL_XOR = 59; -export const LOGICAL_CONDITION = 60; -export const LOGICAL_WHEN_TYPE = 61; -export const LOGICAL_WHEN_DEFINES = 62; -export const LOGICAL_WHEN_ARRAY_SIZE_GREATER = 63; -export const LOOP_PROPERTIES_UNEVALUATED = 64; -export const LOOP_PROPERTIES_UNEVALUATED_EXCEPT = 65; -export const LOOP_PROPERTIES_MATCH = 66; -export const LOOP_PROPERTIES_MATCH_CLOSED = 67; -export const LOOP_PROPERTIES = 68; -export const LOOP_PROPERTIES_EVALUATE = 69; -export const LOOP_PROPERTIES_REGEX = 70; -export const LOOP_PROPERTIES_REGEX_CLOSED = 71; -export const LOOP_PROPERTIES_STARTS_WITH = 72; -export const LOOP_PROPERTIES_EXCEPT = 73; -export const LOOP_PROPERTIES_TYPE = 74; -export const LOOP_PROPERTIES_TYPE_EVALUATE = 75; -export const LOOP_PROPERTIES_EXACTLY_TYPE_STRICT = 76; -export const LOOP_PROPERTIES_EXACTLY_TYPE_STRICT_HASH = 77; -export const LOOP_PROPERTIES_TYPE_STRICT = 78; -export const LOOP_PROPERTIES_TYPE_STRICT_EVALUATE = 79; -export const LOOP_PROPERTIES_TYPE_STRICT_ANY = 80; -export const LOOP_PROPERTIES_TYPE_STRICT_ANY_EVALUATE = 81; -export const LOOP_KEYS = 82; -export const LOOP_ITEMS = 83; -export const LOOP_ITEMS_FROM = 84; -export const LOOP_ITEMS_UNEVALUATED = 85; -export const LOOP_ITEMS_TYPE = 86; -export const LOOP_ITEMS_TYPE_STRICT = 87; -export const LOOP_ITEMS_TYPE_STRICT_ANY = 88; -export const LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH = 89; -export const LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH3 = 90; -export const LOOP_ITEMS_INTEGER_BOUNDED = 91; -export const LOOP_ITEMS_INTEGER_BOUNDED_SIZED = 92; -export const LOOP_CONTAINS = 93; -export const CONTROL_GROUP = 94; -export const CONTROL_GROUP_WHEN_DEFINES = 95; -export const CONTROL_GROUP_WHEN_DEFINES_DIRECT = 96; -export const CONTROL_GROUP_WHEN_TYPE = 97; -export const CONTROL_EVALUATE = 98; -export const CONTROL_DYNAMIC_ANCHOR_JUMP = 99; -export const CONTROL_JUMP = 100; +export const ASSERTION_NUMBER_INTEGER_BOUNDED = 40; +export const ASSERTION_NUMBER_TYPE = 41; +export const ASSERTION_STRING_TYPE = 42; +export const ASSERTION_PROPERTY_TYPE = 43; +export const ASSERTION_PROPERTY_TYPE_EVALUATE = 44; +export const ASSERTION_PROPERTY_TYPE_STRICT = 45; +export const ASSERTION_PROPERTY_TYPE_STRICT_EVALUATE = 46; +export const ASSERTION_PROPERTY_TYPE_STRICT_ANY = 47; +export const ASSERTION_PROPERTY_TYPE_STRICT_ANY_EVALUATE = 48; +export const ASSERTION_ARRAY_PREFIX = 49; +export const ASSERTION_ARRAY_PREFIX_EVALUATE = 50; +export const ASSERTION_OBJECT_PROPERTIES_SIMPLE = 51; +export const ANNOTATION_EMIT = 52; +export const ANNOTATION_EMIT_WRAPPED = 53; +export const ANNOTATION_TO_PARENT = 54; +export const ANNOTATION_BASENAME_TO_PARENT = 55; +export const EVALUATE = 56; +export const LOGICAL_NOT = 57; +export const LOGICAL_NOT_EVALUATE = 58; +export const LOGICAL_OR = 59; +export const LOGICAL_AND = 60; +export const LOGICAL_XOR = 61; +export const LOGICAL_CONDITION = 62; +export const LOGICAL_WHEN_TYPE = 63; +export const LOGICAL_WHEN_DEFINES = 64; +export const LOGICAL_WHEN_ARRAY_SIZE_GREATER = 65; +export const LOOP_PROPERTIES_UNEVALUATED = 66; +export const LOOP_PROPERTIES_UNEVALUATED_EXCEPT = 67; +export const LOOP_PROPERTIES_MATCH = 68; +export const LOOP_PROPERTIES_MATCH_CLOSED = 69; +export const LOOP_PROPERTIES = 70; +export const LOOP_PROPERTIES_EVALUATE = 71; +export const LOOP_PROPERTIES_REGEX = 72; +export const LOOP_PROPERTIES_REGEX_CLOSED = 73; +export const LOOP_PROPERTIES_STARTS_WITH = 74; +export const LOOP_PROPERTIES_EXCEPT = 75; +export const LOOP_PROPERTIES_TYPE = 76; +export const LOOP_PROPERTIES_TYPE_EVALUATE = 77; +export const LOOP_PROPERTIES_EXACTLY_TYPE_STRICT = 78; +export const LOOP_PROPERTIES_EXACTLY_TYPE_STRICT_HASH = 79; +export const LOOP_PROPERTIES_TYPE_STRICT = 80; +export const LOOP_PROPERTIES_TYPE_STRICT_EVALUATE = 81; +export const LOOP_PROPERTIES_TYPE_STRICT_ANY = 82; +export const LOOP_PROPERTIES_TYPE_STRICT_ANY_EVALUATE = 83; +export const LOOP_KEYS = 84; +export const LOOP_ITEMS = 85; +export const LOOP_ITEMS_FROM = 86; +export const LOOP_ITEMS_UNEVALUATED = 87; +export const LOOP_ITEMS_TYPE = 88; +export const LOOP_ITEMS_TYPE_STRICT = 89; +export const LOOP_ITEMS_TYPE_STRICT_ANY = 90; +export const LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH = 91; +export const LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH3 = 92; +export const LOOP_ITEMS_INTEGER_BOUNDED = 93; +export const LOOP_ITEMS_INTEGER_BOUNDED_SIZED = 94; +export const LOOP_CONTAINS = 95; +export const CONTROL_GROUP = 96; +export const CONTROL_GROUP_WHEN_DEFINES = 97; +export const CONTROL_GROUP_WHEN_DEFINES_DIRECT = 98; +export const CONTROL_GROUP_WHEN_TYPE = 99; +export const CONTROL_EVALUATE = 100; +export const CONTROL_DYNAMIC_ANCHOR_JUMP = 101; +export const CONTROL_JUMP = 102; export const INSTRUCTION_NAMES = { "AssertionFail": ASSERTION_FAIL, @@ -141,6 +143,8 @@ export const INSTRUCTION_NAMES = { "AssertionTypeIntegerBoundedStrict": ASSERTION_TYPE_INTEGER_BOUNDED_STRICT, "AssertionTypeIntegerLowerBound": ASSERTION_TYPE_INTEGER_LOWER_BOUND, "AssertionTypeIntegerLowerBoundStrict": ASSERTION_TYPE_INTEGER_LOWER_BOUND_STRICT, + "AssertionNumberIntegerBounded": ASSERTION_NUMBER_INTEGER_BOUNDED, + "AssertionNumberType": ASSERTION_NUMBER_TYPE, "AssertionStringType": ASSERTION_STRING_TYPE, "AssertionPropertyType": ASSERTION_PROPERTY_TYPE, "AssertionPropertyTypeEvaluate": ASSERTION_PROPERTY_TYPE_EVALUATE, diff --git a/vendor/blaze/schemas/canonical-draft3.json b/vendor/blaze/schemas/canonical-draft3.json index 283acb2b8..c269051b6 100644 --- a/vendor/blaze/schemas/canonical-draft3.json +++ b/vendor/blaze/schemas/canonical-draft3.json @@ -19,20 +19,6 @@ ], "$ref": "#/$defs/document", "$defs": { - "metadata": { - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "default": true - }, - "patternProperties": { - "^x-": true - } - }, "document": { "x-lint-exclude": "simple_properties_identifiers", "type": "object", @@ -46,11 +32,12 @@ "maxItems": 1, "minItems": 1, "items": { - "$ref": "#/$defs/entry" + "$ref": "#/$defs/edge" } }, "definitions": { "type": "object", + "minProperties": 1, "propertyNames": { "pattern": "^(0|[1-9][0-9]*)$" }, @@ -59,20 +46,9 @@ } } }, - "unevaluatedProperties": false + "additionalProperties": false }, "edge": { - "x-lint-exclude": "simple_properties_identifiers", - "type": "object", - "required": [ "$ref" ], - "properties": { - "$ref": { - "type": "string" - } - }, - "unevaluatedProperties": false - }, - "entry": { "x-lint-exclude": "simple_properties_identifiers", "type": "object", "required": [ "$ref" ], @@ -82,7 +58,7 @@ "pattern": "^#/definitions/(0|[1-9][0-9]*)$" } }, - "unevaluatedProperties": false + "additionalProperties": false }, "subproperty": { "anyOf": [ @@ -105,7 +81,7 @@ } } }, - "unevaluatedProperties": false + "additionalProperties": false } ] }, @@ -119,7 +95,6 @@ }, { "x-lint-exclude": "simple_properties_identifiers", - "$ref": "#/$defs/metadata", "type": "object", "required": [ "$ref" ], "properties": { @@ -127,10 +102,9 @@ "type": "string" } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "enum" ], "properties": { @@ -172,10 +146,9 @@ "uniqueItems": true } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "type", "minLength" ], "properties": { @@ -197,10 +170,9 @@ "type": "string" } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "type", "divisibleBy" ], "properties": { @@ -218,10 +190,9 @@ "type": "number" } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "type" ], "properties": { @@ -245,10 +216,9 @@ "type": "boolean" } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "type", @@ -276,10 +246,9 @@ "$ref": "#/$defs/edge" } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "type", "items", "minItems", "uniqueItems" ], "properties": { @@ -301,10 +270,9 @@ "type": "boolean" } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "type", @@ -339,10 +307,9 @@ "type": "boolean" } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "type" ], "properties": { @@ -354,10 +321,9 @@ } } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "extends" ], "properties": { @@ -369,10 +335,9 @@ } } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "disallow" ], "properties": { @@ -385,7 +350,7 @@ } } }, - "unevaluatedProperties": false + "additionalProperties": false } ] } diff --git a/vendor/blaze/src/alterschema/schema_helpers.h b/vendor/blaze/src/alterschema/schema_helpers.h index 736fd8756..cd8848d26 100644 --- a/vendor/blaze/src/alterschema/schema_helpers.h +++ b/vendor/blaze/src/alterschema/schema_helpers.h @@ -29,6 +29,7 @@ inline auto parse_schema_type_string(const sourcemeta::core::JSON::String &type, } else if (type == "number") { result.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); result.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); + result.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); } else if (type == "integer") { result.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); } else if (type == "string") { diff --git a/vendor/blaze/src/alterschema/wrap.cc b/vendor/blaze/src/alterschema/wrap.cc index 2b65fca3c..63eeeea93 100644 --- a/vendor/blaze/src/alterschema/wrap.cc +++ b/vendor/blaze/src/alterschema/wrap.cc @@ -80,7 +80,7 @@ auto wrap(const sourcemeta::core::JSON &schema, sourcemeta::core::URI uri{identifier}; try { - schema_reidentify(copy, identifier, resolver, location.dialect); + schema_reidentify(copy, identifier, walker, resolver, location.dialect); // Otherwise we will get an error with the `WRAPPER_IDENTIFIER`, which will // be confusing to end users diff --git a/vendor/blaze/src/canonicalizer/CMakeLists.txt b/vendor/blaze/src/canonicalizer/CMakeLists.txt index e22d6c208..81dd3e87b 100644 --- a/vendor/blaze/src/canonicalizer/CMakeLists.txt +++ b/vendor/blaze/src/canonicalizer/CMakeLists.txt @@ -83,6 +83,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME canonicalizer rules/maximum_can_equal_integer_fold.h rules/maximum_can_equal_true_drop.h rules/maximum_real_for_integer.h + rules/metadata_keywords_drop.h rules/min_contains_without_contains.h rules/min_items_given_min_contains.h rules/min_length_implicit.h @@ -122,6 +123,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME canonicalizer rules/type_with_applicator_to_extends.h rules/unevaluated_items_to_items.h rules/unevaluated_properties_to_additional_properties.h + rules/unknown_keywords_drop.h rules/unknown_keywords_prefix.h rules/unknown_local_ref.h rules/unnecessary_allof_ref_wrapper_draft.h diff --git a/vendor/blaze/src/canonicalizer/canonicalize.cc b/vendor/blaze/src/canonicalizer/canonicalize.cc index a9653c0b8..55bf51853 100644 --- a/vendor/blaze/src/canonicalizer/canonicalize.cc +++ b/vendor/blaze/src/canonicalizer/canonicalize.cc @@ -405,9 +405,13 @@ auto edge_reference(const sourcemeta::core::JSON &body) return nullptr; } -/// Whether the given body is an edge rather than a node +/// Whether the given body is an edge rather than a node. Only a reference +/// that stays inside the document can reach an entry, so one that leaves it +/// becomes an entry of its own and every edge really is an edge of the graph auto is_edge(const sourcemeta::core::JSON &body) -> bool { - return edge_reference(body) != nullptr; + const auto *reference{edge_reference(body)}; + return reference != nullptr && reference->is_string() && + reference->to_string().starts_with('#'); } /// What a subschema says, with the keywords that carry the graph itself taken @@ -423,13 +427,6 @@ auto without_containers(sourcemeta::core::JSON body) -> sourcemeta::core::JSON { return body; } -/// Where following a chain of edges ends up: an entry of the graph, or a -/// reference that leaves the document. A chain that only ever reaches other -/// edges reaches neither, and cannot be expressed as a graph at all -struct Destination { - std::optional entry; - std::optional outside; -}; /// Every place inside a node body where a reference can sit, as a pointer to /// the `$ref` itself. /// @@ -526,8 +523,6 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, // Every reference the document declares, before edges are followed through std::vector> links; std::vector> reference_targets; - // References whose chain ends outside this document keep their own URI - std::vector> outside_targets; { const core::SchemaFrame frame{core::SchemaFrame::Mode::References, @@ -538,15 +533,13 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, default_id, core::SchemaFrame::IdentifierMode::Fallback}; - // Hyper-schema is left out, unlike identifier elimination above. Its - // `links` hold link descriptions rather than subschemas, so lifting them - // into `definitions` and referring to them would say something the - // document never said. The canonical form this pass produces is the one - // `canonical-draft3.json` describes, and that is Draft 3 proper + // Hyper-schema comes this far too. Nothing its vocabulary declares takes + // part in whether an instance is valid, so the rules have dropped all of + // it by now, and what is left is Draft 3 proper however the document + // spells its dialect if (frame.any_subschema( [](const core::SchemaFrame::Location &location) -> bool { - return location.base_dialect != - core::SchemaBaseDialect::JSON_SCHEMA_DRAFT_3; + return !is_draft3(location.base_dialect); })) { return; } @@ -598,14 +591,13 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, static const core::JSON::String REF_KEYWORD{"$ref"}; static const core::JSON::String EXTENDS_KEYWORD{"extends"}; - const auto resolve{[&nodes, &links, - &schema](const core::Pointer &start) -> Destination { + const auto resolve{[&nodes, &links](const core::Pointer &start) + -> std::optional { auto target{start}; for (std::size_t hop = 0; hop <= links.size(); hop++) { const auto match{std::ranges::find(nodes, target)}; if (match != nodes.end()) { - return {.entry = static_cast(match - nodes.begin()), - .outside = std::nullopt}; + return static_cast(match - nodes.begin()); } const auto direct{ @@ -629,43 +621,36 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, continue; } - // Nothing in this document takes the chain any further, so it ends at an - // edge that points out of it. Whatever referred here has to point there - // too, since the edge itself is about to go away - // Held in a named value: the reference points into it, so it has to - // outlive the check below - const auto subschema{without_containers(core::get(schema, target))}; - const auto *outside{edge_reference(subschema)}; - // Only a reference that leaves the document can be carried over. One - // that stays inside it names a subschema this pass is about to take - // away, and a `#/definitions/...` pointer would then be read back as - // one of the entries this pass itself named - if (outside != nullptr && outside->is_string() && - !outside->to_string().starts_with('#')) { - return {.entry = std::nullopt, .outside = outside->to_string()}; - } - break; } - return {.entry = std::nullopt, .outside = std::nullopt}; + return std::nullopt; }}; + // A reference whose chain goes round in circles without ever reaching a + // subschema that says anything never gets to assert anything either, so it + // is the empty schema written the long way round. Each one is replaced by an + // empty schema and the pass starts over, since the document it framed has + // changed underneath it. Every round takes a reference away, so this ends + std::vector vacuous; for (const auto &entry : links) { const auto destination{resolve(entry.second)}; - if (destination.entry.has_value()) { - reference_targets.emplace_back(entry.first, destination.entry.value()); - } else if (destination.outside.has_value()) { - outside_targets.emplace_back(entry.first, destination.outside.value()); + if (destination.has_value()) { + reference_targets.emplace_back(entry.first, destination.value()); } else { - // The chain goes round in circles without ever reaching a subschema that - // says anything. There is no entry for such a reference to point at, and - // dropping it would change what the document means, so the graph form - // does not apply here - return; + vacuous.push_back(entry.first.initial()); } } + if (!vacuous.empty()) { + for (const auto &pointer : vacuous) { + core::set(schema, pointer, sourcemeta::core::JSON::make_object()); + } + + lift_subschemas(schema, walker, resolver, default_dialect, default_id); + return; + } + // The innermost node that contains the given pointer, if any const auto enclosing{ [&nodes](const core::Pointer &pointer) -> std::optional { @@ -711,7 +696,7 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, return wrapper; }}; - const auto entry_node{resolve(core::Pointer{}).entry}; + const auto entry_node{resolve(core::Pointer{})}; if (!entry_node.has_value()) { return; } @@ -748,18 +733,6 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, std::to_string(entry.second)}); } - // A reference that ends up outside the document is written out in full, - // since the edges it used to travel through are not entries of the graph - for (const auto &entry : outside_targets) { - if (!entry.first.starts_with(pointer) || - enclosing(entry.first.initial()) != index) { - continue; - } - - core::set(body, entry.first.resolve_from(pointer), - sourcemeta::core::JSON{entry.second}); - } - if (body.is_object()) { body.erase("$schema"); body.erase("required"); @@ -1011,11 +984,16 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, } }}; + // The entries are written in the order they are numbered, so that the same + // graph always comes out spelled the same way. Following the order the + // groups were named in would instead follow where the nodes came from, and + // a document already in graph form names its nodes after the entries it + // holds, leaving `10` ahead of `2` auto merged{sourcemeta::core::JSON::make_object()}; - for (const auto &group : position) { - auto body{relabel(bodies.at(representative.at(group.first)))}; + for (std::size_t index = 0; index < walked.size(); index++) { + auto body{relabel(bodies.at(representative.at(walked.at(index))))}; unique(body); - merged.assign(std::to_string(group.second), std::move(body)); + merged.assign(std::to_string(index), std::move(body)); } const auto next{position.at(classes.at(collapsed_entry))}; @@ -1110,6 +1088,7 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, #include "rules/flatten_nested_allof.h" #include "rules/flatten_nested_anyof.h" #include "rules/flatten_nested_extends.h" +#include "rules/hyper_schema_keywords_drop.h" #include "rules/if_then_else_implicit.h" #include "rules/if_without_then_else.h" #include "rules/ignored_metaschema.h" @@ -1123,6 +1102,7 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, #include "rules/maximum_can_equal_integer_fold.h" #include "rules/maximum_can_equal_true_drop.h" #include "rules/maximum_real_for_integer.h" +#include "rules/metadata_keywords_drop.h" #include "rules/min_contains_without_contains.h" #include "rules/min_items_given_min_contains.h" #include "rules/min_length_implicit.h" @@ -1162,6 +1142,7 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, #include "rules/type_with_applicator_to_extends.h" #include "rules/unevaluated_items_to_items.h" #include "rules/unevaluated_properties_to_additional_properties.h" +#include "rules/unknown_keywords_drop.h" #include "rules/unknown_keywords_prefix.h" #include "rules/unknown_local_ref.h" #include "rules/unknown_type_names.h" @@ -1192,6 +1173,8 @@ auto canonicalize(sourcemeta::core::JSON &schema, rules.push_back(make_rule()); rules.push_back(make_rule()); rules.push_back(make_rule()); + rules.push_back(make_rule()); + rules.push_back(make_rule()); rules.push_back(make_rule()); rules.push_back(make_rule()); rules.push_back(make_rule()); @@ -1258,6 +1241,7 @@ auto canonicalize(sourcemeta::core::JSON &schema, rules.push_back(make_rule()); rules.push_back(make_rule()); rules.push_back(make_rule()); + rules.push_back(make_rule()); rules.push_back(make_rule()); rules.push_back(make_rule()); rules.push_back(make_rule()); diff --git a/vendor/blaze/src/canonicalizer/helpers.h b/vendor/blaze/src/canonicalizer/helpers.h index aebd2821a..416fb1eed 100644 --- a/vendor/blaze/src/canonicalizer/helpers.h +++ b/vendor/blaze/src/canonicalizer/helpers.h @@ -13,11 +13,126 @@ inline auto is_in_place_applicator(const SchemaKeywordType type) -> bool { type == SchemaKeywordType::ApplicatorValueInPlaceNegate; } -// Walk up from a schema location, continuing as long as the traversal -// predicate returns true for each keyword type encountered. Returns a -// reference to the pointer of the ancestor where the match callback returned -// true, or nullopt if no match was found or the traversal predicate stopped -// the walk. +// TODO: Move upstream, so that the compiler can share this predicate rather +// than repeating the same check +// Draft 3 and Draft 4 read "integer" as a number written without a fractional +// part, so `3.0` is not one there, and `type: "integer"` says something that +// an `enum` of integral values cannot. Draft 6 onwards widened it to any +// number whose fractional part is zero, and the dialects before Draft 3 read +// it that way too. Kept in step with `integral_reals_are_integers` in the +// compiler, which decides the same question when evaluating, other than the +// OpenAPI 3.0 Schema Object that the compiler's copy also names. No rule here +// reaches that dialect, as every one of them asks for a JSON Schema +// vocabulary, so adding it would be a line no test could hold. A rule that +// does come to reach it needs it here too +inline auto integral_reals_are_integers(const SchemaVocabularies &vocabularies) + -> bool { + return !vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4_HYPER}); +} + +// Whether an instance of a type the schema admits can equal this value. +// Equality reads a number by its value, so an integral number matches however +// it was written, which makes this a different question from whether the value +// is itself of an admitted type: an option of `1.0` is what an instance of `1` +// equals, and that instance is an integer in every dialect +inline auto type_can_equal(const sourcemeta::core::JSON::TypeSet &types, + const sourcemeta::core::JSON &value) -> bool { + using Type = sourcemeta::core::JSON::Type; + if (!value.is_number()) { + return types.test(std::to_underlying(value.type())); + } + + return (value.is_integral() && + types.test(std::to_underlying(Type::Integer))) || + types.test(std::to_underlying(Type::Real)) || + types.test(std::to_underlying(Type::Decimal)); +} + +// Whether every instance that can equal this value is of a type the schema +// admits, which is what leaves `type` with nothing to say that the value does +// not. Draft 3 and Draft 4 do not read a number written with a fractional part +// as an integer, so `type: "integer"` still turns away the `1.0` and the `1e0` +// that an option of `1` lets through +inline auto type_always_equals(const sourcemeta::core::JSON::TypeSet &types, + const sourcemeta::core::JSON &value, + const SchemaVocabularies &vocabularies) -> bool { + using Type = sourcemeta::core::JSON::Type; + if (!value.is_number()) { + return types.test(std::to_underlying(value.type())); + } + + // A number written with a fractional or an exponent part is stored as a real + // or as a decimal, so a type that names every number covers each spelling, + // and only an integral value also has an integer one to cover + if (types.test(std::to_underlying(Type::Real)) && + types.test(std::to_underlying(Type::Decimal))) { + return !value.is_integral() || + types.test(std::to_underlying(Type::Integer)); + } + + return value.is_integral() && types.test(std::to_underlying(Type::Integer)) && + integral_reals_are_integers(vocabularies); +} + +// Whether the `type` of a schema says nothing its `enum` options do not, which +// is what lets the type go. Draft 3 reads a bare `any` and an empty subschema +// as holding for every instance, so neither says anything, while a subschema +// this cannot read may say something the options do not +inline auto type_is_implied_by_enum(const sourcemeta::core::JSON &type, + const sourcemeta::core::JSON &options, + const SchemaVocabularies &vocabularies) + -> bool { + if (vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER})) { + if (type.is_string() && type.to_string() == "any") { + return true; + } + + if (type.is_array()) { + bool has_tautology{false}; + bool has_unknown_subschema{false}; + for (const auto &entry : type.as_array()) { + if (entry.is_string() && entry.to_string() == "any") { + has_tautology = true; + break; + } + + if (entry.is_object()) { + if (entry.empty()) { + has_tautology = true; + break; + } + + has_unknown_subschema = true; + } + } + + if (has_tautology) { + return true; + } + + if (has_unknown_subschema) { + return false; + } + } + } + + const auto types{parse_schema_type(type)}; + if (!types.any()) { + return false; + } + + return std::ranges::all_of( + options.as_array(), [&types, &vocabularies](const auto &option) -> bool { + return type_always_equals(types, option, vocabularies); + }); +} + // Whether a `type` value only consists of simple type names that can be // parsed into a complete set of instance types. Draft 0 to Draft 3 unions // may contain subschemas or `any`, in which case the parsed set is an @@ -119,6 +234,11 @@ inline auto into_unsatisfiable(sourcemeta::core::JSON &schema, } } +// Walk up from a schema location, continuing as long as the traversal +// predicate returns true for each keyword type encountered. Returns a +// reference to the pointer of the ancestor where the match callback returned +// true, or nullopt if no match was found or the traversal predicate stopped +// the walk. template auto walk_up(const sourcemeta::core::JSON &root, const SchemaFrame &frame, const SchemaFrame::Location &location, const SchemaWalker &walker, @@ -170,6 +290,105 @@ auto walk_up_in_place_applicators(const sourcemeta::core::JSON &root, is_in_place_applicator, matches); } +// Walk up the in-place applicator chain of a location once, reporting whether +// any ancestor matches and collecting every scope the walk passes through on +// the way. A reference landing on any of those scopes is one that reaches this +// location without moving through the instance, which is what makes them worth +// collecting. The two answers come from the same walk because the chain and +// the predicate that bounds it are the same either way +template +auto walk_up_in_place_applicators_collecting_scopes( + const sourcemeta::core::JSON &root, const SchemaFrame &frame, + const SchemaFrame::Location &location, const SchemaWalker &walker, + const SchemaResolver &resolver, const MatchCallback &matches, + std::set &scopes) -> bool { + scopes.insert(location.pointer); + auto current_pointer{location.pointer}; + auto current_parent{location.parent}; + + while (current_parent.has_value()) { + const auto &parent_pointer{current_parent.value()}; + const auto relative_pointer{current_pointer.resolve_from(parent_pointer)}; + assert(!relative_pointer.empty() && relative_pointer.at(0).is_property()); + const auto parent{frame.traverse(parent_pointer)}; + assert(parent.has_value()); + const auto &parent_vocabularies{ + frame.vocabularies(parent.value().get(), resolver)}; + if (!is_in_place_applicator( + walker(relative_pointer.at(0).to_property(), parent_vocabularies) + .type)) { + return false; + } + + if (matches(sourcemeta::core::get(root, parent_pointer), + parent_vocabularies)) { + return true; + } + + scopes.insert(parent_pointer); + current_pointer = parent_pointer; + current_parent = parent.value().get().parent; + } + + return false; +} + +// The lexical walk stops at a subschema nothing encloses, which is what a +// definition under `$defs` looks like. Whichever scope references that +// definition still sees the annotations it produces, so a keyword that reads +// them, such as `unevaluatedItems`, can sit on the far side of the reference. +// This repeats the walk from every scope that reaches this location through a +// reference, so such a keyword is found wherever it hides +template +auto walk_up_in_place_applicators_across_references( + const sourcemeta::core::JSON &root, const SchemaFrame &frame, + const SchemaFrame::Location &location, const SchemaWalker &walker, + const SchemaResolver &resolver, const MatchCallback &matches) -> bool { + std::vector> pending{ + std::cref(location)}; + std::set visited{location.pointer}; + + while (!pending.empty()) { + const auto ¤t{pending.back().get()}; + pending.pop_back(); + + // The caller already checked the location it asked about, but a scope we + // arrived at through a reference still has to be checked on its own + if (current.pointer != location.pointer && + matches(sourcemeta::core::get(root, current.pointer), + frame.vocabularies(current, resolver))) { + return true; + } + + std::set scopes; + if (walk_up_in_place_applicators_collecting_scopes( + root, frame, current, walker, resolver, matches, scopes)) { + return true; + } + + frame.for_each_reference( + [&](const SchemaReferenceType, + const sourcemeta::core::WeakPointer &origin, + const SchemaFrame::Reference &reference) -> void { + const auto destination{frame.traverse(reference.destination)}; + if (!destination.has_value() || + !scopes.contains(destination.value().get().pointer)) { + return; + } + + const auto source{frame.traverse(origin.initial())}; + if (!source.has_value() || + !visited.insert(source.value().get().pointer).second) { + return; + } + + pending.emplace_back(std::cref(source.value().get())); + }); + } + + return false; +} + #define ONLY_CONTINUE_IF(condition) \ if (!(condition)) { \ return false; \ diff --git a/vendor/blaze/src/canonicalizer/rules/additional_items_implicit.h b/vendor/blaze/src/canonicalizer/rules/additional_items_implicit.h index f4fd2dad8..0c62c928d 100644 --- a/vendor/blaze/src/canonicalizer/rules/additional_items_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/additional_items_implicit.h @@ -14,6 +14,7 @@ class AdditionalItemsImplicit final : public SchemaTransformRule { const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7}) && @@ -24,8 +25,9 @@ class AdditionalItemsImplicit final : public SchemaTransformRule { const auto *items{schema.try_at("items")}; ONLY_CONTINUE_IF(items && items->is_array() && !schema.defines("additionalItems")); - this->is_draft3_ = - vocabularies.contains(SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3); + this->is_draft3_ = vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}); return true; } diff --git a/vendor/blaze/src/canonicalizer/rules/dependencies_to_extends_disallow.h b/vendor/blaze/src/canonicalizer/rules/dependencies_to_extends_disallow.h index 8f08ecb59..feab48824 100644 --- a/vendor/blaze/src/canonicalizer/rules/dependencies_to_extends_disallow.h +++ b/vendor/blaze/src/canonicalizer/rules/dependencies_to_extends_disallow.h @@ -13,7 +13,9 @@ class DependenciesToExtendsDisallow final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( - vocabularies.contains(SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3) && + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && schema.is_object()); const auto *dependencies{schema.try_at("dependencies")}; diff --git a/vendor/blaze/src/canonicalizer/rules/disallow_to_array_of_schemas.h b/vendor/blaze/src/canonicalizer/rules/disallow_to_array_of_schemas.h index 479037c78..e3bbaae41 100644 --- a/vendor/blaze/src/canonicalizer/rules/disallow_to_array_of_schemas.h +++ b/vendor/blaze/src/canonicalizer/rules/disallow_to_array_of_schemas.h @@ -12,15 +12,18 @@ class DisallowToArrayOfSchemas final : public SchemaTransformRule { const sourcemeta::core::SchemaFrame::Location &, const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { - ONLY_CONTINUE_IF(vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3}) && - schema.is_object() && schema.defines("disallow")); + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object() && schema.defines("disallow")); - this->convert_to_schemas_ = - vocabularies.contains(SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3); + this->convert_to_schemas_ = vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}); const auto &disallow{schema.at("disallow")}; if (disallow.is_string()) { diff --git a/vendor/blaze/src/canonicalizer/rules/divisible_by_implicit.h b/vendor/blaze/src/canonicalizer/rules/divisible_by_implicit.h index f7705e93a..72a4aacad 100644 --- a/vendor/blaze/src/canonicalizer/rules/divisible_by_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/divisible_by_implicit.h @@ -11,10 +11,12 @@ class DivisibleByImplicit final : public SchemaTransformRule { const sourcemeta::core::SchemaFrame::Location &, const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { - ONLY_CONTINUE_IF(vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3}) && - schema.is_object() && !schema.defines("divisibleBy")); + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object() && !schema.defines("divisibleBy")); const auto *type{schema.try_at("type")}; ONLY_CONTINUE_IF(type && type->is_string() && diff --git a/vendor/blaze/src/canonicalizer/rules/draft3_type_any.h b/vendor/blaze/src/canonicalizer/rules/draft3_type_any.h index 581a555d8..96fe86977 100644 --- a/vendor/blaze/src/canonicalizer/rules/draft3_type_any.h +++ b/vendor/blaze/src/canonicalizer/rules/draft3_type_any.h @@ -12,7 +12,9 @@ class Draft3TypeAny final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( - vocabularies.contains(SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3) && + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && schema.is_object()); const auto *type{schema.try_at("type")}; diff --git a/vendor/blaze/src/canonicalizer/rules/empty_dependencies_drop.h b/vendor/blaze/src/canonicalizer/rules/empty_dependencies_drop.h index 45a262ec8..875b22083 100644 --- a/vendor/blaze/src/canonicalizer/rules/empty_dependencies_drop.h +++ b/vendor/blaze/src/canonicalizer/rules/empty_dependencies_drop.h @@ -13,6 +13,7 @@ class EmptyDependenciesDrop final : public SchemaTransformRule { const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7}) && diff --git a/vendor/blaze/src/canonicalizer/rules/enum_drop_redundant_validation.h b/vendor/blaze/src/canonicalizer/rules/enum_drop_redundant_validation.h index 75615cec4..14d721f73 100644 --- a/vendor/blaze/src/canonicalizer/rules/enum_drop_redundant_validation.h +++ b/vendor/blaze/src/canonicalizer/rules/enum_drop_redundant_validation.h @@ -23,11 +23,18 @@ class EnumDropRedundantValidation final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_2019_09_VALIDATION, SchemaVocabularies::Known::JSON_SCHEMA_2020_12_VALIDATION}) && - schema.is_object() && !schema.defines("type")); + schema.is_object()); const auto *enum_value{schema.try_at("enum")}; ONLY_CONTINUE_IF(enum_value && enum_value->is_array()); + // `enum_with_type` takes away a `type` the options already imply, so one + // that is still here says something they do not, and belongs in a branch + // of its own rather than beside them + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type == nullptr || !type_is_implied_by_enum( + *type, *enum_value, vocabularies)); + this->keywords_.clear(); this->wrap_keywords_.clear(); this->is_pre_draft4_ = vocabularies.contains_any( diff --git a/vendor/blaze/src/canonicalizer/rules/enum_filter_by_type.h b/vendor/blaze/src/canonicalizer/rules/enum_filter_by_type.h index c5d34f0b9..138178b08 100644 --- a/vendor/blaze/src/canonicalizer/rules/enum_filter_by_type.h +++ b/vendor/blaze/src/canonicalizer/rules/enum_filter_by_type.h @@ -17,6 +17,7 @@ class EnumFilterByType final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, @@ -32,23 +33,12 @@ class EnumFilterByType final : public SchemaTransformRule { const auto declared_types{parse_schema_type(*type)}; ONLY_CONTINUE_IF(declared_types.any()); - const bool integer_matches_integral{ - vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, - SchemaVocabularies::Known::JSON_SCHEMA_2019_09_VALIDATION, - SchemaVocabularies::Known::JSON_SCHEMA_2020_12_VALIDATION}) && - declared_types.test( - std::to_underlying(sourcemeta::core::JSON::Type::Integer))}; this->matching_indices_.clear(); bool has_mismatch{false}; std::size_t index{0}; for (const auto &value : enum_value->as_array()) { - const bool matches{ - declared_types.test(std::to_underlying(value.type())) || - (integer_matches_integral && value.is_integral())}; - if (matches) { + if (type_can_equal(declared_types, value)) { this->matching_indices_.push_back(index); } else { has_mismatch = true; diff --git a/vendor/blaze/src/canonicalizer/rules/enum_split_by_type.h b/vendor/blaze/src/canonicalizer/rules/enum_split_by_type.h index 1d32f627a..2570419a0 100644 --- a/vendor/blaze/src/canonicalizer/rules/enum_split_by_type.h +++ b/vendor/blaze/src/canonicalizer/rules/enum_split_by_type.h @@ -28,7 +28,8 @@ class EnumSplitByType final : public SchemaTransformRule { {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3})}; + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER})}; ONLY_CONTINUE_IF((any_of_dialect || type_union_dialect) && schema.is_object()); diff --git a/vendor/blaze/src/canonicalizer/rules/enum_with_type.h b/vendor/blaze/src/canonicalizer/rules/enum_with_type.h index 60ba163e9..bf1db2604 100644 --- a/vendor/blaze/src/canonicalizer/rules/enum_with_type.h +++ b/vendor/blaze/src/canonicalizer/rules/enum_with_type.h @@ -27,54 +27,7 @@ class EnumWithType final : public SchemaTransformRule { const auto *enum_value{schema.try_at("enum")}; ONLY_CONTINUE_IF(enum_value && enum_value->is_array()); - if (vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER})) { - if (type->is_string() && type->to_string() == "any") { - return true; - } - - if (type->is_array()) { - bool has_tautology{false}; - bool has_unknown_subschema{false}; - for (const auto &entry : type->as_array()) { - if (entry.is_string() && entry.to_string() == "any") { - has_tautology = true; - break; - } - if (entry.is_object()) { - if (entry.empty()) { - has_tautology = true; - break; - } - has_unknown_subschema = true; - } - } - - if (has_tautology) { - return true; - } - - if (has_unknown_subschema) { - return false; - } - } - } - - const auto current_types{parse_schema_type(*type)}; - ONLY_CONTINUE_IF(current_types.any()); - const bool integer_matches_integral{ - vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7}) && - current_types.test( - std::to_underlying(sourcemeta::core::JSON::Type::Integer))}; - ONLY_CONTINUE_IF(std::ranges::all_of( - enum_value->as_array(), - [¤t_types, integer_matches_integral](const auto &item) -> auto { - return current_types.test(std::to_underlying(item.type())) || - (integer_matches_integral && item.is_integral()); - })); + ONLY_CONTINUE_IF(type_is_implied_by_enum(*type, *enum_value, vocabularies)); return true; } diff --git a/vendor/blaze/src/canonicalizer/rules/equal_numeric_bounds_to_enum.h b/vendor/blaze/src/canonicalizer/rules/equal_numeric_bounds_to_enum.h index b377741a6..78d5a161a 100644 --- a/vendor/blaze/src/canonicalizer/rules/equal_numeric_bounds_to_enum.h +++ b/vendor/blaze/src/canonicalizer/rules/equal_numeric_bounds_to_enum.h @@ -21,10 +21,17 @@ class EqualNumericBoundsToEnum final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0}) && schema.is_object()); + // An integer only where the dialect reads one as any number whose + // fractional part is zero. Draft 3 and Draft 4 read it as a number written + // without a fractional part, so there `type: "integer"` rejects `3.0` + // where `enum: [ 3 ]` accepts it, the two being equal as numbers. Keeping + // the type beside the enum is not an option either, as the canonical form + // has no entry that carries both const auto *type{schema.try_at("type")}; - ONLY_CONTINUE_IF( - type && type->is_string() && - (type->to_string() == "integer" || type->to_string() == "number")); + ONLY_CONTINUE_IF(type && type->is_string() && + (type->to_string() == "number" || + (type->to_string() == "integer" && + integral_reals_are_integers(vocabularies)))); const auto *minimum{schema.try_at("minimum")}; ONLY_CONTINUE_IF(minimum && minimum->is_number()); const auto *maximum{schema.try_at("maximum")}; diff --git a/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_boolean_integer_fold.h b/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_boolean_integer_fold.h index b19f814d0..869cd0efb 100644 --- a/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_boolean_integer_fold.h +++ b/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_boolean_integer_fold.h @@ -14,6 +14,7 @@ class ExclusiveMaximumBooleanIntegerFold final : public SchemaTransformRule { const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && schema.is_object()); diff --git a/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_boolean_integer_fold.h b/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_boolean_integer_fold.h index f66428a48..82db3e155 100644 --- a/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_boolean_integer_fold.h +++ b/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_boolean_integer_fold.h @@ -14,6 +14,7 @@ class ExclusiveMinimumBooleanIntegerFold final : public SchemaTransformRule { const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && schema.is_object()); diff --git a/vendor/blaze/src/canonicalizer/rules/extends_to_array.h b/vendor/blaze/src/canonicalizer/rules/extends_to_array.h index c0813696e..949604c00 100644 --- a/vendor/blaze/src/canonicalizer/rules/extends_to_array.h +++ b/vendor/blaze/src/canonicalizer/rules/extends_to_array.h @@ -11,12 +11,14 @@ class ExtendsToArray final : public SchemaTransformRule { const sourcemeta::core::SchemaFrame::Location &, const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { - ONLY_CONTINUE_IF(vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3}) && - schema.is_object()); + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object()); const auto *extends{schema.try_at("extends")}; ONLY_CONTINUE_IF(extends && !extends->is_array()); diff --git a/vendor/blaze/src/canonicalizer/rules/hyper_schema_keywords_drop.h b/vendor/blaze/src/canonicalizer/rules/hyper_schema_keywords_drop.h new file mode 100644 index 000000000..ef148659e --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/hyper_schema_keywords_drop.h @@ -0,0 +1,59 @@ +class HyperSchemaKeywordsDrop final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + HyperSchemaKeywordsDrop() + : SchemaTransformRule{"hyper_schema_keywords_drop"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::SchemaVocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const -> bool override { + // The canonical form says what makes an instance valid, and none of these + // takes part in that: `links` describes link relations, `readonly` and + // `mediaType` describe the instance to a reader, and the rest describe how + // a document is addressed. Two documents that differ in none but these + // accept the same instances, so the canonical form is the one without + // them. + // + // These are the keywords the Draft 3 hyper-schema meta-schema adds to the + // Draft 3 one, named here rather than asked of the walker: a document on a + // pre-vocabulary dialect puts every keyword it knows in the one vocabulary + // that dialect stands for, `$schema` and `properties` along with `links` + // + // Only Draft 3 is taken this far. The canonical form of every other + // dialect still describes a nested document, which leaves these keywords + // somewhere to sit, so dropping them there would be a change of its own + static const std::set HYPER_SCHEMA_KEYWORDS{ + "contentEncoding", "fragmentResolution", "links", "mediaType", + "pathStart", "readonly", "root"}; + + ONLY_CONTINUE_IF( + vocabularies.contains( + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER) && + schema.is_object()); + + std::vector keywords; + for (const auto &entry : schema.as_object()) { + if (HYPER_SCHEMA_KEYWORDS.contains(entry.first)) { + keywords.push_back(entry.first); + } + } + + ONLY_CONTINUE_IF(!keywords.empty()); + this->keywords_ = std::move(keywords); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + for (const auto &keyword : this->keywords_) { + schema.erase(keyword); + } + } + +private: + mutable std::vector keywords_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/implicit_contains_keywords.h b/vendor/blaze/src/canonicalizer/rules/implicit_contains_keywords.h index 8a7079e3a..263919fea 100644 --- a/vendor/blaze/src/canonicalizer/rules/implicit_contains_keywords.h +++ b/vendor/blaze/src/canonicalizer/rules/implicit_contains_keywords.h @@ -25,17 +25,15 @@ class ImplicitContainsKeywords final : public SchemaTransformRule { } else { ONLY_CONTINUE_IF(!schema.defines("minContains") && !schema.defines("maxContains")); - ONLY_CONTINUE_IF( - !walk_up_in_place_applicators( - root, frame, location, walker, resolver, - [](const sourcemeta::core::JSON &ancestor, - const SchemaVocabularies &ancestor_vocabularies) -> bool { - return ancestor.defines("unevaluatedItems") && - ancestor_vocabularies.contains( - SchemaVocabularies::Known:: - JSON_SCHEMA_2020_12_UNEVALUATED); - }) - .has_value()); + ONLY_CONTINUE_IF(!walk_up_in_place_applicators_across_references( + root, frame, location, walker, resolver, + [](const sourcemeta::core::JSON &ancestor, + const SchemaVocabularies &ancestor_vocabularies) -> bool { + return ancestor.defines("unevaluatedItems") && + ancestor_vocabularies.contains( + SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_UNEVALUATED); + })); } return true; diff --git a/vendor/blaze/src/canonicalizer/rules/implicit_object_keywords.h b/vendor/blaze/src/canonicalizer/rules/implicit_object_keywords.h index b34cdce1d..4119f3643 100644 --- a/vendor/blaze/src/canonicalizer/rules/implicit_object_keywords.h +++ b/vendor/blaze/src/canonicalizer/rules/implicit_object_keywords.h @@ -99,6 +99,7 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { !schema.defines("patternProperties") && vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, @@ -137,6 +138,7 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0})); @@ -146,6 +148,7 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7})}; @@ -156,7 +159,8 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3}); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}); this->has_work_ = this->add_pattern_properties_ || this->add_property_names_ || this->add_min_properties_ || @@ -172,6 +176,7 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, @@ -191,6 +196,7 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4})}; this->add_unique_items_ = diff --git a/vendor/blaze/src/canonicalizer/rules/items_implicit.h b/vendor/blaze/src/canonicalizer/rules/items_implicit.h index bc061cad2..e582d772d 100644 --- a/vendor/blaze/src/canonicalizer/rules/items_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/items_implicit.h @@ -31,19 +31,17 @@ class ItemsImplicit final : public SchemaTransformRule { vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_2020_12_UNEVALUATED, SchemaVocabularies::Known::JSON_SCHEMA_2019_09_APPLICATOR}))); - ONLY_CONTINUE_IF( - !walk_up_in_place_applicators( - root, frame, location, walker, resolver, - [](const sourcemeta::core::JSON &ancestor, - const SchemaVocabularies &ancestor_vocabularies) -> bool { - return ancestor.defines("unevaluatedItems") && - ancestor_vocabularies.contains_any( - {SchemaVocabularies::Known:: - JSON_SCHEMA_2020_12_UNEVALUATED, - SchemaVocabularies::Known:: - JSON_SCHEMA_2019_09_APPLICATOR}); - }) - .has_value()); + ONLY_CONTINUE_IF(!walk_up_in_place_applicators_across_references( + root, frame, location, walker, resolver, + [](const sourcemeta::core::JSON &ancestor, + const SchemaVocabularies &ancestor_vocabularies) -> bool { + return ancestor.defines("unevaluatedItems") && + ancestor_vocabularies.contains_any( + {SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_UNEVALUATED, + SchemaVocabularies::Known:: + JSON_SCHEMA_2019_09_APPLICATOR}); + })); return true; } diff --git a/vendor/blaze/src/canonicalizer/rules/metadata_keywords_drop.h b/vendor/blaze/src/canonicalizer/rules/metadata_keywords_drop.h new file mode 100644 index 000000000..95d0d6d93 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/metadata_keywords_drop.h @@ -0,0 +1,57 @@ +class MetadataKeywordsDrop final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + MetadataKeywordsDrop() : SchemaTransformRule{"metadata_keywords_drop"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::SchemaVocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const -> bool override { + // The canonical form says what makes an instance valid, and none of these + // takes part in that: they name a schema, describe it, and say what to + // put where an instance says nothing. Two documents that differ in none + // but these accept the same instances, so the canonical form is the one + // without them. + // + // These are the metadata keywords of the Draft 3 meta-schema, named here + // rather than asked of the walker: a document on a pre-vocabulary dialect + // puts every keyword it knows in the one vocabulary that dialect stands + // for, `title` and `type` alike + // + // Only Draft 3 is taken this far. The canonical form of every other + // dialect still describes a nested document, which leaves these keywords + // somewhere to sit, so dropping them there would be a change of its own + static const std::set METADATA_KEYWORDS{ + "default", "description", "title"}; + + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object()); + + std::vector keywords; + for (const auto &entry : schema.as_object()) { + if (METADATA_KEYWORDS.contains(entry.first)) { + keywords.push_back(entry.first); + } + } + + ONLY_CONTINUE_IF(!keywords.empty()); + this->keywords_ = std::move(keywords); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + for (const auto &keyword : this->keywords_) { + schema.erase(keyword); + } + } + +private: + mutable std::vector keywords_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/min_length_implicit.h b/vendor/blaze/src/canonicalizer/rules/min_length_implicit.h index 597962de0..03d24591f 100644 --- a/vendor/blaze/src/canonicalizer/rules/min_length_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/min_length_implicit.h @@ -19,6 +19,7 @@ class MinLengthImplicit final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0}) && diff --git a/vendor/blaze/src/canonicalizer/rules/required_property_implicit.h b/vendor/blaze/src/canonicalizer/rules/required_property_implicit.h index 41110f918..8a303c632 100644 --- a/vendor/blaze/src/canonicalizer/rules/required_property_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/required_property_implicit.h @@ -13,7 +13,9 @@ class RequiredPropertyImplicit final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( - vocabularies.contains(SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3) && + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && schema.is_object()); const auto *type{schema.try_at("type")}; diff --git a/vendor/blaze/src/canonicalizer/rules/type_boolean_as_enum.h b/vendor/blaze/src/canonicalizer/rules/type_boolean_as_enum.h index 2c2624c75..ed383590e 100644 --- a/vendor/blaze/src/canonicalizer/rules/type_boolean_as_enum.h +++ b/vendor/blaze/src/canonicalizer/rules/type_boolean_as_enum.h @@ -19,6 +19,7 @@ class TypeBooleanAsEnum final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0}) && diff --git a/vendor/blaze/src/canonicalizer/rules/type_inherit_in_place.h b/vendor/blaze/src/canonicalizer/rules/type_inherit_in_place.h index cfc60e184..b3d8ee64c 100644 --- a/vendor/blaze/src/canonicalizer/rules/type_inherit_in_place.h +++ b/vendor/blaze/src/canonicalizer/rules/type_inherit_in_place.h @@ -18,6 +18,7 @@ class TypeInheritInPlace final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0})); diff --git a/vendor/blaze/src/canonicalizer/rules/type_null_as_enum.h b/vendor/blaze/src/canonicalizer/rules/type_null_as_enum.h index 3b2aa68b3..74249b716 100644 --- a/vendor/blaze/src/canonicalizer/rules/type_null_as_enum.h +++ b/vendor/blaze/src/canonicalizer/rules/type_null_as_enum.h @@ -19,6 +19,7 @@ class TypeNullAsEnum final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0}) && diff --git a/vendor/blaze/src/canonicalizer/rules/type_union_implicit.h b/vendor/blaze/src/canonicalizer/rules/type_union_implicit.h index 2a67e0867..49a38aa3e 100644 --- a/vendor/blaze/src/canonicalizer/rules/type_union_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/type_union_implicit.h @@ -11,12 +11,14 @@ class TypeUnionImplicit final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &walker, const sourcemeta::core::SchemaResolver &resolver) const -> bool override { ONLY_CONTINUE_IF(schema.is_object() && !schema.empty()); - ONLY_CONTINUE_IF(!vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3}) || - !schema.defines("disallow")); + ONLY_CONTINUE_IF( + !vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) || + !schema.defines("disallow")); ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_2020_12_VALIDATION, SchemaVocabularies::Known::JSON_SCHEMA_2019_09_VALIDATION, @@ -24,6 +26,7 @@ class TypeUnionImplicit final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0})); diff --git a/vendor/blaze/src/canonicalizer/rules/type_union_to_schemas.h b/vendor/blaze/src/canonicalizer/rules/type_union_to_schemas.h index eb5af68e5..2b99e7474 100644 --- a/vendor/blaze/src/canonicalizer/rules/type_union_to_schemas.h +++ b/vendor/blaze/src/canonicalizer/rules/type_union_to_schemas.h @@ -11,12 +11,14 @@ class TypeUnionToSchemas final : public SchemaTransformRule { const sourcemeta::core::SchemaFrame::Location &, const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { - ONLY_CONTINUE_IF(vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3}) && - schema.is_object()); + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object()); const auto *type{schema.try_at("type")}; ONLY_CONTINUE_IF(type && type->is_array()); diff --git a/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_allof.h b/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_allof.h index 5a2c2c79b..009a8acef 100644 --- a/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_allof.h +++ b/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_allof.h @@ -154,6 +154,22 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { return true; } + // Merging only collapses the sibling applicators into the `allOf` that + // is already there, and the condition has to come out false as soon as + // the transform returns. So merging is the right move only when those + // siblings are the whole reason we are here: typed keywords with no + // `type` to sit under, and references that need a branch of their own, + // all survive a merge and have to go through the extraction path instead + const bool mergeable_siblings{std::ranges::any_of( + APPLICATORS_WITHOUT_ALLOF, [&schema](const auto *applicator) -> bool { + return schema.defines(applicator); + })}; + if (!mergeable_siblings || has_orphaned_typed_keywords || + modern_ref_needs_wrapping || this->has_modern_ref_ || + this->has_dynamic_ref_ || this->has_recursive_ref_) { + return true; + } + const auto all_refs_fixed{!frame.any_reference_from( location.pointer, [&](const sourcemeta::core::SchemaReferenceType, diff --git a/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_extends.h b/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_extends.h index b5b8b2f0c..900271444 100644 --- a/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_extends.h +++ b/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_extends.h @@ -12,12 +12,14 @@ class TypeWithApplicatorToExtends final : public SchemaTransformRule { const sourcemeta::core::SchemaFrame::Location &, const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { - ONLY_CONTINUE_IF(vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3}) && - schema.is_object()); + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object()); const auto *extends_value{schema.try_at("extends")}; const bool has_extends{(extends_value != nullptr) && diff --git a/vendor/blaze/src/canonicalizer/rules/unknown_keywords_drop.h b/vendor/blaze/src/canonicalizer/rules/unknown_keywords_drop.h new file mode 100644 index 000000000..cba90e8cf --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/unknown_keywords_drop.h @@ -0,0 +1,50 @@ +class UnknownKeywordsDrop final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + UnknownKeywordsDrop() : SchemaTransformRule{"unknown_keywords_drop"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::SchemaVocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &) const -> bool override { + // A keyword the dialect does not define says nothing about whether an + // instance is valid, so the canonical form is the one without it. Every + // other dialect keeps such a keyword under an `x-` prefix instead, which + // `unknown_keywords_prefix` does, as their canonical forms describe a + // nested document that has somewhere to put it + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object()); + + std::vector keywords; + for (const auto &entry : schema.as_object()) { + // If there is any i.e. optional vocabulary we don't recognise, then this + // seemingly unknown keyword might belong to one of those, and thus it + // might not be safe to flag it + if (walker(entry.first, vocabularies).type == + SchemaKeywordType::Unknown && + !vocabularies.has_unknown()) { + keywords.push_back(entry.first); + } + } + + ONLY_CONTINUE_IF(!keywords.empty()); + this->keywords_ = std::move(keywords); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + for (const auto &keyword : this->keywords_) { + schema.erase(keyword); + } + } + +private: + mutable std::vector keywords_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/unknown_keywords_prefix.h b/vendor/blaze/src/canonicalizer/rules/unknown_keywords_prefix.h index f50bcc90b..b737b4bd1 100644 --- a/vendor/blaze/src/canonicalizer/rules/unknown_keywords_prefix.h +++ b/vendor/blaze/src/canonicalizer/rules/unknown_keywords_prefix.h @@ -10,7 +10,14 @@ class UnknownKeywordsPrefix final : public SchemaTransformRule { const SchemaFrame::Location &, const SchemaWalker &walker, const SchemaResolver &) const -> bool override { - ONLY_CONTINUE_IF(schema.is_object()); + // Draft 3 deletes an unknown keyword outright instead, which + // `unknown_keywords_drop` does, as its canonical form has nowhere to put + // one + ONLY_CONTINUE_IF( + !vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object()); std::vector locations; for (const auto &entry : schema.as_object()) { if (entry.first.starts_with("x-")) { diff --git a/vendor/blaze/src/canonicalizer/rules/unsatisfiable_empty_enum.h b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_empty_enum.h index e0089bec3..7b2a716bd 100644 --- a/vendor/blaze/src/canonicalizer/rules/unsatisfiable_empty_enum.h +++ b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_empty_enum.h @@ -17,6 +17,7 @@ class UnsatisfiableEmptyEnum final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, diff --git a/vendor/blaze/src/canonicalizer/rules/unsatisfiable_exclusive_equal_bounds.h b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_exclusive_equal_bounds.h index f459b2b6e..515aeed4e 100644 --- a/vendor/blaze/src/canonicalizer/rules/unsatisfiable_exclusive_equal_bounds.h +++ b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_exclusive_equal_bounds.h @@ -14,6 +14,7 @@ class UnsatisfiableExclusiveEqualBounds final : public SchemaTransformRule { const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && schema.is_object()); diff --git a/vendor/blaze/src/canonicalizer/rules/unsatisfiable_type_and_enum.h b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_type_and_enum.h index 916ef0f52..e3d788677 100644 --- a/vendor/blaze/src/canonicalizer/rules/unsatisfiable_type_and_enum.h +++ b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_type_and_enum.h @@ -18,6 +18,7 @@ class UnsatisfiableTypeAndEnum final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, @@ -33,19 +34,9 @@ class UnsatisfiableTypeAndEnum final : public SchemaTransformRule { const auto declared_types{parse_schema_type(*type)}; ONLY_CONTINUE_IF(declared_types.any()); - const bool integer_matches_integral{ - vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, - SchemaVocabularies::Known::JSON_SCHEMA_2019_09_VALIDATION, - SchemaVocabularies::Known::JSON_SCHEMA_2020_12_VALIDATION}) && - declared_types.test( - std::to_underlying(sourcemeta::core::JSON::Type::Integer))}; ONLY_CONTINUE_IF(std::ranges::none_of( - enum_value->as_array(), - [&declared_types, integer_matches_integral](const auto &value) -> auto { - return declared_types.test(std::to_underlying(value.type())) || - (integer_matches_integral && value.is_integral()); + enum_value->as_array(), [&declared_types](const auto &value) -> auto { + return type_can_equal(declared_types, value); })); this->unsatisfiable_ = unsatisfiable_schema(vocabularies); return true; diff --git a/vendor/blaze/src/canonicalizer/schema_helpers.h b/vendor/blaze/src/canonicalizer/schema_helpers.h index e94125166..dab11fa4d 100644 --- a/vendor/blaze/src/canonicalizer/schema_helpers.h +++ b/vendor/blaze/src/canonicalizer/schema_helpers.h @@ -30,6 +30,7 @@ inline auto parse_schema_type_string(const sourcemeta::core::JSON::String &type, } else if (type == "number") { result.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); result.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); + result.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); } else if (type == "integer") { result.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); } else if (type == "string") { diff --git a/vendor/blaze/src/compiler/CMakeLists.txt b/vendor/blaze/src/compiler/CMakeLists.txt index 987106efe..3167c2ac0 100644 --- a/vendor/blaze/src/compiler/CMakeLists.txt +++ b/vendor/blaze/src/compiler/CMakeLists.txt @@ -1,10 +1,11 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME compiler FOLDER "Blaze/Compiler" - PRIVATE_HEADERS error.h unevaluated.h + PRIVATE_HEADERS error.h SOURCES compile.cc compile_json.cc compile_helpers.h keyword_iterator.h postprocess.h - default_compiler.cc unevaluated.cc + unevaluated.h + default_compiler.cc default_compiler_2020_12.h default_compiler_2019_09.h default_compiler_draft7.h diff --git a/vendor/blaze/src/compiler/compile.cc b/vendor/blaze/src/compiler/compile.cc index fabb4bf77..ca9aa0dee 100644 --- a/vendor/blaze/src/compiler/compile.cc +++ b/vendor/blaze/src/compiler/compile.cc @@ -18,6 +18,7 @@ #include "compile_helpers.h" #include "keyword_iterator.h" #include "postprocess.h" +#include "unevaluated.h" namespace { @@ -31,17 +32,27 @@ auto is_metaschema_reference(const sourcemeta::core::WeakPointer &origin) // Draft 6 introduced boolean schemas. Draft 4 and earlier have none, and the // only places they accept a boolean are `additionalProperties` and -// `additionalItems`, whose own definitions spell that out +// `additionalItems`, whose own definitions spell that out. The OpenAPI 3.0 +// Schema Object has none either, and offers a boolean at +// `additionalProperties` alone auto booleans_are_schemas( const sourcemeta::core::SchemaVocabularies &vocabularies) -> bool { using Known = sourcemeta::core::SchemaVocabularies::Known; return !vocabularies.contains_any( {Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, - Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER}); + Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER, + Known::OPENAPI_3_0_BASE}); } +// Draft 7 and earlier let `$ref` override whatever sits beside it, so such a +// sibling is not a keyword of the schema at all and nothing about it, not even +// its shape, is this compiler's business // Draft 4 and earlier spell these as flags on a sibling bound rather than as -// bounds of their own, and their meta-schemas ask for that sibling to be there +// bounds of their own, and their meta-schemas ask for that sibling to be +// there. The OpenAPI 3.0 Schema Object is deliberately absent: it spells them +// as flags the way Draft 4 does, but its meta-schema declares no dependency +// between a flag and the bound it modifies, so one standing on its own is +// well-formed there and merely has nothing to modify auto exclusive_bounds_need_a_sibling( const sourcemeta::core::SchemaVocabularies &vocabularies) -> bool { using Known = sourcemeta::core::SchemaVocabularies::Known; @@ -57,7 +68,17 @@ auto is_schema(const sourcemeta::core::JSON &value, const bool allow_boolean) // The reason the keyword's value is not the shape the meta-schema asks for, or // `nullptr` when it is. The location the error carries names the keyword, so -// these read as statements about it +// these read as statements about it. +// +// What a dialect's meta-schema states is what settles this, rather than what +// its prose asks for, so that a schema the meta-schema accepts is one that +// compiles. A dialect whose prose carries a rule its meta-schema leaves +// unencoded therefore goes unenforced here: OpenAPI 3.0 asks that `items` be +// present whenever `type` is `"array"`, and that a Discriminator Object stand +// beside a composite keyword, while its meta-schema expresses neither, and it +// omits the lower bound that would keep `allOf`, `anyOf` and `oneOf` from +// standing empty. Catching those belongs to a linter, which is free to be +// stricter than the document the dialect publishes auto keyword_shape_error( const sourcemeta::core::JSON::String &keyword, const sourcemeta::core::SchemaKeywordType type, const bool known, @@ -72,6 +93,11 @@ auto keyword_shape_error( "This keyword was expected to be set to a boolean"}; static constexpr auto EXPECTED_NUMBER{ "This keyword was expected to be set to a number"}; + static constexpr auto EXPECTED_OBJECT{ + "This keyword was expected to be set to an object"}; + static constexpr auto EXPECTED_DIALECT_OBJECT{ + "This keyword was expected to be set to an object of the shape this " + "dialect defines"}; static constexpr auto EXPECTED_NON_NEGATIVE_INTEGER{ "This keyword was expected to be set to a non-negative integer"}; static constexpr auto EXPECTED_SCHEMA{ @@ -96,12 +122,86 @@ auto keyword_shape_error( return value.is_string() ? nullptr : EXPECTED_STRING; } if (keyword == "uniqueItems" || keyword == "deprecated" || - keyword == "readOnly" || keyword == "writeOnly") { + keyword == "readOnly" || keyword == "writeOnly" || + keyword == "nullable") { return value.is_boolean() ? nullptr : EXPECTED_BOOLEAN; } if (keyword == "examples") { return value.is_array() ? nullptr : EXPECTED_ARRAY; } + // The OpenAPI dialects define these three as objects of their own. What + // follows is what their meta-schemas state, other than the `format` + // annotations two of their members carry, which assert nothing by default + if (keyword == "discriminator" || keyword == "xml" || + keyword == "externalDocs") { + if (!value.is_object()) { + return EXPECTED_OBJECT; + } + + // Each OpenAPI version defines these objects for itself, and the later + // ones carry members this does not know about, so only the dialect this + // was written against is held to the shape below + if (!vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known::OPENAPI_3_0_BASE)) { + return nullptr; + } + + if (keyword == "discriminator") { + const auto *property_name{value.try_at("propertyName")}; + if (property_name == nullptr || !property_name->is_string()) { + return EXPECTED_DIALECT_OBJECT; + } + + const auto *mapping{value.try_at("mapping")}; + if (mapping != nullptr && + (!mapping->is_object() || + std::ranges::any_of(mapping->as_object(), + [](const auto &entry) -> bool { + return !entry.second.is_string(); + }))) { + return EXPECTED_DIALECT_OBJECT; + } + + // Unlike the other two, this one its meta-schema leaves open + return nullptr; + } + + if (keyword == "externalDocs") { + const auto *url{value.try_at("url")}; + if (url == nullptr || !url->is_string()) { + return EXPECTED_DIALECT_OBJECT; + } + } + + for (const auto &entry : value.as_object()) { + if (entry.first.starts_with("x-")) { + continue; + } + + const auto expected_string{ + keyword == "externalDocs" + ? (entry.first == "description" || entry.first == "url") + : (entry.first == "name" || entry.first == "namespace" || + entry.first == "prefix")}; + const auto expected_boolean{ + keyword == "xml" && + (entry.first == "attribute" || entry.first == "wrapped")}; + if (expected_string) { + if (!entry.second.is_string()) { + return EXPECTED_DIALECT_OBJECT; + } + } else if (expected_boolean) { + if (!entry.second.is_boolean()) { + return EXPECTED_DIALECT_OBJECT; + } + } else { + // Both of their meta-schemas close them to the members they name + return EXPECTED_DIALECT_OBJECT; + } + } + + return nullptr; + } if (keyword == "maxContains" || keyword == "minContains") { // These only exist from 2019-09 onwards, where a number whose fractional // part is zero counts as an integer @@ -117,7 +217,8 @@ auto keyword_shape_error( JSON_SCHEMA_DRAFT_3_HYPER, sourcemeta::core::SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, sourcemeta::core::SchemaVocabularies::Known:: - JSON_SCHEMA_DRAFT_4_HYPER}) + JSON_SCHEMA_DRAFT_4_HYPER, + sourcemeta::core::SchemaVocabularies::Known::OPENAPI_3_0_BASE}) ? (value.is_boolean() ? nullptr : EXPECTED_BOOLEAN) : (value.is_number() ? nullptr : EXPECTED_NUMBER)); } @@ -292,11 +393,16 @@ auto compile_subschema(const sourcemeta::blaze::Context &context, const auto *shape_error{keyword_shape_error( keyword, metadata.type, official, schema_context.schema.at(keyword), allow_boolean, schema_context.vocabularies)}; + static const sourcemeta::core::JSON::String KEYWORD_REF{"$ref"}; + const auto overridden_by_ref{ + keyword != KEYWORD_REF && schema_context.schema.defines(KEYWORD_REF) && + ref_object_ignores_siblings(schema_context.vocabularies)}; + // Draft 3 spells these as flags on a sibling bound rather than as bounds of // their own, and its meta-schema asks for that sibling to be there static const sourcemeta::core::JSON::String KEYWORD_MINIMUM{"minimum"}; static const sourcemeta::core::JSON::String KEYWORD_MAXIMUM{"maximum"}; - if (official && + if (!overridden_by_ref && official && exclusive_bounds_need_a_sibling(schema_context.vocabularies) && ((keyword == "exclusiveMinimum" && !schema_context.schema.defines(KEYWORD_MINIMUM)) || @@ -311,7 +417,7 @@ auto compile_subschema(const sourcemeta::blaze::Context &context, "This keyword was expected to accompany the bound it applies to"); } - if (shape_error != nullptr) [[unlikely]] { + if (!overridden_by_ref && shape_error != nullptr) [[unlikely]] { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location( diff --git a/vendor/blaze/src/compiler/compile_helpers.h b/vendor/blaze/src/compiler/compile_helpers.h index dcc615053..ff9eb4ed7 100644 --- a/vendor/blaze/src/compiler/compile_helpers.h +++ b/vendor/blaze/src/compiler/compile_helpers.h @@ -9,6 +9,7 @@ #include // assert #include // std::cref #include // std::distance +#include // std::numeric_limits #include // std::optional #include // std::regex, std::regex_match, std::smatch #include // std::out_of_range @@ -254,6 +255,28 @@ inline auto is_numeric_integer_type_check(const Instruction &instruction) sourcemeta::core::JSON::Type::Integer; } +// A multiple of one admits exactly the integers, so it bounds a number the way +// an integer type check does, without saying anything about the type +inline auto is_numeric_integral_multiple_check(const Instruction &instruction) + -> bool { + if (instruction.type != InstructionIndex::AssertionDivisible) { + return false; + } + + const auto &divisor{std::get(instruction.value)}; + if (!divisor.is_integral()) { + return false; + } + + // A real or decimal may spell an integer too large to represent, and such a + // divisor is never one, so a failed conversion just means no fusion + try { + return divisor.as_integer() == 1; + } catch (const std::out_of_range &) { + return false; + } +} + inline auto is_numeric_bound_check(const Instruction &instruction) -> bool { return instruction.type == InstructionIndex::AssertionGreaterEqual || instruction.type == InstructionIndex::AssertionGreater || @@ -277,12 +300,22 @@ inline auto merge_integer_bound(const Instruction &instruction, if (instruction.type == InstructionIndex::AssertionGreaterEqual) { minimum = minimum.has_value() ? std::max(minimum.value(), value) : value; } else if (instruction.type == InstructionIndex::AssertionGreater) { + // Tightening the bound past the representable range would wrap around, so + // leave such a bound to the standalone instruction that can still hold it + if (value == std::numeric_limits::max()) { + return false; + } + const auto adjusted{value + 1}; minimum = minimum.has_value() ? std::max(minimum.value(), adjusted) : adjusted; } else if (instruction.type == InstructionIndex::AssertionLessEqual) { maximum = maximum.has_value() ? std::min(maximum.value(), value) : value; } else if (instruction.type == InstructionIndex::AssertionLess) { + if (value == std::numeric_limits::min()) { + return false; + } + const auto adjusted{value - 1}; maximum = maximum.has_value() ? std::min(maximum.value(), adjusted) : adjusted; @@ -293,6 +326,47 @@ inline auto merge_integer_bound(const Instruction &instruction, return true; } +// Whether the bound fits the integer the instructions carry. One that does not +// sits beyond the size of anything that can be held in memory, so the keyword +// compilers say such a bound outright rather than convert a value that does +// not fit. The comparison reads the number by its value, so it holds whichever +// way the literal came to be stored +inline auto bound_is_representable(const sourcemeta::core::JSON &value) + -> bool { + static const sourcemeta::core::JSON LIMIT{ + std::numeric_limits::max()}; + return value <= LIMIT; +} + +// Draft 3 to 7 let `$ref` replace every sibling keyword, so nothing those +// siblings say ever takes effect, and the OpenAPI 3.0 Schema Object answers +// with Draft 4 here. Core's schema walker frames nothing under one of those +// drafts either, which is what keeps such a sibling from compiling at all +inline auto ref_overrides_sibling_keywords(const SchemaContext &schema_context) + -> bool { + using Known = sourcemeta::core::SchemaVocabularies::Known; + return schema_context.schema.is_object() && + schema_context.schema.defines("$ref") && + schema_context.vocabularies.contains_any( + {Known::JSON_SCHEMA_DRAFT_7, Known::JSON_SCHEMA_DRAFT_7_HYPER, + Known::JSON_SCHEMA_DRAFT_6, Known::JSON_SCHEMA_DRAFT_6_HYPER, + Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER, + Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, + Known::OPENAPI_3_0_BASE}); +} + +// The OpenAPI 3.0 Schema Object spells a reference as a Reference Object, whose +// every other member the specification says to ignore. The shape of such a +// member is therefore not this dialect's to judge at all, and its own +// meta-schema agrees: it accepts whatever stands beside a `$ref`. Every other +// dialect here lets `$ref` override its siblings without excusing them from +// being well-formed, which their meta-schemas do go on to check +inline auto ref_object_ignores_siblings( + const sourcemeta::core::SchemaVocabularies &vocabularies) -> bool { + return vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known::OPENAPI_3_0_BASE); +} + // Note that the size keywords that let the type level fuse their bound accept // any integral number, including reals with no fractional part like `2.0`, so // this must recognise those too or the fused bound would be silently dropped @@ -577,7 +651,8 @@ inline auto required_properties(const SchemaContext &schema_context) schema_context.vocabularies.contains( Known::JSON_SCHEMA_2019_09_VALIDATION) || schema_context.vocabularies.contains( - Known::JSON_SCHEMA_2020_12_VALIDATION)}; + Known::JSON_SCHEMA_2020_12_VALIDATION) || + schema_context.vocabularies.contains(Known::OPENAPI_3_0_BASE)}; ValueStringSet result; diff --git a/vendor/blaze/src/compiler/default_compiler.cc b/vendor/blaze/src/compiler/default_compiler.cc index 500327245..92779a8e3 100644 --- a/vendor/blaze/src/compiler/default_compiler.cc +++ b/vendor/blaze/src/compiler/default_compiler.cc @@ -8,16 +8,76 @@ #include "default_compiler_draft7.h" #include "default_compiler_openapi.h" +#include // std::ranges::any_of #include // assert #include // std::string #include // std::unordered_set +namespace { + +// Whether any element of the array is a schema +auto any_element_is_a_schema(const sourcemeta::core::JSON &value, + const bool allow_boolean) -> bool { + return value.is_array() && + std::ranges::any_of(value.as_array(), + [allow_boolean](const auto &element) -> bool { + return is_schema(element, allow_boolean); + }); +} + +// Whether a schema sits where a keyword of this kind holds one, and so would +// have to compile. A single schema is enough to settle it, as several of these +// keywords let a schema stand beside a value that is not one: Draft 3 `type` +// lists type names among its subschemas, and `dependencies` lists property +// names among them, so asking that every member or element be a schema would +// hand a schema to a compiler that cannot be given one +auto holds_schema(const sourcemeta::core::SchemaKeywordType type, + const sourcemeta::core::JSON &value, const bool allow_boolean) + -> bool { + switch (type) { + case sourcemeta::core::SchemaKeywordType::Unknown: + case sourcemeta::core::SchemaKeywordType::Assertion: + case sourcemeta::core::SchemaKeywordType::Annotation: + case sourcemeta::core::SchemaKeywordType::Reference: + case sourcemeta::core::SchemaKeywordType::Other: + case sourcemeta::core::SchemaKeywordType::Comment: + return false; + // The schemas are the members + case sourcemeta::core::SchemaKeywordType::LocationMembers: + case sourcemeta::core::SchemaKeywordType:: + ApplicatorMembersTraversePropertyStatic: + case sourcemeta::core::SchemaKeywordType:: + ApplicatorMembersTraversePropertyRegex: + case sourcemeta::core::SchemaKeywordType::ApplicatorMembersInPlaceSome: + return value.is_object() && + std::ranges::any_of( + value.as_object(), [allow_boolean](const auto &entry) -> bool { + return is_schema(entry.second, allow_boolean); + }); + // The schemas are the elements, and the value itself is never one + case sourcemeta::core::SchemaKeywordType::ApplicatorElementsTraverseItem: + case sourcemeta::core::SchemaKeywordType::ApplicatorElementsInPlace: + case sourcemeta::core::SchemaKeywordType::ApplicatorElementsInPlaceSome: + case sourcemeta::core::SchemaKeywordType:: + ApplicatorElementsInPlaceSomeNegate: + return any_element_is_a_schema(value, allow_boolean); + // Either the value is the schema, or its elements are + case sourcemeta::core::SchemaKeywordType:: + ApplicatorValueOrElementsTraverseAnyItemOrItem: + case sourcemeta::core::SchemaKeywordType::ApplicatorValueOrElementsInPlace: + return is_schema(value, allow_boolean) || + any_element_is_a_schema(value, allow_boolean); + // The value is the schema + default: + return is_schema(value, allow_boolean); + } +} + // NOLINTNEXTLINE(google-readability-function-size,hicpp-function-size,readability-function-size) -auto sourcemeta::blaze::default_schema_compiler( - const sourcemeta::blaze::Context &context, - const sourcemeta::blaze::SchemaContext &schema_context, - const sourcemeta::blaze::DynamicContext &dynamic_context, - const sourcemeta::blaze::Instructions ¤t) +auto dispatch_keyword(const sourcemeta::blaze::Context &context, + const sourcemeta::blaze::SchemaContext &schema_context, + const sourcemeta::blaze::DynamicContext &dynamic_context, + const sourcemeta::blaze::Instructions ¤t) -> sourcemeta::blaze::Instructions { assert(!dynamic_context.keyword.empty()); @@ -46,6 +106,7 @@ auto sourcemeta::blaze::default_schema_compiler( Known::JSON_SCHEMA_DRAFT_4_HYPER, Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, + Known::OPENAPI_3_0_BASE, Known::OPENAPI_3_1_BASE, Known::OPENAPI_3_2_BASE, Known::SOURCEMETA_EXTENSION_V1}; @@ -70,13 +131,6 @@ auto sourcemeta::blaze::default_schema_compiler( current); \ } -#define STOP_IF_SIBLING_KEYWORD(vocabulary, _keyword) \ - if (schema_context.vocabularies.contains(vocabulary) && \ - schema_context.schema.is_object() && \ - schema_context.schema.defines(_keyword)) { \ - return {}; \ - } - // ******************************************** // 2020-12 // ******************************************** @@ -293,8 +347,6 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_7, Known::JSON_SCHEMA_DRAFT_7_HYPER, "$ref", compiler_draft3_core_ref); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_7, "$ref"); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_7_HYPER, "$ref"); // Any COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_7, Known::JSON_SCHEMA_DRAFT_7_HYPER, @@ -382,8 +434,6 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_6, Known::JSON_SCHEMA_DRAFT_6_HYPER, "$ref", compiler_draft3_core_ref); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_6, "$ref"); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_6_HYPER, "$ref"); // Any COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_6, Known::JSON_SCHEMA_DRAFT_6_HYPER, @@ -468,8 +518,6 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER, "$ref", compiler_draft3_core_ref); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_4, "$ref"); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_4_HYPER, "$ref"); // Applicators COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER, @@ -541,8 +589,6 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, "$ref", compiler_draft3_core_ref); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_3, "$ref"); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_3_HYPER, "$ref"); // Applicators COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, @@ -609,9 +655,101 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::OPENAPI_3_1_BASE, Known::OPENAPI_3_2_BASE, "example", compiler_openapi_noop); + // ******************************************** + // OPENAPI 3.0 + // ******************************************** + + // Every arm below guards on this vocabulary alone, never sharing a + // `COMPILE_ANY` with a JSON Schema one. The OpenAPI 3.0 Schema Object is a + // closed dialect whose keyword set is neither a subset nor a superset of + // Draft 4, so a shared arm would hand it a keyword it must not have. The + // rejection at the end of this function relies on this table naming every + // keyword of the dialect and nothing else + + COMPILE(Known::OPENAPI_3_0_BASE, "$ref", compiler_draft3_core_ref); + + // Applicators + COMPILE(Known::OPENAPI_3_0_BASE, "allOf", compiler_draft4_applicator_allof); + COMPILE(Known::OPENAPI_3_0_BASE, "anyOf", compiler_draft4_applicator_anyof); + COMPILE(Known::OPENAPI_3_0_BASE, "oneOf", compiler_draft4_applicator_oneof); + COMPILE(Known::OPENAPI_3_0_BASE, "not", compiler_draft4_applicator_not); + COMPILE(Known::OPENAPI_3_0_BASE, "properties", + compiler_draft3_applicator_properties); + COMPILE(Known::OPENAPI_3_0_BASE, "additionalProperties", + compiler_draft3_applicator_additionalproperties); + COMPILE(Known::OPENAPI_3_0_BASE, "items", compiler_draft3_applicator_items); + + // Any + COMPILE(Known::OPENAPI_3_0_BASE, "type", compiler_draft3_validation_type); + COMPILE(Known::OPENAPI_3_0_BASE, "enum", compiler_draft3_validation_enum); + + // Object + COMPILE(Known::OPENAPI_3_0_BASE, "required", + compiler_draft4_validation_required); + COMPILE(Known::OPENAPI_3_0_BASE, "maxProperties", + compiler_draft4_validation_maxproperties); + COMPILE(Known::OPENAPI_3_0_BASE, "minProperties", + compiler_draft4_validation_minproperties); + + // Array + COMPILE(Known::OPENAPI_3_0_BASE, "uniqueItems", + compiler_draft3_validation_uniqueitems); + COMPILE(Known::OPENAPI_3_0_BASE, "maxItems", + compiler_draft3_validation_maxitems); + COMPILE(Known::OPENAPI_3_0_BASE, "minItems", + compiler_draft3_validation_minitems); + + // String + COMPILE(Known::OPENAPI_3_0_BASE, "pattern", + compiler_draft3_validation_pattern); + COMPILE(Known::OPENAPI_3_0_BASE, "maxLength", + compiler_draft3_validation_maxlength); + COMPILE(Known::OPENAPI_3_0_BASE, "minLength", + compiler_draft3_validation_minlength); + COMPILE(Known::OPENAPI_3_0_BASE, "format", compiler_draft3_validation_format); + + // Number + COMPILE(Known::OPENAPI_3_0_BASE, "maximum", + compiler_draft3_validation_maximum); + COMPILE(Known::OPENAPI_3_0_BASE, "minimum", + compiler_draft3_validation_minimum); + COMPILE(Known::OPENAPI_3_0_BASE, "multipleOf", + compiler_draft3_validation_divisibleby); + + // `maximum` and `minimum` read these as siblings, as they do on the Draft 4 + // rung, so the keywords themselves emit nothing. They need an arm all the + // same, or the rejection below would refuse them + COMPILE(Known::OPENAPI_3_0_BASE, "exclusiveMaximum", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "exclusiveMinimum", compiler_openapi_noop); + + // `type` reads `nullable` as a sibling, and the rest assert nothing at all + COMPILE(Known::OPENAPI_3_0_BASE, "nullable", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "discriminator", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "xml", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "externalDocs", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "example", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "readOnly", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "writeOnly", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "deprecated", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "title", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "description", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "default", compiler_openapi_noop); + #undef COMPILE #undef COMPILE_ANY -#undef STOP_IF_SIBLING_KEYWORD + + // The OpenAPI 3.0 Schema Object is closed: its meta-schema sets + // `additionalProperties` to `false` and admits no member other than the + // keywords it names and those prefixed with `x-`. A member outside that set + // is no keyword we may ignore, so compilation refuses it rather than passing + // it by, and the table above is therefore a complete enumeration of the + // dialect rather than a convenience + if (schema_context.vocabularies.contains(Known::OPENAPI_3_0_BASE) && + !dynamic_context.keyword.starts_with("x-")) { + throw sourcemeta::blaze::CompilerError( + schema_context.base, absolute_schema_location(context, schema_context), + PROHIBITED_KEYWORD_FOR_DIALECT); + } if ((schema_context.vocabularies.contains(Known::JSON_SCHEMA_2019_09_CORE) || schema_context.vocabularies.contains(Known::JSON_SCHEMA_2020_12_CORE)) && @@ -639,3 +777,45 @@ auto sourcemeta::blaze::default_schema_compiler( return {}; } + +} // namespace + +auto sourcemeta::blaze::default_schema_compiler( + const sourcemeta::blaze::Context &context, + const sourcemeta::blaze::SchemaContext &schema_context, + const sourcemeta::blaze::DynamicContext &dynamic_context, + const sourcemeta::blaze::Instructions ¤t) + -> sourcemeta::blaze::Instructions { + if (ref_overrides_sibling_keywords(schema_context)) { + const auto &metadata{ + context.walker(dynamic_context.keyword, schema_context.vocabularies)}; + if (metadata.type != sourcemeta::core::SchemaKeywordType::Reference) { + // Whether the shape an overridden keyword takes is one its dialect + // allows at all is still for the keyword compiler to judge, so such a + // keyword compiles for that judgement alone and what it makes is thrown + // away. One holding a schema is where that stops, as core's schema + // walker frames nothing under an overridden keyword, and an unframed + // schema is not one this compiler can be handed. The dialect that reads + // a reference as an object of its own stops earlier still, as there the + // sibling is not merely overridden but ignored. + // + // Draft 3 has no boolean schemas, but its own definitions of these two + // keywords accept a boolean in place of one + const auto allow_boolean{ + booleans_are_schemas(schema_context.vocabularies) || + dynamic_context.keyword == "additionalProperties" || + dynamic_context.keyword == "additionalItems"}; + if (!ref_object_ignores_siblings(schema_context.vocabularies) && + !holds_schema(metadata.type, + schema_context.schema.at(dynamic_context.keyword), + allow_boolean)) { + [[maybe_unused]] const auto discarded{dispatch_keyword( + context, schema_context, dynamic_context, current)}; + } + + return {}; + } + } + + return dispatch_keyword(context, schema_context, dynamic_context, current); +} diff --git a/vendor/blaze/src/compiler/default_compiler_2019_09.h b/vendor/blaze/src/compiler/default_compiler_2019_09.h index a466478fa..14cb7e81c 100644 --- a/vendor/blaze/src/compiler/default_compiler_2019_09.h +++ b/vendor/blaze/src/compiler/default_compiler_2019_09.h @@ -3,6 +3,8 @@ #include +#include // std::to_underlying + #include "compile_helpers.h" #include "default_compiler_draft4.h" @@ -149,28 +151,41 @@ auto compiler_2019_09_applicator_contains_with_options( return {}; } + // These bounds accept any integral number, including one spelled with a + // fractional or exponent part like `1.0` or `1e0`, as this dialect reads + // every number with a zero fractional part as an integer. A bound too large + // to hold is left out, as the general check below cannot carry it std::size_t minimum{1}; - if (schema_context.schema.defines("minContains")) { - if (schema_context.schema.at("minContains").is_integer() && - schema_context.schema.at("minContains").is_positive()) { - minimum = static_cast( - schema_context.schema.at("minContains").to_integer()); - } else if (schema_context.schema.at("minContains").is_real() && - schema_context.schema.at("minContains").is_positive()) { + bool minimum_is_unreachable{false}; + if (schema_context.schema.defines("minContains") && + schema_context.schema.at("minContains").is_integral() && + schema_context.schema.at("minContains").is_positive()) { + if (bound_is_representable(schema_context.schema.at("minContains"))) { minimum = static_cast( schema_context.schema.at("minContains").as_integer()); + } else { + minimum_is_unreachable = true; } } std::optional maximum; - if (schema_context.schema.defines("maxContains")) { - if (schema_context.schema.at("maxContains").is_integer() && - schema_context.schema.at("maxContains").is_positive()) { - maximum = schema_context.schema.at("maxContains").to_integer(); - } else if (schema_context.schema.at("maxContains").is_real() && - schema_context.schema.at("maxContains").is_positive()) { - maximum = schema_context.schema.at("maxContains").as_integer(); - } + if (schema_context.schema.defines("maxContains") && + schema_context.schema.at("maxContains").is_integral() && + schema_context.schema.at("maxContains").is_positive() && + bound_is_representable(schema_context.schema.at("maxContains"))) { + maximum = static_cast( + schema_context.schema.at("maxContains").as_integer()); + } + + // A lower bound too large to hold is one no array has enough items to meet, + // so every array fails and non-arrays are left alone, which is what this + // keyword asks for. Carrying such a bound into the general check below would + // instead report a count the schema never stated + if (minimum_is_unreachable) { + ValueTypes types; + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Array)); + return {make(sourcemeta::blaze::InstructionIndex::AssertionNotTypeStrictAny, + context, schema_context, dynamic_context, types)}; } // An unsatisfiable range is deliberately left for the general check below, diff --git a/vendor/blaze/src/compiler/default_compiler_draft3.h b/vendor/blaze/src/compiler/default_compiler_draft3.h index c5e942bc9..2e9a2411a 100644 --- a/vendor/blaze/src/compiler/default_compiler_draft3.h +++ b/vendor/blaze/src/compiler/default_compiler_draft3.h @@ -8,7 +8,9 @@ #include // std::sort, std::ranges::any_of, std::ranges::all_of, std::find_if, std::ranges::none_of #include // assert +#include // std::int32_t, std::int64_t #include // std::initializer_list +#include // std::numeric_limits #include // std::set #include // std::move, std::to_underlying @@ -111,9 +113,83 @@ static constexpr auto EXPECTED_NON_NEGATIVE{ static constexpr auto EXPECTED_TYPE_NAMES{ "This keyword was expected to be set to a known type name, or to a " "non-empty array of unique ones"}; +static constexpr auto UNSUPPORTED_VALUE_FOR_DIALECT{ + "The value this keyword is set to is not supported by this dialect"}; +static constexpr auto PROHIBITED_KEYWORD_FOR_DIALECT{ + "This keyword is prohibited by this dialect"}; +// A subschema that `narrows_type_away` drops is never compiled, so the refusal +// at the end of the dispatch table never reaches its members. Walk them here, +// as a member this dialect does not define is an error rather than one more +// keyword to pass by. The dialect is closed, so the applicators below are the +// complete set of places a further subschema can sit +static inline auto assert_members_are_permitted( + const sourcemeta::core::JSON &schema, + const sourcemeta::blaze::Context &context, + const sourcemeta::blaze::SchemaContext &schema_context, + const std::uint64_t depth = 0) -> void { + if (depth >= context.tweaks.max_depth) [[unlikely]] { + throw sourcemeta::blaze::CompilerDepthLimitError{context.tweaks.max_depth}; + } + + // The OpenAPI 3.0 Schema Object is the only dialect here that treats a + // member it does not define as an error, so it is the only one whose pruned + // subschemas have anything to answer for + if (!schema_context.vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known::OPENAPI_3_0_BASE)) { + return; + } + + if (!schema.is_object()) { + return; + } + + // A Reference Object ignores whatever sits beside `$ref`, so those members + // answer to no keyword set + if (schema.defines("$ref")) { + return; + } + + for (const auto &entry : schema.as_object()) { + if (entry.first.starts_with("x-")) { + continue; + } + + if (context.walker(entry.first, schema_context.vocabularies).type == + sourcemeta::core::SchemaKeywordType::Unknown) { + throw sourcemeta::blaze::CompilerError( + schema_context.base, + absolute_schema_location(context, schema_context), + PROHIBITED_KEYWORD_FOR_DIALECT); + } + + if (entry.first == "not" || entry.first == "items" || + entry.first == "additionalProperties") { + assert_members_are_permitted(entry.second, context, schema_context, + depth + 1); + } else if (entry.first == "allOf" || entry.first == "anyOf" || + entry.first == "oneOf") { + if (entry.second.is_array()) { + for (const auto &element : entry.second.as_array()) { + assert_members_are_permitted(element, context, schema_context, + depth + 1); + } + } + } else if (entry.first == "properties") { + if (entry.second.is_object()) { + for (const auto &property : entry.second.as_object()) { + assert_members_are_permitted(property.second, context, schema_context, + depth + 1); + } + } + } + } +} + static constexpr auto EXPECTED_UNIQUE_VALUES{ "This keyword was expected to be set to a non-empty array of unique " "values"}; +static constexpr auto EXPECTED_NON_EMPTY_VALUES{ + "This keyword was expected to be set to a non-empty array of values"}; static constexpr auto EXPECTED_SCHEMA_OBJECT{ "Every value of this keyword was expected to be a valid schema"}; static constexpr auto EXPECTED_DEPENDENCIES{ @@ -122,24 +198,80 @@ static constexpr auto EXPECTED_DEPENDENCIES{ // Draft 3 and Draft 4 read "integer" as a number written without a fractional // part, so a bound like `2.0` is not one there. Draft 6 onwards widened it to -// any number whose fractional part is zero +// any number whose fractional part is zero. The OpenAPI 3.0 Schema Object +// answers with Draft 4, as its meta-schema is a Draft 4 document. +// +// Note that OpenAPI 3.0.4 says the opposite in prose, that "both `1` and +// `1.0` are equivalent, and are both considered to be integers", reversing +// what 3.0.3 said. We keep the Draft 4 reading for every dated OpenAPI 3.0 +// dialect regardless, because the Schema Object extends the Draft 4 base +// dialect and so is the base dialect's question to answer, not a patch +// release's to redefine auto integral_reals_are_integers( const sourcemeta::core::SchemaVocabularies &vocabularies) -> bool { using Known = sourcemeta::core::SchemaVocabularies::Known; return !vocabularies.contains_any( {Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, - Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER}); + Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER, + Known::OPENAPI_3_0_BASE}); +} + +// A number too large to hold as a 64-bit integer is kept as a decimal, where +// whether it is an integer stays a question about how the literal was written +// rather than about how it came to be stored +auto is_dialect_integer( + const sourcemeta::core::JSON &value, + const sourcemeta::core::SchemaVocabularies &vocabularies) -> bool { + if (!value.is_integral()) { + return false; + } + + return integral_reals_are_integers(vocabularies) || value.is_integer() || + (value.is_decimal() && value.to_decimal().is_integer()); } // Draft 6 introduced boolean schemas. Draft 4 and earlier have none, and the // only places they accept a boolean are `additionalProperties` and -// `additionalItems`, whose own definitions spell that out +// `additionalItems`, whose own definitions spell that out. The OpenAPI 3.0 +// Schema Object has none either, and offers a boolean at +// `additionalProperties` alone auto booleans_are_schemas( const sourcemeta::core::SchemaVocabularies &vocabularies) -> bool { using Known = sourcemeta::core::SchemaVocabularies::Known; return !vocabularies.contains_any( {Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, - Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER}); + Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER, + Known::OPENAPI_3_0_BASE}); +} + +// OpenAPI 3.0 bans `type: "null"` and offers `nullable` in its place, which +// widens a sibling `type` and nothing else. It takes effect only where `type` +// is present, so on its own it constrains nothing, and it does not relax any +// other keyword, so a null instance still has to satisfy every sibling. +// The vocabulary has to be in play, or a member by that name in a dialect that +// gives it no meaning would be read as author data one moment and as a keyword +// the next +auto is_nullable(const sourcemeta::core::SchemaVocabularies &vocabularies, + const sourcemeta::core::JSON &schema) -> bool { + using Known = sourcemeta::core::SchemaVocabularies::Known; + return vocabularies.contains(Known::OPENAPI_3_0_BASE) && schema.is_object() && + schema.defines("nullable") && schema.at("nullable").is_boolean() && + schema.at("nullable").to_boolean(); +} + +// Whether a sibling `type` names this type and may be taken as a guarantee +// about the instance. A scalar `type` normally is one, which lets the compiler +// specialise on it, either by folding another keyword's bound into the type +// assertion or by emitting an instruction that assumes the instance's shape. +// `nullable` widens it to also admit null, so there it guarantees nothing and +// every such specialisation has to stand down +auto assume_type(const sourcemeta::core::SchemaVocabularies &vocabularies, + const sourcemeta::core::JSON &schema, + const std::string_view name) -> bool { + return schema.is_object() && schema.defines("type") && + schema.at("type").is_string() && + schema.at("type").to_string() == name && + !is_nullable(vocabularies, schema); } auto is_schema(const sourcemeta::core::JSON &value, const bool allow_boolean) @@ -159,11 +291,22 @@ auto all_are_schemas(const sourcemeta::core::JSON &value, // that does not satisfy that is not a constraint at all, so we ignore it // rather than compile part of it or turn it into a failure auto is_schema_array(const sourcemeta::core::JSON &value, - const bool allow_boolean) -> bool { - return value.is_array() && !value.empty() && + const bool allow_boolean, const bool allow_empty = false) + -> bool { + return value.is_array() && (allow_empty || !value.empty()) && all_are_schemas(value, allow_boolean); } +// Draft 4 onwards give `allOf`, `anyOf` and `oneOf` a `minItems` of one, so an +// empty array is malformed there. The OpenAPI 3.0 Schema Object constrains +// only the member type, leaving an empty one well-formed, and an empty +// conjunction or disjunction has a settled meaning of its own +auto in_place_applicators_may_be_empty( + const sourcemeta::core::SchemaVocabularies &vocabularies) -> bool { + using Known = sourcemeta::core::SchemaVocabularies::Known; + return vocabularies.contains(Known::OPENAPI_3_0_BASE); +} + // Draft 3 describes `type` and `disallow` as a type name or an array of unique // entries, each of which is a type name or a subschema auto is_draft3_type_union(const sourcemeta::core::JSON &value) -> bool { @@ -218,6 +361,14 @@ compile_properties(const sourcemeta::blaze::Context &context, const sourcemeta::blaze::DynamicContext &dynamic_context, const sourcemeta::blaze::Instructions &) -> std::vector> { + // The keywords that fuse with `properties` reach it from the side, past the + // point where an overridden keyword is dropped. A `properties` that `$ref` + // overrides enforces nothing, and nothing under it is framed, so there is + // neither anything to compile here nor anything to fuse with + if (ref_overrides_sibling_keywords(schema_context)) { + return {}; + } + std::vector> properties; for (const auto &entry : schema_context.schema.at("properties").as_object()) { @@ -322,10 +473,8 @@ auto compile_required_assertions(const Context &context, return {}; } - const auto assume_object{schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == - "object"}; + const auto assume_object{assume_type(schema_context.vocabularies, + schema_context.schema, "object")}; if (properties_set.empty()) { return {}; @@ -463,7 +612,10 @@ auto properties_as_loop(const Context &context, schema_context.vocabularies.contains( Known::JSON_SCHEMA_2019_09_VALIDATION) || schema_context.vocabularies.contains( - Known::JSON_SCHEMA_2020_12_VALIDATION); + Known::JSON_SCHEMA_2020_12_VALIDATION) || + schema_context.vocabularies.contains(Known::OPENAPI_3_0_BASE); + // `const` is deliberately absent from the OpenAPI 3.0 dialect below, which + // has `enum` but no `const` of its own const auto imports_const = schema_context.vocabularies.contains(Known::JSON_SCHEMA_DRAFT_6) || schema_context.vocabularies.contains(Known::JSON_SCHEMA_DRAFT_7) || @@ -661,6 +813,17 @@ auto extract_integer_bounds(const Instructions &children) return {minimum, maximum}; } +auto integer_lower_bound_extra_index(const Instructions &children) + -> std::size_t { + for (const auto &child : children) { + if (child.type == InstructionIndex::AssertionGreaterEqual) { + return child.extra_index; + } + } + + return 0; +} + auto compiler_draft3_applicator_properties_with_options( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions ¤t, @@ -680,6 +843,11 @@ auto compiler_draft3_applicator_properties_with_options( } if (narrows_type_away(schema_context.schema, {"object"})) { + for (const auto &entry : + schema_context.schema.at(dynamic_context.keyword).as_object()) { + assert_members_are_permitted(entry.second, context, schema_context); + } + return {}; } @@ -739,10 +907,8 @@ auto compiler_draft3_applicator_properties_with_options( ? dynamic_context : relative_dynamic_context()}; - const auto assume_object{schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == - "object"}; + const auto assume_object{assume_type(schema_context.vocabularies, + schema_context.schema, "object")}; ValueStringSet required{required_properties(schema_context)}; @@ -941,10 +1107,13 @@ auto compiler_draft3_applicator_properties_with_options( ? InstructionIndex::AssertionTypeIntegerBoundedStrict : InstructionIndex::AssertionTypeIntegerBounded; auto instance_location = substeps.front().relative_instance_location; + const auto minimum_extra_index{ + integer_lower_bound_extra_index(substeps)}; substeps.clear(); auto fused = make(index, context, schema_context, relative_dynamic_context(), bounds); fused.relative_instance_location = std::move(instance_location); + fused.extra_index = minimum_extra_index; substeps.push_back(std::move(fused)); } else if (is_integer_type_lower_bound_pattern(substeps)) { const auto minimum = extract_integer_lower_bound(substeps); @@ -953,11 +1122,14 @@ auto compiler_draft3_applicator_properties_with_options( ? InstructionIndex::AssertionTypeIntegerLowerBoundStrict : InstructionIndex::AssertionTypeIntegerLowerBound; auto instance_location = substeps.front().relative_instance_location; + const auto minimum_extra_index{ + integer_lower_bound_extra_index(substeps)}; substeps.clear(); auto fused = make(index, context, schema_context, relative_dynamic_context(), ValueIntegerBounds{minimum, 0}); fused.relative_instance_location = std::move(instance_location); + fused.extra_index = minimum_extra_index; substeps.push_back(std::move(fused)); } else if (substeps.size() == 2) { bool has_items_bounded{false}; @@ -1287,10 +1459,8 @@ auto compiler_draft3_applicator_patternproperties( inline auto properties_enforce_closed_object(const Context &context, const SchemaContext &schema_context) -> bool { - const bool assume_object{schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == - "object"}; + const bool assume_object{assume_type(schema_context.vocabularies, + schema_context.schema, "object")}; if (!assume_object || !schema_context.schema.defines("properties") || !schema_context.schema.at("properties").is_object()) { return false; @@ -1334,6 +1504,9 @@ auto compiler_draft3_applicator_additionalproperties_with_options( const DynamicContext &dynamic_context, const bool annotate, const bool track_evaluation) -> Instructions { if (narrows_type_away(schema_context.schema, {"object"})) { + assert_members_are_permitted( + schema_context.schema.at(dynamic_context.keyword), context, + schema_context); return {}; } @@ -1686,6 +1859,9 @@ auto compiler_draft3_applicator_items_with_options( } if (narrows_type_away(schema_context.schema, {"array"})) { + assert_members_are_permitted( + schema_context.schema.at(dynamic_context.keyword), context, + schema_context); return {}; } @@ -1934,6 +2110,15 @@ auto compiler_draft3_validation_enum(const Context &context, EXPECTED_UNIQUE_VALUES); } + // The OpenAPI 3.0 Schema Object asks for at least one value, and is alone in + // permitting duplicates among them + if (schema_context.vocabularies.contains(Known::OPENAPI_3_0_BASE) && + schema_context.schema.at(dynamic_context.keyword).empty()) { + throw sourcemeta::blaze::CompilerError( + schema_context.base, absolute_schema_location(context, schema_context), + EXPECTED_NON_EMPTY_VALUES); + } + if (schema_context.schema.at(dynamic_context.keyword).empty()) { return {make(sourcemeta::blaze::InstructionIndex::AssertionFail, context, schema_context, dynamic_context, ValueNone{})}; @@ -1954,14 +2139,20 @@ auto compiler_draft3_validation_enum(const Context &context, sourcemeta::core::PropertyHashJSON hasher; for (const auto &option : schema_context.schema.at(dynamic_context.keyword).as_array()) { + // Neither 2019-09 onwards nor the OpenAPI 3.0 Schema Object asks for the + // options to be unique, and the comparison below weighs these hashes + // against the deduplicated set of options, so a repeated option must not + // be counted twice + if (!options.insert(option).second) { + continue; + } + if (option.is_string()) { const auto hash{hasher(option.to_string())}; if (hasher.is_perfect(hash)) { perfect_string_hashes.emplace_back(option.to_string(), hash); } } - - options.insert(option); } // Only apply this optimisation on fast validation, as it @@ -2000,9 +2191,8 @@ auto compiler_draft3_validation_maxlength(const Context &context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral() || - (!integral_reals_are_integers(schema_context.vocabularies) && - !schema_context.schema.at(dynamic_context.keyword).is_integer())) { + if (!is_dialect_integer(schema_context.schema.at(dynamic_context.keyword), + schema_context.vocabularies)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_INTEGER); @@ -2039,9 +2229,15 @@ auto compiler_draft3_validation_maxlength(const Context &context, // property name, so there we must still emit them ourselves if (context.mode == Mode::FastValidation && !schema_context.is_property_name && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "string") { + assume_type(schema_context.vocabularies, schema_context.schema, + "string")) { + return {}; + } + + // A bound too large to hold sits beyond the length of anything that can + // exist, so every string meets it and there is nothing left to assert + if (!bound_is_representable( + schema_context.schema.at(dynamic_context.keyword))) { return {}; } @@ -2059,9 +2255,8 @@ auto compiler_draft3_validation_minlength(const Context &context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral() || - (!integral_reals_are_integers(schema_context.vocabularies) && - !schema_context.schema.at(dynamic_context.keyword).is_integer())) { + if (!is_dialect_integer(schema_context.schema.at(dynamic_context.keyword), + schema_context.vocabularies)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_INTEGER); @@ -2077,14 +2272,29 @@ auto compiler_draft3_validation_minlength(const Context &context, return {}; } + // A bound too large to hold is beyond the length of anything that can + // exist, so no string can meet it, and nothing else is affected. A property + // name is always a string, which leaves the type check nothing to spare + if (!bound_is_representable( + schema_context.schema.at(dynamic_context.keyword))) { + if (schema_context.is_property_name) { + return {make(sourcemeta::blaze::InstructionIndex::AssertionFail, context, + schema_context, dynamic_context, ValueNone{})}; + } + + ValueTypes types; + types.set(std::to_underlying(sourcemeta::core::JSON::Type::String)); + return {make(sourcemeta::blaze::InstructionIndex::AssertionNotTypeStrictAny, + context, schema_context, dynamic_context, types)}; + } + // We'll handle it at the type level as an optimization. Note that the type // compiler bails out before it reads these bounds when it is compiling a // property name, so there we must still emit them ourselves if (context.mode == Mode::FastValidation && !schema_context.is_property_name && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "string") { + assume_type(schema_context.vocabularies, schema_context.schema, + "string")) { return {}; } @@ -2103,9 +2313,8 @@ auto compiler_draft3_validation_maxitems(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral() || - (!integral_reals_are_integers(schema_context.vocabularies) && - !schema_context.schema.at(dynamic_context.keyword).is_integer())) { + if (!is_dialect_integer(schema_context.schema.at(dynamic_context.keyword), + schema_context.vocabularies)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_INTEGER); @@ -2123,9 +2332,15 @@ auto compiler_draft3_validation_maxitems(const Context &context, // We'll handle it at the type level as an optimization if (context.mode == Mode::FastValidation && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "array") { + assume_type(schema_context.vocabularies, schema_context.schema, + "array")) { + return {}; + } + + // A bound too large to hold sits beyond the size of anything that can + // exist, so every array meets it and there is nothing left to assert + if (!bound_is_representable( + schema_context.schema.at(dynamic_context.keyword))) { return {}; } @@ -2142,9 +2357,8 @@ auto compiler_draft3_validation_minitems(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral() || - (!integral_reals_are_integers(schema_context.vocabularies) && - !schema_context.schema.at(dynamic_context.keyword).is_integer())) { + if (!is_dialect_integer(schema_context.schema.at(dynamic_context.keyword), + schema_context.vocabularies)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_INTEGER); @@ -2160,11 +2374,25 @@ auto compiler_draft3_validation_minitems(const Context &context, return {}; } + // A bound too large to hold is beyond the size of anything that can + // exist, so no array can meet it, and nothing else is affected. A property + // name is always a string, which no array bound applies to + if (!bound_is_representable( + schema_context.schema.at(dynamic_context.keyword))) { + if (schema_context.is_property_name) { + return {}; + } + + ValueTypes types; + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Array)); + return {make(sourcemeta::blaze::InstructionIndex::AssertionNotTypeStrictAny, + context, schema_context, dynamic_context, types)}; + } + // We'll handle it at the type level as an optimization if (context.mode == Mode::FastValidation && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "array") { + assume_type(schema_context.vocabularies, schema_context.schema, + "array")) { return {}; } @@ -2243,6 +2471,33 @@ auto compiler_draft3_validation_minimum(const Context &context, schema_context.schema.at(dynamic_context.keyword)})}; } +// Whether the name was one this function knows, so that callers can tell an +// unrecognised name apart from one that set no bit +auto assign_type_bit(sourcemeta::blaze::ValueTypes &types, + const std::string_view name) -> bool { + if (name == "null") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Null)); + } else if (name == "boolean") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Boolean)); + } else if (name == "object") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Object)); + } else if (name == "array") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Array)); + } else if (name == "number") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); + } else if (name == "integer") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); + } else if (name == "string") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::String)); + } else { + return false; + } + + return true; +} + auto compiler_draft3_validation_type(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, @@ -2254,6 +2509,32 @@ auto compiler_draft3_validation_type(const Context &context, schema_context.vocabularies.contains(Known::JSON_SCHEMA_DRAFT_3) || schema_context.vocabularies.contains(Known::JSON_SCHEMA_DRAFT_3_HYPER)}; + // This dialect types `type` as a string drawn from six names. It has no + // array form and no null type, offering `nullable` in place of the latter. + // `assign_type_bit` answers for the seven JSON Schema names, so the null one + // has to be ruled out separately + if (schema_context.vocabularies.contains(Known::OPENAPI_3_0_BASE)) { + ValueTypes recognised{}; + if (!value.is_string() || value.to_string() == "null" || + !assign_type_bit(recognised, value.to_string())) { + throw sourcemeta::blaze::CompilerError( + schema_context.base, + absolute_schema_location(context, schema_context), + UNSUPPORTED_VALUE_FOR_DIALECT); + } + + // A scalar `type` widened by `nullable` is the array form in all but + // spelling, so it takes the same instruction and skips the scalar fast + // paths below, none of which can express a bound alongside a second + // permitted type + if (is_nullable(schema_context.vocabularies, schema_context.schema)) { + ValueTypes types{recognised}; + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Null)); + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrictAny, + context, schema_context, dynamic_context, types)}; + } + } + if (is_draft3) { if (value.is_string() && value.to_string() == "any") { return draft3_any_type_instructions(context, schema_context, @@ -2501,16 +2782,12 @@ auto compiler_draft3_validation_type(const Context &context, context, schema_context, dynamic_context, types)}; } if (type == "integer") { - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("enum") && - schema_context.schema.at("enum").is_array() && - std::ranges::all_of(schema_context.schema.at("enum").as_array(), - [](const auto &candidate) -> auto { - return candidate.is_integer(); - })) { - return {}; - } - + // Draft 4 and earlier have no shortcut for an all-integer `enum` here, + // and neither does the OpenAPI 3.0 Schema Object, unlike every other + // type name. Equality reads a number by its value rather than by how it + // is written, so the options admit an instance carrying a fractional + // part, which none of these dialects counts as an integer, and so this + // assertion always has work left to do return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, context, schema_context, dynamic_context, sourcemeta::core::JSON::Type::Integer)}; @@ -2633,24 +2910,7 @@ auto compiler_draft3_validation_type(const Context &context, continue; } - const auto &type_string{element.to_string()}; - if (type_string == "null") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Null)); - } else if (type_string == "boolean") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Boolean)); - } else if (type_string == "object") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Object)); - } else if (type_string == "array") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Array)); - } else if (type_string == "number") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); - } else if (type_string == "integer") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); - } else if (type_string == "string") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::String)); - } else if (!is_draft3) { + if (!assign_type_bit(types, element.to_string()) && !is_draft3) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), @@ -2964,6 +3224,22 @@ auto compiler_draft3_validation_divisibleby( schema_context.schema.at(dynamic_context.keyword)})}; } +// The formats that the OpenAPI 3.1 and 3.2 specifications define for numbers, +// which the rest of this compiler treats apart from the string ones +auto is_openapi_numeric_format(const SchemaContext &schema_context, + const sourcemeta::core::JSON &format) -> bool { + using Known = sourcemeta::core::SchemaVocabularies::Known; + if (!format.is_string() || + (!schema_context.vocabularies.contains(Known::OPENAPI_3_1_BASE) && + !schema_context.vocabularies.contains(Known::OPENAPI_3_2_BASE))) { + return false; + } + + const auto &name{format.to_string()}; + return name == "int32" || name == "int64" || name == "float" || + name == "double"; +} + auto compiler_draft3_validation_format(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, @@ -2986,7 +3262,6 @@ auto compiler_draft3_validation_format(const Context &context, schema_context.schema.at("x-format-assertion").to_boolean()}; const auto assert_active{context.tweaks.format_assertion || force_assertion}; - if ((is_2019_09_format && assert_active) || is_2020_12_format_assertion || (is_2020_12_format_annotation && assert_active)) { const auto &format{schema_context.schema.at(dynamic_context.keyword)}; @@ -2995,6 +3270,47 @@ auto compiler_draft3_validation_format(const Context &context, } const auto &name{format.to_string()}; + + // These are the only formats the OpenAPI 3.1 and 3.2 specifications + // define themselves, and supporting any other entry of the Format + // Registry they defer to is strictly optional + // TODO: Consider asserting the numeric and string formats that the + // OpenAPI Format Registry defines + if (is_openapi_numeric_format(schema_context, format)) { + Instructions instructions; + if (name == "int32") { + instructions.push_back(make( + sourcemeta::blaze::InstructionIndex::AssertionNumberIntegerBounded, + context, schema_context, dynamic_context, + ValueIntegerBounds{std::numeric_limits::min(), + std::numeric_limits::max()})); + } else if (name == "int64") { + instructions.push_back(make( + sourcemeta::blaze::InstructionIndex::AssertionNumberIntegerBounded, + context, schema_context, dynamic_context, + ValueIntegerBounds{std::numeric_limits::min(), + std::numeric_limits::max()})); + } else { + instructions.push_back( + make(sourcemeta::blaze::InstructionIndex::AssertionNumberType, + context, schema_context, dynamic_context, + name == "float" ? ValueNumberType::Float + : ValueNumberType::Double)); + } + + // Unlike the string formats, the annotation cannot be grouped by the + // instance type, as `ControlGroupWhenType` does not take a number + if (annotations_enabled(context, dynamic_context.keyword)) { + instructions.push_back( + make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, + schema_context, dynamic_context, + sourcemeta::core::JSON{ + schema_context.schema.at(dynamic_context.keyword)})); + } + + return instructions; + } + ValueStringType type; if (name == "date-time") { type = ValueStringType::DateTime; @@ -3068,6 +3384,15 @@ auto compiler_draft3_validation_format(const Context &context, sourcemeta::core::JSON{ schema_context.schema.at(dynamic_context.keyword)})}; + // A format that applies to numbers cannot have its annotation grouped by + // the instance type, so emit it the way the assertion path does rather + // than collect it for strings alone + if (is_openapi_numeric_format( + schema_context, + schema_context.schema.at(dynamic_context.keyword))) { + return children; + } + return {make(sourcemeta::blaze::InstructionIndex::ControlGroupWhenType, context, schema_context, relative_dynamic_context(), ValueType::String, std::move(children))}; @@ -3098,7 +3423,52 @@ auto compiler_draft3_validation_format(const Context &context, schema_context.vocabularies.contains(Known::JSON_SCHEMA_DRAFT_7) || schema_context.vocabularies.contains(Known::JSON_SCHEMA_DRAFT_7_HYPER)}; - if (is_draft7) { + if (schema_context.vocabularies.contains(Known::OPENAPI_3_0_BASE)) { + // OpenAPI 3.0 defines these two for numbers, so they cannot resolve to a + // string logical type like the rest of this chain does + if (name == "int32" || name == "int64") { + return {make( + sourcemeta::blaze::InstructionIndex::AssertionNumberIntegerBounded, + context, schema_context, dynamic_context, + name == "int32" + ? ValueIntegerBounds{std::numeric_limits::min(), + std::numeric_limits::max()} + : ValueIntegerBounds{std::numeric_limits::min(), + std::numeric_limits::max()})}; + } + + if (name == "float" || name == "double") { + return {make(sourcemeta::blaze::InstructionIndex::AssertionNumberType, + context, schema_context, dynamic_context, + name == "float" ? ValueNumberType::Float + : ValueNumberType::Double)}; + } + + if (name == "date-time") { + type = ValueStringType::DateTime; + } else if (name == "date") { + type = ValueStringType::Date; + } else if (name == "byte") { + type = ValueStringType::Base64; + } else if (name == "email") { + // The Schema Object is a subset of JSON Schema Draft Wright-00, whose + // own formats apply alongside the ones the specification tabulates + type = ValueStringType::Email; + } else if (name == "hostname") { + type = ValueStringType::Hostname; + } else if (name == "ipv4") { + type = ValueStringType::IPv4; + } else if (name == "ipv6") { + type = ValueStringType::IPv6; + } else if (name == "uri") { + type = ValueStringType::URI; + } else { + // `binary` stands for unencoded binary data, which a JSON string cannot + // carry, and `password` is a hint to obscure the value, so neither of + // the two remaining formats constrains the instance + return {}; + } + } else if (is_draft7) { if (name == "date-time") { type = ValueStringType::DateTime; } else if (name == "date") { diff --git a/vendor/blaze/src/compiler/default_compiler_draft4.h b/vendor/blaze/src/compiler/default_compiler_draft4.h index c69beae56..879169a12 100644 --- a/vendor/blaze/src/compiler/default_compiler_draft4.h +++ b/vendor/blaze/src/compiler/default_compiler_draft4.h @@ -20,10 +20,12 @@ auto compiler_draft4_validation_required(const Context &context, const DynamicContext &dynamic_context, const Instructions ¤t) -> Instructions { - // Draft 4 alone asks that `required` name at least one property + // Draft 4 asks that `required` name at least one property, and the OpenAPI + // 3.0 Schema Object, whose meta-schema is a Draft 4 document, asks the same using Known = sourcemeta::core::SchemaVocabularies::Known; const auto allows_empty{!schema_context.vocabularies.contains_any( - {Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER})}; + {Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER, + Known::OPENAPI_3_0_BASE})}; if (!is_string_array(schema_context.schema.at(dynamic_context.keyword)) || (!allows_empty && schema_context.schema.at(dynamic_context.keyword).empty())) { @@ -42,8 +44,10 @@ auto compiler_draft4_applicator_allof(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!is_schema_array(schema_context.schema.at(dynamic_context.keyword), - booleans_are_schemas(schema_context.vocabularies))) { + if (!is_schema_array( + schema_context.schema.at(dynamic_context.keyword), + booleans_are_schemas(schema_context.vocabularies), + in_place_applicators_may_be_empty(schema_context.vocabularies))) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_SCHEMA_ARRAY); @@ -90,13 +94,23 @@ auto compiler_draft4_applicator_anyof(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!is_schema_array(schema_context.schema.at(dynamic_context.keyword), - booleans_are_schemas(schema_context.vocabularies))) { + if (!is_schema_array( + schema_context.schema.at(dynamic_context.keyword), + booleans_are_schemas(schema_context.vocabularies), + in_place_applicators_may_be_empty(schema_context.vocabularies))) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_SCHEMA_ARRAY); } + // A disjunction over no branches can never be satisfied. Only a dialect that + // permits the empty array gets here, as the shape check above refuses it + // everywhere else + if (schema_context.schema.at(dynamic_context.keyword).empty()) { + return {make(sourcemeta::blaze::InstructionIndex::AssertionFail, context, + schema_context, dynamic_context, ValueNone{})}; + } + Instructions disjunctors; for (std::uint64_t index = 0; index < schema_context.schema.at(dynamic_context.keyword).size(); @@ -166,13 +180,23 @@ auto compiler_draft4_applicator_oneof(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!is_schema_array(schema_context.schema.at(dynamic_context.keyword), - booleans_are_schemas(schema_context.vocabularies))) { + if (!is_schema_array( + schema_context.schema.at(dynamic_context.keyword), + booleans_are_schemas(schema_context.vocabularies), + in_place_applicators_may_be_empty(schema_context.vocabularies))) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_SCHEMA_ARRAY); } + // A disjunction over no branches can never be satisfied. Only a dialect that + // permits the empty array gets here, as the shape check above refuses it + // everywhere else + if (schema_context.schema.at(dynamic_context.keyword).empty()) { + return {make(sourcemeta::blaze::InstructionIndex::AssertionFail, context, + schema_context, dynamic_context, ValueNone{})}; + } + Instructions disjunctors; for (std::uint64_t index = 0; index < schema_context.schema.at(dynamic_context.keyword).size(); @@ -231,9 +255,8 @@ auto compiler_draft4_validation_maxproperties( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral() || - (!integral_reals_are_integers(schema_context.vocabularies) && - !schema_context.schema.at(dynamic_context.keyword).is_integer())) { + if (!is_dialect_integer(schema_context.schema.at(dynamic_context.keyword), + schema_context.vocabularies)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_INTEGER); @@ -253,9 +276,15 @@ auto compiler_draft4_validation_maxproperties( // We'll handle it at the type level as an optimization if (context.mode == Mode::FastValidation && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "object") { + assume_type(schema_context.vocabularies, schema_context.schema, + "object")) { + return {}; + } + + // A bound too large to hold sits beyond the size of anything that can + // exist, so every object meets it and there is nothing left to assert + if (!bound_is_representable( + schema_context.schema.at(dynamic_context.keyword))) { return {}; } @@ -272,9 +301,8 @@ auto compiler_draft4_validation_minproperties( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral() || - (!integral_reals_are_integers(schema_context.vocabularies) && - !schema_context.schema.at(dynamic_context.keyword).is_integer())) { + if (!is_dialect_integer(schema_context.schema.at(dynamic_context.keyword), + schema_context.vocabularies)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_INTEGER); @@ -292,11 +320,25 @@ auto compiler_draft4_validation_minproperties( return {}; } + // A bound too large to hold is beyond the size of anything that can + // exist, so no object can meet it, and nothing else is affected. A property + // name is always a string, which no object bound applies to + if (!bound_is_representable( + schema_context.schema.at(dynamic_context.keyword))) { + if (schema_context.is_property_name) { + return {}; + } + + ValueTypes types; + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Object)); + return {make(sourcemeta::blaze::InstructionIndex::AssertionNotTypeStrictAny, + context, schema_context, dynamic_context, types)}; + } + // We'll handle it at the type level as an optimization if (context.mode == Mode::FastValidation && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "object") { + assume_type(schema_context.vocabularies, schema_context.schema, + "object")) { return {}; } diff --git a/vendor/blaze/src/compiler/default_compiler_draft6.h b/vendor/blaze/src/compiler/default_compiler_draft6.h index 5b1a1e7d3..d4482eefa 100644 --- a/vendor/blaze/src/compiler/default_compiler_draft6.h +++ b/vendor/blaze/src/compiler/default_compiler_draft6.h @@ -151,20 +151,14 @@ auto compiler_draft6_validation_type(const Context &context, // A preceding array loop at this location already rejects a non-array, // so the type assertion is redundant. The array size bounds are only - // redundant when there are none to enforce, as the loops other than the - // sized one do not check the item count. Only a loop that truly rejects - // a non-array qualifies: some item loops instead pass a non-array - // untouched, so they are deliberately excluded below + // redundant when there are none to enforce, as the loop may be + // carrying bounds fused from a different schema. Only a loop that + // truly rejects a non-array qualifies: every other item loop holds + // vacuously on a non-array, as `items` asserts nothing on one if (context.mode == Mode::FastValidation && minimum == 0 && !maximum.has_value() && !current.empty() && - (current.back().type == - sourcemeta::blaze::InstructionIndex:: - LoopItemsPropertiesExactlyTypeStrictHash || - current.back().type == - sourcemeta::blaze::InstructionIndex:: - LoopItemsPropertiesExactlyTypeStrictHash3 || - current.back().type == sourcemeta::blaze::InstructionIndex:: - LoopItemsIntegerBoundedSized) && + current.back().type == sourcemeta::blaze::InstructionIndex:: + LoopItemsIntegerBoundedSized && current.back().relative_instance_location == to_pointer(dynamic_context.base_instance_location)) { return {}; diff --git a/vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler.h b/vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler.h index 4dd7271b0..acdc3ea99 100644 --- a/vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler.h +++ b/vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler.h @@ -6,7 +6,6 @@ #endif #include -#include #include @@ -21,6 +20,7 @@ #include // std::numeric_limits #include // std::map #include // std::optional, std::nullopt +#include // std::set #include // std::string #include // std::string_view #include // std::tuple @@ -34,6 +34,23 @@ namespace sourcemeta::blaze { +/// @ingroup compiler +struct SchemaUnevaluatedEntry { + /// The absolute pointers of the static keyword dependencies + std::set static_dependencies; + /// The absolute pointers of the dynamic keyword dependencies, along with the + /// references that lead to them + std::set dynamic_dependencies; + /// Whether the entry cannot be fully resolved, which means + /// there might be unknown dynamic dependencies + bool unresolved{false}; +}; + +/// @ingroup compiler +/// The flattened set of unevaluated cases in the schema by absolute URI +using SchemaUnevaluatedEntries = + std::map; + /// @ingroup compiler /// The schema compiler context is the current subschema information you have at /// your disposal to implement a keyword diff --git a/vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler_unevaluated.h b/vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler_unevaluated.h deleted file mode 100644 index daab62965..000000000 --- a/vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler_unevaluated.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef SOURCEMETA_BLAZE_COMPILER_UNEVALUATED_H -#define SOURCEMETA_BLAZE_COMPILER_UNEVALUATED_H - -#ifndef SOURCEMETA_BLAZE_COMPILER_EXPORT -#include -#endif - -#include -#include -#include - -#include // std::map -#include // std::set - -// TODO: Eventually this file should dissapear and move this analysis as part of -// framing - -namespace sourcemeta::blaze { - -/// @ingroup compiler -struct SchemaUnevaluatedEntry { - /// The absolute pointers of the static keyword dependencies - std::set static_dependencies; - /// The absolute pointers of the dynamic keyword dependencies, along with the - /// references that lead to them - std::set dynamic_dependencies; - /// Whether the entry cannot be fully resolved, which means - /// there might be unknown dynamic dependencies - bool unresolved{false}; -}; - -/// @ingroup compiler -/// The flattened set of unevaluated cases in the schema by absolute URI -using SchemaUnevaluatedEntries = - std::map; - -/// @ingroup compiler -/// -/// This function performs a static analysis pass on `unevaluatedProperties` and -/// `unevaluatedItems` occurences throughout the entire schema (if any). -auto SOURCEMETA_BLAZE_COMPILER_EXPORT -unevaluated(const sourcemeta::core::JSON &schema, - const sourcemeta::core::SchemaFrame &frame, - const sourcemeta::core::SchemaWalker &walker, - const sourcemeta::core::SchemaResolver &resolver) - -> SchemaUnevaluatedEntries; - -} // namespace sourcemeta::blaze - -#endif diff --git a/vendor/blaze/src/compiler/postprocess.h b/vendor/blaze/src/compiler/postprocess.h index b5a1e0188..58b8cab0c 100644 --- a/vendor/blaze/src/compiler/postprocess.h +++ b/vendor/blaze/src/compiler/postprocess.h @@ -207,7 +207,9 @@ inline auto fuse_numeric_bounds(Instructions &instructions, for (std::size_t type_index = 0; type_index < instructions.size(); ++type_index) { const auto &type_instruction{instructions[type_index]}; - if (!is_numeric_integer_type_check(type_instruction)) { + const auto anchors_on_type{is_numeric_integer_type_check(type_instruction)}; + if (!anchors_on_type && + !is_numeric_integral_multiple_check(type_instruction)) { continue; } @@ -220,6 +222,7 @@ inline auto fuse_numeric_bounds(Instructions &instructions, std::vector bound_indices; std::optional minimum; std::optional maximum; + std::optional minimum_index; for (std::size_t index = 0; index < instructions.size(); ++index) { const auto &candidate{instructions[index]}; @@ -232,10 +235,15 @@ inline auto fuse_numeric_bounds(Instructions &instructions, continue; } + const auto previous_minimum{minimum}; if (!merge_integer_bound(candidate, minimum, maximum)) { continue; } + if (minimum != previous_minimum) { + minimum_index = index; + } + bound_indices.push_back(index); } @@ -249,7 +257,16 @@ inline auto fuse_numeric_bounds(Instructions &instructions, InstructionIndex fused_type; Value fused_value; - if (minimum.has_value() && maximum.has_value()) { + if (!anchors_on_type) { + // Without a type check to absorb there is no lower-bound-only form to + // fall back on, as the instruction has to carry both ends of the range + if (!minimum.has_value() || !maximum.has_value()) { + continue; + } + + fused_type = InstructionIndex::AssertionNumberIntegerBounded; + fused_value = ValueIntegerBounds{minimum.value(), maximum.value()}; + } else if (minimum.has_value() && maximum.has_value()) { fused_type = is_strict ? InstructionIndex::AssertionTypeIntegerBoundedStrict : InstructionIndex::AssertionTypeIntegerBounded; @@ -265,11 +282,14 @@ inline auto fuse_numeric_bounds(Instructions &instructions, const auto first_index{ std::min(type_index, *std::ranges::min_element(bound_indices))}; + const auto &minimum_metadata{ + extra[instructions[minimum_index.value()].extra_index]}; const auto extra_index{extra.size()}; - extra.push_back({.relative_schema_location = parent->first, - .keyword_location = parent->second, - .schema_resource = type_metadata.schema_resource, - .vocabulary = type_metadata.vocabulary}); + extra.push_back( + {.relative_schema_location = minimum_metadata.relative_schema_location, + .keyword_location = minimum_metadata.keyword_location, + .schema_resource = type_metadata.schema_resource, + .vocabulary = type_metadata.vocabulary}); Instructions result; result.reserve(instructions.size() - bound_indices.size()); diff --git a/vendor/blaze/src/compiler/schema_helpers.h b/vendor/blaze/src/compiler/schema_helpers.h index ba69c4fb8..4b3dff553 100644 --- a/vendor/blaze/src/compiler/schema_helpers.h +++ b/vendor/blaze/src/compiler/schema_helpers.h @@ -28,6 +28,7 @@ inline auto parse_schema_type_string(const sourcemeta::core::JSON::String &type, } else if (type == "number") { result.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); result.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); + result.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); } else if (type == "integer") { result.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); } else if (type == "string") { diff --git a/vendor/blaze/src/compiler/unevaluated.cc b/vendor/blaze/src/compiler/unevaluated.cc deleted file mode 100644 index 77c418d1f..000000000 --- a/vendor/blaze/src/compiler/unevaluated.cc +++ /dev/null @@ -1,311 +0,0 @@ -#include -#include - -#include "compile_helpers.h" - -#include // std::set -#include // std::pair - -namespace { -using namespace sourcemeta::core; -using namespace sourcemeta::blaze; -using Known = SchemaVocabularies::Known; - -// NOLINTBEGIN(cert-err58-cpp,bugprone-throwing-static-initialization) -const std::string UNEVALUATED_PROPERTIES{"unevaluatedProperties"}; -const std::string UNEVALUATED_ITEMS{"unevaluatedItems"}; -// NOLINTEND(cert-err58-cpp,bugprone-throwing-static-initialization) - -// Whether a location is already being visited has to survive the nested calls -// that walk through it, and stop applying the moment they unwind -class PathGuard { -public: - PathGuard(std::set> &visited, - const WeakPointer &pointer, const bool is_static) - : visited_{visited}, entry_{pointer, is_static}, - first_visit_{visited.insert(this->entry_).second} {} - - ~PathGuard() { - if (this->first_visit_) { - this->visited_.erase(this->entry_); - } - } - - [[nodiscard]] auto first_visit() const -> bool { return this->first_visit_; } - - PathGuard(const PathGuard &) = delete; - auto operator=(const PathGuard &) -> PathGuard & = delete; - PathGuard(PathGuard &&) = delete; - auto operator=(PathGuard &&) -> PathGuard & = delete; - -private: - std::set> &visited_; - const std::pair entry_; - const bool first_visit_; -}; - -auto find_adjacent_dependencies( - const JSON::String ¤t, const JSON &schema, const SchemaFrame &frame, - const SchemaWalker &walker, const SchemaResolver &resolver, - const std::set &keywords, const SchemaFrame::Location &root, - const SchemaFrame::Location &entry, const bool is_static, - std::set> &visited, - sourcemeta::blaze::SchemaUnevaluatedEntry &result) -> void { - // A schema may reference itself, directly or through a chain of in-place - // applicators. Following such a cycle forever exhausts the stack, so we cut - // it as soon as we meet a location already on the path we came in through. - // The mark only lives as long as that path, as two references to a common - // target are not a cycle, and each of them still has to be accounted for - const PathGuard guard{visited, entry.pointer, is_static}; - if (!guard.first_visit()) { - return; - } - - const auto &subschema{get(schema, entry.pointer)}; - if (!subschema.is_object()) { - return; - } - - const auto &subschema_vocabularies{frame.vocabularies(entry, resolver)}; - - for (const auto &property : subschema.as_object()) { - if (property.first == current && entry.pointer == root.pointer) { - continue; - } - if (keywords.contains(property.first)) { - // In 2019-09, `additionalItems` takes no effect without `items` - if (subschema_vocabularies.contains( - Known::JSON_SCHEMA_2019_09_APPLICATOR) && - property.first == "additionalItems" && !subschema.defines("items")) { - continue; - } - - auto pointer{entry.pointer.concat(make_weak_pointer(property.first))}; - if (is_static) { - result.static_dependencies.emplace(std::move(pointer)); - } else { - result.dynamic_dependencies.emplace(std::move(pointer)); - } - - continue; - } - - switch (walker(property.first, subschema_vocabularies).type) { - // References - case SchemaKeywordType::Reference: { - const auto reference{ - frame.dereference(entry, make_weak_pointer(property.first))}; - if (reference.first == SchemaReferenceType::Static && - reference.second.has_value()) { - // Recurse into a dedicated entry so that whether this reference's - // target contributes any dynamic dependency can be read directly, - // rather than inferred from whether it grew the shared deduplicated - // set, which misses every reference after the first to a common - // target - SchemaUnevaluatedEntry nested; - find_adjacent_dependencies( - current, schema, frame, walker, resolver, keywords, root, - reference.second.value().get(), is_static, visited, nested); - - // Whatever the target contributes gets recorded at the location of - // the target itself, which tells the applicators this reference sits - // under nothing about it. Record the reference as a dependency of - // its own too, so that they can still tell that reaching through it - // leads to one, and therefore that they cannot short-circuit. Only - // the dynamic dependencies are consulted that way, whereas the - // static ones name the keyword locations that evaluate, which a - // reference is not one of - if (!is_static && !nested.dynamic_dependencies.empty()) { - result.dynamic_dependencies.emplace( - entry.pointer.concat(make_weak_pointer(property.first))); - } - - result.unresolved = result.unresolved || nested.unresolved; - result.static_dependencies.merge(nested.static_dependencies); - result.dynamic_dependencies.merge(nested.dynamic_dependencies); - } else if (reference.first == SchemaReferenceType::Dynamic) { - result.unresolved = true; - } - - break; - } - - // Static - case SchemaKeywordType::ApplicatorElementsInPlace: - // TODO(C++23): Use std::views::enumerate when available in libc++ - for (std::size_t index = 0; index < property.second.size(); index++) { - find_adjacent_dependencies( - current, schema, frame, walker, resolver, keywords, root, - frame.traverse(entry, make_weak_pointer(property.first, index)), - is_static, visited, result); - } - - break; - - // Dynamic - case SchemaKeywordType::ApplicatorElementsInPlaceSome: - if (property.second.is_array()) { - for (std::size_t index = 0; index < property.second.size(); index++) { - find_adjacent_dependencies( - current, schema, frame, walker, resolver, keywords, root, - frame.traverse(entry, make_weak_pointer(property.first, index)), - false, visited, result); - } - } - - break; - case SchemaKeywordType::ApplicatorValueTraverseAnyItem: - [[fallthrough]]; - case SchemaKeywordType::ApplicatorValueTraverseParent: - [[fallthrough]]; - case SchemaKeywordType::ApplicatorValueInPlaceMaybe: - if ((property.second.is_object() || property.second.is_boolean())) { - find_adjacent_dependencies( - current, schema, frame, walker, resolver, keywords, root, - frame.traverse(entry, make_weak_pointer(property.first)), false, - visited, result); - } - - break; - case SchemaKeywordType::ApplicatorValueOrElementsInPlace: - if (property.second.is_array()) { - for (std::size_t index = 0; index < property.second.size(); index++) { - find_adjacent_dependencies( - current, schema, frame, walker, resolver, keywords, root, - frame.traverse(entry, make_weak_pointer(property.first, index)), - false, visited, result); - } - } else if ((property.second.is_object() || - property.second.is_boolean())) { - find_adjacent_dependencies( - current, schema, frame, walker, resolver, keywords, root, - frame.traverse(entry, make_weak_pointer(property.first)), false, - visited, result); - } - - break; - case SchemaKeywordType::ApplicatorMembersInPlaceSome: - if (property.second.is_object()) { - for (const auto &pair : property.second.as_object()) { - find_adjacent_dependencies( - current, schema, frame, walker, resolver, keywords, root, - frame.traverse(entry, - make_weak_pointer(property.first, pair.first)), - false, visited, result); - } - } - - break; - - // Anything else does not contribute to the dependency list - default: - break; - } - } -} - -auto register_under_all_bases(SchemaUnevaluatedEntries &result, - const SchemaFrame &frame, - const SchemaFrame::Location &location, - const JSON::String &keyword, - const SchemaUnevaluatedEntry &value) -> void { - result.emplace(frame.uri(location, make_weak_pointer(keyword)), value); - frame.for_each_location([&](const SchemaReferenceType, const std::string_view, - const SchemaFrame::Location &alternate) -> void { - if (alternate.pointer != location.pointer || - alternate.base == location.base) { - return; - } - - if (alternate.type != SchemaFrame::LocationType::Subschema && - alternate.type != SchemaFrame::LocationType::Resource && - alternate.type != SchemaFrame::LocationType::Anchor) { - return; - } - - result.emplace(frame.uri(alternate, make_weak_pointer(keyword)), value); - }); -} - -} // namespace - -namespace sourcemeta::blaze { - -// TODO: Refactor this entire function using `SchemaFrame`'s new `Instances` -// mode. We can loop over every subschema that defines `unevaluatedProperties` -// or `unevaluatedItems`, find all other subschemas with the same unresolved -// instance location (static dependency) or conditional equivalent unresolved -// instance location (dynamic dependency) and see if those ones define any of -// the dependent keywords. -auto unevaluated(const JSON &schema, const SchemaFrame &frame, - const SchemaWalker &walker, const SchemaResolver &resolver) - -> SchemaUnevaluatedEntries { - SchemaUnevaluatedEntries result; - - frame.for_each_subschema([&](const SchemaFrame::Location &location) -> void { - const auto &subschema{get(schema, location.pointer)}; - assert((subschema.is_object() || subschema.is_boolean())); - if (!subschema.is_object()) { - return; - } - - const bool has_unevaluated_properties{ - subschema.defines("unevaluatedProperties")}; - const bool has_unevaluated_items{subschema.defines("unevaluatedItems")}; - if (!has_unevaluated_properties && !has_unevaluated_items) { - return; - } - - const auto &subschema_vocabularies{frame.vocabularies(location, resolver)}; - - // The same pointer may be reachable through alternate identifiers whose - // dynamic anchors carry a different base, so we register the entry under - // each of them - if (has_unevaluated_properties) { - if ((subschema_vocabularies.contains( - Known::JSON_SCHEMA_2020_12_UNEVALUATED) && - subschema_vocabularies.contains( - Known::JSON_SCHEMA_2020_12_APPLICATOR)) || - subschema_vocabularies.contains( - Known::JSON_SCHEMA_2019_09_APPLICATOR)) { - std::set> visited; - SchemaUnevaluatedEntry unevaluated; - find_adjacent_dependencies( - "unevaluatedProperties", schema, frame, walker, resolver, - {"properties", "patternProperties", "additionalProperties", - "unevaluatedProperties"}, - location, location, true, visited, unevaluated); - register_under_all_bases(result, frame, location, - UNEVALUATED_PROPERTIES, unevaluated); - } - } - - if (has_unevaluated_items) { - std::set> visited; - SchemaUnevaluatedEntry unevaluated; - if (subschema_vocabularies.contains( - Known::JSON_SCHEMA_2020_12_UNEVALUATED) && - subschema_vocabularies.contains( - Known::JSON_SCHEMA_2020_12_APPLICATOR)) { - find_adjacent_dependencies( - "unevaluatedItems", schema, frame, walker, resolver, - {"prefixItems", "items", "contains", "unevaluatedItems"}, location, - location, true, visited, unevaluated); - register_under_all_bases(result, frame, location, UNEVALUATED_ITEMS, - unevaluated); - } else if (subschema_vocabularies.contains( - Known::JSON_SCHEMA_2019_09_APPLICATOR)) { - find_adjacent_dependencies( - "unevaluatedItems", schema, frame, walker, resolver, - {"items", "additionalItems", "unevaluatedItems"}, location, - location, true, visited, unevaluated); - register_under_all_bases(result, frame, location, UNEVALUATED_ITEMS, - unevaluated); - } - } - }); - - return result; -} - -} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/compiler/unevaluated.h b/vendor/blaze/src/compiler/unevaluated.h new file mode 100644 index 000000000..2eecf332c --- /dev/null +++ b/vendor/blaze/src/compiler/unevaluated.h @@ -0,0 +1,351 @@ +#ifndef SOURCEMETA_BLAZE_COMPILER_UNEVALUATED_H_ +#define SOURCEMETA_BLAZE_COMPILER_UNEVALUATED_H_ + +#include + +#include +#include + +#include // assert +#include // std::size_t +#include // std::set +#include // std::string_view +#include // std::move, std::pair + +#include "compile_helpers.h" + +namespace sourcemeta::blaze { + +// NOLINTBEGIN(cert-err58-cpp,bugprone-throwing-static-initialization) +static const sourcemeta::core::JSON::String UNEVALUATED_PROPERTIES{ + "unevaluatedProperties"}; +static const sourcemeta::core::JSON::String UNEVALUATED_ITEMS{ + "unevaluatedItems"}; +// NOLINTEND(cert-err58-cpp,bugprone-throwing-static-initialization) + +// Whether a location is already being visited has to survive the nested calls +// that walk through it, and stop applying the moment they unwind +class PathGuard { +public: + PathGuard(std::set> &visited, + const sourcemeta::core::WeakPointer &pointer, const bool is_static) + : visited_{visited}, entry_{pointer, is_static}, + first_visit_{visited.insert(this->entry_).second} {} + + ~PathGuard() { + if (this->first_visit_) { + this->visited_.erase(this->entry_); + } + } + + [[nodiscard]] auto first_visit() const -> bool { return this->first_visit_; } + + PathGuard(const PathGuard &) = delete; + auto operator=(const PathGuard &) -> PathGuard & = delete; + PathGuard(PathGuard &&) = delete; + auto operator=(PathGuard &&) -> PathGuard & = delete; + +private: + std::set> &visited_; + const std::pair entry_; + const bool first_visit_; +}; + +inline auto find_adjacent_dependencies( + const sourcemeta::core::JSON::String ¤t, + const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const std::set &keywords, + const sourcemeta::core::SchemaFrame::Location &root, + const sourcemeta::core::SchemaFrame::Location &entry, const bool is_static, + std::set> &visited, + SchemaUnevaluatedEntry &result) -> void { + // A schema may reference itself, directly or through a chain of in-place + // applicators. Following such a cycle forever exhausts the stack, so we cut + // it as soon as we meet a location already on the path we came in through. + // The mark only lives as long as that path, as two references to a common + // target are not a cycle, and each of them still has to be accounted for + const PathGuard guard{visited, entry.pointer, is_static}; + if (!guard.first_visit()) { + return; + } + + const auto &subschema{sourcemeta::core::get(schema, entry.pointer)}; + if (!subschema.is_object()) { + return; + } + + const auto &subschema_vocabularies{frame.vocabularies(entry, resolver)}; + + for (const auto &property : subschema.as_object()) { + if (property.first == current && entry.pointer == root.pointer) { + continue; + } + if (keywords.contains(property.first)) { + // In 2019-09, `additionalItems` takes no effect without `items` + if (subschema_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2019_09_APPLICATOR) && + property.first == "additionalItems" && !subschema.defines("items")) { + continue; + } + + auto pointer{entry.pointer.concat(make_weak_pointer(property.first))}; + if (is_static) { + result.static_dependencies.emplace(std::move(pointer)); + } else { + result.dynamic_dependencies.emplace(std::move(pointer)); + } + + continue; + } + + switch (walker(property.first, subschema_vocabularies).type) { + // References + case sourcemeta::core::SchemaKeywordType::Reference: { + const auto reference{ + frame.dereference(entry, make_weak_pointer(property.first))}; + if (reference.first == sourcemeta::core::SchemaReferenceType::Static && + reference.second.has_value()) { + // Recurse into a dedicated entry so that whether this reference's + // target contributes any dynamic dependency can be read directly, + // rather than inferred from whether it grew the shared deduplicated + // set, which misses every reference after the first to a common + // target + SchemaUnevaluatedEntry nested; + find_adjacent_dependencies( + current, schema, frame, walker, resolver, keywords, root, + reference.second.value().get(), is_static, visited, nested); + + // Whatever the target contributes gets recorded at the location of + // the target itself, which tells the applicators this reference sits + // under nothing about it. Record the reference as a dependency of + // its own too, so that they can still tell that reaching through it + // leads to one, and therefore that they cannot short-circuit. Only + // the dynamic dependencies are consulted that way, whereas the + // static ones name the keyword locations that evaluate, which a + // reference is not one of + if (!is_static && !nested.dynamic_dependencies.empty()) { + result.dynamic_dependencies.emplace( + entry.pointer.concat(make_weak_pointer(property.first))); + } + + result.unresolved = result.unresolved || nested.unresolved; + result.static_dependencies.merge(nested.static_dependencies); + result.dynamic_dependencies.merge(nested.dynamic_dependencies); + } else if (reference.first == + sourcemeta::core::SchemaReferenceType::Dynamic) { + result.unresolved = true; + } + + break; + } + + // Static + case sourcemeta::core::SchemaKeywordType::ApplicatorElementsInPlace: + // TODO(C++23): Use std::views::enumerate when available in libc++ + for (std::size_t index = 0; index < property.second.size(); index++) { + find_adjacent_dependencies( + current, schema, frame, walker, resolver, keywords, root, + frame.traverse(entry, make_weak_pointer(property.first, index)) + .value() + .get(), + is_static, visited, result); + } + + break; + + // Dynamic + case sourcemeta::core::SchemaKeywordType::ApplicatorElementsInPlaceSome: + if (property.second.is_array()) { + for (std::size_t index = 0; index < property.second.size(); index++) { + find_adjacent_dependencies( + current, schema, frame, walker, resolver, keywords, root, + frame.traverse(entry, make_weak_pointer(property.first, index)) + .value() + .get(), + false, visited, result); + } + } + + break; + case sourcemeta::core::SchemaKeywordType::ApplicatorValueTraverseAnyItem: + [[fallthrough]]; + case sourcemeta::core::SchemaKeywordType::ApplicatorValueTraverseParent: + [[fallthrough]]; + case sourcemeta::core::SchemaKeywordType::ApplicatorValueInPlaceMaybe: + if ((property.second.is_object() || property.second.is_boolean())) { + find_adjacent_dependencies( + current, schema, frame, walker, resolver, keywords, root, + frame.traverse(entry, make_weak_pointer(property.first)) + .value() + .get(), + false, visited, result); + } + + break; + case sourcemeta::core::SchemaKeywordType:: + ApplicatorValueOrElementsInPlace: + if (property.second.is_array()) { + for (std::size_t index = 0; index < property.second.size(); index++) { + find_adjacent_dependencies( + current, schema, frame, walker, resolver, keywords, root, + frame.traverse(entry, make_weak_pointer(property.first, index)) + .value() + .get(), + false, visited, result); + } + } else if ((property.second.is_object() || + property.second.is_boolean())) { + find_adjacent_dependencies( + current, schema, frame, walker, resolver, keywords, root, + frame.traverse(entry, make_weak_pointer(property.first)) + .value() + .get(), + false, visited, result); + } + + break; + case sourcemeta::core::SchemaKeywordType::ApplicatorMembersInPlaceSome: + if (property.second.is_object()) { + for (const auto &pair : property.second.as_object()) { + find_adjacent_dependencies( + current, schema, frame, walker, resolver, keywords, root, + frame + .traverse(entry, + make_weak_pointer(property.first, pair.first)) + .value() + .get(), + false, visited, result); + } + } + + break; + + // Anything else does not contribute to the dependency list + default: + break; + } + } +} + +inline auto register_under_all_bases( + SchemaUnevaluatedEntries &result, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::JSON::String &keyword, + const SchemaUnevaluatedEntry &value) -> void { + result.emplace(frame.uri(location, make_weak_pointer(keyword)), value); + frame.for_each_location( + [&](const sourcemeta::core::SchemaReferenceType, const std::string_view, + const sourcemeta::core::SchemaFrame::Location &alternate) -> void { + if (alternate.pointer != location.pointer || + alternate.base == location.base) { + return; + } + + if (alternate.type != + sourcemeta::core::SchemaFrame::LocationType::Subschema && + alternate.type != + sourcemeta::core::SchemaFrame::LocationType::Resource && + alternate.type != + sourcemeta::core::SchemaFrame::LocationType::Anchor) { + return; + } + + result.emplace(frame.uri(alternate, make_weak_pointer(keyword)), value); + }); +} + +// TODO: Refactor this entire function using `SchemaFrame`'s new `Instances` +// mode. We can loop over every subschema that defines `unevaluatedProperties` +// or `unevaluatedItems`, find all other subschemas with the same unresolved +// instance location (static dependency) or conditional equivalent unresolved +// instance location (dynamic dependency) and see if those ones define any of +// the dependent keywords. +inline auto unevaluated(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver) + -> SchemaUnevaluatedEntries { + SchemaUnevaluatedEntries result; + + frame.for_each_subschema( + [&](const sourcemeta::core::SchemaFrame::Location &location) -> void { + const auto &subschema{sourcemeta::core::get(schema, location.pointer)}; + assert((subschema.is_object() || subschema.is_boolean())); + if (!subschema.is_object()) { + return; + } + + const bool has_unevaluated_properties{ + subschema.defines("unevaluatedProperties")}; + const bool has_unevaluated_items{subschema.defines("unevaluatedItems")}; + if (!has_unevaluated_properties && !has_unevaluated_items) { + return; + } + + const auto &subschema_vocabularies{ + frame.vocabularies(location, resolver)}; + + // The same pointer may be reachable through alternate identifiers whose + // dynamic anchors carry a different base, so we register the entry + // under each of them + if (has_unevaluated_properties) { + if ((subschema_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_UNEVALUATED) && + subschema_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_APPLICATOR)) || + subschema_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2019_09_APPLICATOR)) { + std::set> visited; + SchemaUnevaluatedEntry unevaluated; + find_adjacent_dependencies( + "unevaluatedProperties", schema, frame, walker, resolver, + {"properties", "patternProperties", "additionalProperties", + "unevaluatedProperties"}, + location, location, true, visited, unevaluated); + register_under_all_bases(result, frame, location, + UNEVALUATED_PROPERTIES, unevaluated); + } + } + + if (has_unevaluated_items) { + std::set> visited; + SchemaUnevaluatedEntry unevaluated; + if (subschema_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_UNEVALUATED) && + subschema_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_APPLICATOR)) { + find_adjacent_dependencies( + "unevaluatedItems", schema, frame, walker, resolver, + {"prefixItems", "items", "contains", "unevaluatedItems"}, + location, location, true, visited, unevaluated); + register_under_all_bases(result, frame, location, UNEVALUATED_ITEMS, + unevaluated); + } else if (subschema_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2019_09_APPLICATOR)) { + find_adjacent_dependencies( + "unevaluatedItems", schema, frame, walker, resolver, + {"items", "additionalItems", "unevaluatedItems"}, location, + location, true, visited, unevaluated); + register_under_all_bases(result, frame, location, UNEVALUATED_ITEMS, + unevaluated); + } + } + }); + + return result; +} + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/convert/convert.cc b/vendor/blaze/src/convert/convert.cc index 347204e5e..7d4c353b8 100644 --- a/vendor/blaze/src/convert/convert.cc +++ b/vendor/blaze/src/convert/convert.cc @@ -37,9 +37,9 @@ namespace { using Rule = std::tuple, bool>; /// Construct a rule entry for the given rule type -template T> -[[nodiscard]] auto make_rule() -> Rule { - return {std::make_unique(), +template T, typename... Args> +[[nodiscard]] auto make_rule(Args &&...arguments) -> Rule { + return {std::make_unique(std::forward(arguments)...), std::is_same_v}; } @@ -516,6 +516,7 @@ auto apply(const std::vector &rules, sourcemeta::core::JSON &schema, #include "rules/upgrade_draft_4_to_draft_6.h" #include "rules/upgrade_draft_6_to_draft_7.h" #include "rules/upgrade_draft_7_to_draft_2019_09.h" +#include "rules/upgrade_openapi_3_0_to_draft_4.h" #include "rules/upgrade_openapi_3_1_to_openapi_3_2.h" #undef ONLY_CONTINUE_IF @@ -544,6 +545,40 @@ auto convert(sourcemeta::core::JSON &schema, std::vector rules; rules.reserve(20); + + // Again here, and not only among the spellings above: a marker that said + // something when the pass began can stop saying anything once a rung has + // moved the resource holding it to the dialect the marker names, and a + // conversion that left it behind would not be done with the document + rules.push_back(make_rule()); + + // A document on the OpenAPI 3.0 dialect forks here rather than standing on a + // rung. Every target below reaches it through Draft 4, which is the base + // dialect its Schema Object extends, so this runs ahead of the ladder and + // the rungs carry on from there + rules.push_back(make_rule(target)); + + // Whether this conversion starts from OpenAPI 3.0. Usually only the caller + // can say, because the Schema Object admits no `$schema` member of its own, + // so a document written in that dialect is named from the outside. The + // exception is the override marker, which names a dialect for a document that + // cannot declare one, and which `declared_dialect` reads ahead of `$schema` + // exactly as core frames it. + // + // So the question is what the document says it is, and only where it says + // nothing does the default the caller passed decide. Asking merely whether a + // `$schema` member is present answers both halves wrongly: a document naming + // this dialect through the marker is read as not being on it, and one naming + // another dialect that way is read as being on it. + // + // What this buys is the keywords the fork carries across as themselves. Each + // is author data to the rung that promotes it, and shadowing it there would + // undo the fork, so the rules that would are told to leave it be + const auto declared{declared_dialect(schema)}; + const auto from_openapi_3_0{ + names_openapi_3_0_dialect(declared) || + (declared.empty() && names_openapi_3_0_dialect(default_dialect))}; + rules.push_back(make_rule()); rules.push_back(make_rule()); @@ -563,7 +598,10 @@ auto convert(sourcemeta::core::JSON &schema, target == ConvertTarget::Draft202012 || target == ConvertTarget::OpenAPI31 || target == ConvertTarget::OpenAPI32) { - rules.push_back(make_rule()); + rules.push_back(make_rule( + from_openapi_3_0 + ? std::vector{"readOnly", "writeOnly"} + : std::vector{})); rules.push_back(make_rule()); } @@ -571,7 +609,9 @@ auto convert(sourcemeta::core::JSON &schema, target == ConvertTarget::Draft202012 || target == ConvertTarget::OpenAPI31 || target == ConvertTarget::OpenAPI32) { - rules.push_back(make_rule()); + rules.push_back(make_rule( + from_openapi_3_0 ? std::vector{"deprecated"} + : std::vector{})); rules.push_back(make_rule()); rules.push_back(make_rule()); rules.push_back(make_rule()); @@ -586,7 +626,11 @@ auto convert(sourcemeta::core::JSON &schema, if (target == ConvertTarget::OpenAPI31 || target == ConvertTarget::OpenAPI32) { - rules.push_back(make_rule()); + rules.push_back(make_rule( + from_openapi_3_0 + ? std::vector{"example", "discriminator", + "externalDocs", "xml"} + : std::vector{})); rules.push_back(make_rule()); } diff --git a/vendor/blaze/src/convert/helpers.h b/vendor/blaze/src/convert/helpers.h index abf8cbfbd..994839dbb 100644 --- a/vendor/blaze/src/convert/helpers.h +++ b/vendor/blaze/src/convert/helpers.h @@ -74,20 +74,6 @@ inline auto assign_before_first_key(sourcemeta::core::JSON &schema, schema.try_assign_before(key, value, first); } -inline auto bump_dialect(sourcemeta::core::JSON &schema, const Site &site, - const std::string_view dialect) -> void { - if (!at_dialect_declaration(schema, site)) { - return; - } - - if (schema.defines("$schema")) { - schema.assign("$schema", sourcemeta::core::JSON{dialect}); - return; - } - - assign_before_first_key(schema, "$schema", sourcemeta::core::JSON{dialect}); -} - inline auto current_dialect_or_override(const sourcemeta::core::JSON &schema) -> std::string_view { return declared_dialect(schema); @@ -146,6 +132,119 @@ inline auto is_metaschema_target(const sourcemeta::core::JSON &schema, constexpr std::string_view OPENAPI_3_1_DIALECT{ "https://spec.openapis.org/oas/3.1/dialect/base"}; +// The OpenAPI 3.0 Schema Object dialect, which the conversion reads as a +// source and never writes. It is not a rung: its Schema Object extends the +// Draft 4 base dialect while OpenAPI 3.1's extends 2020-12, so it sits below +// Draft 4 by base dialect and below 3.1 by product version, and those two +// orderings disagree. A document on it forks instead, reaching an OpenAPI +// target through 3.1 and a JSON Schema target through Draft 4. +// +// Both dated spellings are listed and neither is canonical. Unlike OpenAPI +// 3.1's three URIs, these name two documents that genuinely differ, so there +// is nothing here for `normalized_official_dialect` to fold. They differ only +// in the parts of an OpenAPI document that a Schema Object never reaches, +// which is why one set of rules serves both +// NOLINTNEXTLINE(cert-err58-cpp,bugprone-throwing-static-initialization) +constexpr std::array OPENAPI_3_0_DIALECTS{ + {"tag:spec.openapis.org,2024-10-18:oas/3.0/dialect", + "tag:spec.openapis.org,2021-09-28:oas/3.0/dialect"}}; + +// Whether the dialect is one of those, asked of the spelling as written. A tag +// URI carries no fragment and no scheme the spelling rules fold, so what the +// document says is already the only spelling there is +inline auto names_openapi_3_0_dialect(const std::string_view dialect) -> bool { + return std::ranges::find(OPENAPI_3_0_DIALECTS, dialect) != + OPENAPI_3_0_DIALECTS.cend(); +} + +// Fold an OpenAPI 3.0 `nullable` into the `type` beside it, which is the one +// member of that dialect carrying an assertion rather than an annotation. Both +// targets of the fork accept a type array, so the same rewrite serves each. +// +// The keyword takes effect only where `type` is explicitly defined within the +// same Schema Object, which the specification states outright. One standing +// alone constrained nothing, and is left exactly where it is so that the +// shadowing carries it across as the author data it was. Erasing it would drop +// something the author wrote, which is not what becomes of any other member +// this dialect has, and turning it into `{"type": ["null"]}` would invent a +// constraint it never applied. +// +// A `nullable` that a `$ref` suppressed is left untouched too, whatever it +// says. Such a keyword never applied, and the caller shadows it along with +// every other sibling instead +inline auto fold_nullable_into_type(sourcemeta::core::JSON &schema) -> void { + const auto *nullable{schema.try_at("nullable")}; + if (nullable == nullptr || !nullable->is_boolean() || + !nullable->to_boolean() || schema.defines("$ref") || + !schema.defines("type")) { + return; + } + + static const sourcemeta::core::JSON NULL_TYPE{"null"}; + auto &type{schema.at("type")}; + if (type.is_string()) { + auto names{sourcemeta::core::JSON::make_array()}; + names.push_back(type); + names.push_back_if_unique(NULL_TYPE); + type = std::move(names); + } else if (type.is_array()) { + // An array here is not a shape OpenAPI 3.0 permits, as its `type` takes a + // single name. Carrying one anyway costs a line and keeps the rewrite from + // depending on a check that runs elsewhere + type.push_back_if_unique(NULL_TYPE); + } else { + return; + } + + schema.erase("nullable"); +} + +// Draft 4 reads `enum` as a set and requires its members to be unique, while +// the OpenAPI 3.0 Schema Object states `uniqueItems: false` for it outright. A +// repeated option says nothing a single one does not, so a document carrying +// one would otherwise reach a dialect that cannot read it back +inline auto deduplicate_enum(sourcemeta::core::JSON &schema) -> void { + auto *options{schema.try_at("enum")}; + if (options == nullptr || !options->is_array() || options->unique()) { + return; + } + + auto distinct{sourcemeta::core::JSON::make_array()}; + for (const auto &option : options->as_array()) { + distinct.push_back_if_unique(option); + } + + *options = std::move(distinct); +} + +// An empty `anyOf` or `oneOf` is satisfied by no instance whatever, which +// Draft 4 has no boolean schema to say and spells as a `not` of the schema +// every instance satisfies. That goes into `allOf`, whose meaning is already +// the conjunction this needs, so a `not` the author wrote is left exactly as it +// stands. Draft 4 requires at least one branch in each of the three, while the +// OpenAPI 3.0 Schema Object places no bound on them at all +inline auto fold_empty_applicators(sourcemeta::core::JSON &schema) -> void { + static const std::array UNSATISFIABLE_WHEN_EMPTY{ + {"anyOf", "oneOf"}}; + + for (const auto &keyword : UNSATISFIABLE_WHEN_EMPTY) { + const sourcemeta::core::JSON::String name{keyword}; + const auto *branches{schema.try_at(name)}; + if (branches == nullptr || !branches->is_array() || !branches->empty()) { + continue; + } + + schema.erase(name); + if (!schema.defines("allOf") || !schema.at("allOf").is_array()) { + schema.assign("allOf", sourcemeta::core::JSON::make_array()); + } + + auto nothing{sourcemeta::core::JSON::make_object()}; + nothing.assign("not", sourcemeta::core::JSON::make_object()); + schema.at("allOf").push_back(std::move(nothing)); + } +} + // The OpenAPI 3.2 dialect, which declares the same four keywords as 3.1 with // strictly more permissive constraints, so the rung above only has a dialect // to move. OpenAPI 3.2 publishes one dated URI with no undated alias, so there @@ -313,11 +412,13 @@ inline auto names_official_metaschema(const std::string_view uri) -> bool { }); } -// A dialect the ladder does not name is one the conversion has no rules for, -// whether it belongs to a draft older than the ladder starts at or to a -// meta-schema of the caller's own +// Whether the conversion has rules for moving a schema off this dialect. A +// dialect it does not name is one it has no rules for, whether that is a draft +// older than the ladder starts at or a meta-schema of the caller's own. Every +// rung qualifies, and so does OpenAPI 3.0, which forks off the ladder rather +// than standing on it inline auto names_ladder_dialect(const std::string_view dialect) -> bool { - return dialect_position(dialect) > 0; + return dialect_position(dialect) > 0 || names_openapi_3_0_dialect(dialect); } // Core reads this keyword as a dialect too, so it may well be a keyword the @@ -328,6 +429,40 @@ inline auto is_own_dialect_override(const sourcemeta::core::JSON &value) return value.is_string() && dialect_position(value.to_string()) > 0; } +inline auto bump_dialect(sourcemeta::core::JSON &schema, const Site &site, + const std::string_view dialect) -> void { + // Core reads the override marker as the dialect of the position carrying it, + // ahead of any `$schema` beside it and wherever that position is + if (schema.is_object()) { + auto *override_value{schema.try_at(DIALECT_OVERRIDE_KEYWORD)}; + if (override_value != nullptr && override_value->is_string() && + names_ladder_dialect(override_value->to_string())) { + // Where nothing else is read, the dialect moves by writing the marker. A + // `$schema` written here instead would not be the thing read, and the + // position would keep the dialect it had while its keywords moved on + if (!at_dialect_declaration(schema, site)) { + *override_value = sourcemeta::core::JSON{dialect}; + return; + } + + // Here a `$schema` is read, and the marker would go on overriding it, so + // the marker gives way to the declaration that now says the same thing + schema.erase(DIALECT_OVERRIDE_KEYWORD); + } + } + + if (!at_dialect_declaration(schema, site)) { + return; + } + + if (schema.defines("$schema")) { + schema.assign("$schema", sourcemeta::core::JSON{dialect}); + return; + } + + assign_before_first_key(schema, "$schema", sourcemeta::core::JSON{dialect}); +} + // Whether an identifier and a dialect name the same thing once both are // resolved against what the caller said the document is called inline auto names_the_same_uri(const sourcemeta::core::JSON &schema, diff --git a/vendor/blaze/src/convert/rules/dialect_override_becomes_dollar_schema.h b/vendor/blaze/src/convert/rules/dialect_override_becomes_dollar_schema.h index f171069d3..2766c425d 100644 --- a/vendor/blaze/src/convert/rules/dialect_override_becomes_dollar_schema.h +++ b/vendor/blaze/src/convert/rules/dialect_override_becomes_dollar_schema.h @@ -2,7 +2,15 @@ /// any `$schema` beside it, so a document carrying one has whatever dialect it /// names whether or not the author meant that. Settling it into a real /// `$schema` is what makes the rest of the conversion, and the output, depend -/// on nothing but what JSON Schema itself defines +/// on nothing but what JSON Schema itself defines. +/// +/// That is only possible where a `$schema` is the thing framing reads. A marker +/// deeper than that names a dialect for one position, which JSON Schema has no +/// way to say, so it can only be settled when it says nothing: when it names +/// the dialect that position would have had anyway. One that names another +/// dialect stays exactly where it is, and `bump_dialect` moves it along with +/// the schema it governs, because dropping it would silently reinterpret every +/// keyword underneath it class DialectOverrideBecomesDollarSchema final : public SchemaTransformRule { public: DialectOverrideBecomesDollarSchema() @@ -11,7 +19,7 @@ class DialectOverrideBecomesDollarSchema final : public SchemaTransformRule { [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, const sourcemeta::core::JSON &, const sourcemeta::core::SchemaVocabularies &, - const Site &, + const Site &site, const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { @@ -23,7 +31,37 @@ class DialectOverrideBecomesDollarSchema final : public SchemaTransformRule { // would read as a dialect here. Any other value is the author's own data // that happens to share the name, and is left exactly as written const auto *value{schema.try_at(DIALECT_OVERRIDE_KEYWORD)}; - return value != nullptr && is_own_dialect_override(*value); + return value != nullptr && is_own_dialect_override(*value) && + (at_dialect_declaration(schema, site) || this->says_nothing(site)); + } + + auto begin_pass() const -> void override { this->redundant_.clear(); } + + auto plan(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::SchemaVocabularies &, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const Site &site, const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const -> void override { + this->redundant_.erase(site.pointer); + if (!schema.is_object() || !location.parent.has_value()) { + return; + } + + const auto *value{schema.try_at(DIALECT_OVERRIDE_KEYWORD)}; + if (value == nullptr || !value->is_string()) { + return; + } + + // What the position would read as without the marker is what the resource + // holding it reads as, which only the frame can say + const auto enclosing{frame.traverse(location.parent.value())}; + if (enclosing.has_value() && + without_empty_fragment(enclosing.value().get().dialect) == + without_empty_fragment(value->to_string())) { + this->redundant_.insert(site.pointer); + } } auto transform(sourcemeta::core::JSON &schema, const Site &site) const @@ -32,9 +70,9 @@ class DialectOverrideBecomesDollarSchema final : public SchemaTransformRule { schema.erase(DIALECT_OVERRIDE_KEYWORD); // Only where framing would read a `$schema` does one have to be written to - // keep the dialect the document had. Anywhere else the keyword said - // nothing that a `$schema` could say, and the subschema takes the dialect - // of the resource holding it + // keep the dialect the document had. Anywhere else the marker said nothing + // a `$schema` could say, and said nothing at all, so the subschema takes + // the dialect of the resource holding it either way if (!dialect.is_string() || schema.defines("$schema") || !at_dialect_declaration(schema, site)) { return; @@ -42,4 +80,13 @@ class DialectOverrideBecomesDollarSchema final : public SchemaTransformRule { assign_before_first_key(schema, "$schema", std::move(dialect)); } + +private: + // Whether the marker at this position names the dialect the position would + // read as without it + [[nodiscard]] auto says_nothing(const Site &site) const -> bool { + return this->redundant_.contains(site.pointer); + } + + mutable std::set redundant_; }; diff --git a/vendor/blaze/src/convert/rules/prefix_promoted_draft_2019_09_keywords.h b/vendor/blaze/src/convert/rules/prefix_promoted_draft_2019_09_keywords.h index c2a2293b2..12253f6e3 100644 --- a/vendor/blaze/src/convert/rules/prefix_promoted_draft_2019_09_keywords.h +++ b/vendor/blaze/src/convert/rules/prefix_promoted_draft_2019_09_keywords.h @@ -1,7 +1,9 @@ class PrefixPromoted201909Keywords final : public SchemaTransformRule { public: - PrefixPromoted201909Keywords() - : SchemaTransformRule{"prefix_promoted_2019_09_keywords"} {}; + PrefixPromoted201909Keywords( + std::vector &&keywords_left_alone = {}) + : SchemaTransformRule{"prefix_promoted_2019_09_keywords"}, + keywords_left_alone_{std::move(keywords_left_alone)} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -17,7 +19,7 @@ class PrefixPromoted201909Keywords final : public SchemaTransformRule { promoted_keyword_is_author_data(schema, PROMOTING_DIALECT)); for (const auto &keyword : KEYWORDS) { - if (schema.defines(keyword)) { + if (schema.defines(keyword) && !this->is_left_alone(schema, keyword)) { return true; } } @@ -30,7 +32,8 @@ class PrefixPromoted201909Keywords final : public SchemaTransformRule { this->renames_.clear(); for (const auto &keyword : KEYWORDS) { const std::string keyword_name{keyword}; - if (!schema.defines(keyword_name)) { + if (!schema.defines(keyword_name) || + this->is_left_alone(schema, keyword)) { continue; } @@ -66,5 +69,28 @@ class PrefixPromoted201909Keywords final : public SchemaTransformRule { "unevaluatedProperties", "maxContains", "minContains", "contentSchema", "deprecated"}}; + // A keyword the caller asked to carry across as the keyword it already was, + // rather than as the author data it would otherwise be read as. + // + // The caller names the dialect of a document, not of each position in it, and + // a document may embed a resource written in another dialect whose members + // are exactly the author data this rule exists to protect. What tells the two + // apart is the value: the source dialect constrained the keyword it named, so + // one carrying a shape that dialect never allowed was never that keyword + [[nodiscard]] auto is_left_alone(const sourcemeta::core::JSON &schema, + const std::string_view keyword) const + -> bool { + if (std::ranges::find(this->keywords_left_alone_, keyword) == + this->keywords_left_alone_.cend()) { + return false; + } + + // Only a boolean is the annotation this names. A value of any other shape + // was never that keyword, whatever it is called, and carrying it across as + // one would leave an output the target's own meta-schema rejects + return schema.at(sourcemeta::core::JSON::String{keyword}).is_boolean(); + } + + const std::vector keywords_left_alone_; mutable std::unordered_map renames_; }; diff --git a/vendor/blaze/src/convert/rules/prefix_promoted_draft_7_keywords.h b/vendor/blaze/src/convert/rules/prefix_promoted_draft_7_keywords.h index a9570c7d9..9fccf545b 100644 --- a/vendor/blaze/src/convert/rules/prefix_promoted_draft_7_keywords.h +++ b/vendor/blaze/src/convert/rules/prefix_promoted_draft_7_keywords.h @@ -1,7 +1,9 @@ class PrefixPromotedDraft7Keywords final : public SchemaTransformRule { public: - PrefixPromotedDraft7Keywords() - : SchemaTransformRule{"prefix_promoted_draft_7_keywords"} {}; + PrefixPromotedDraft7Keywords( + std::vector &&keywords_left_alone = {}) + : SchemaTransformRule{"prefix_promoted_draft_7_keywords"}, + keywords_left_alone_{std::move(keywords_left_alone)} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -17,7 +19,7 @@ class PrefixPromotedDraft7Keywords final : public SchemaTransformRule { promoted_keyword_is_author_data(schema, PROMOTING_DIALECT)); for (const auto &keyword : KEYWORDS) { - if (schema.defines(keyword)) { + if (schema.defines(keyword) && !this->is_left_alone(schema, keyword)) { return true; } } @@ -30,7 +32,8 @@ class PrefixPromotedDraft7Keywords final : public SchemaTransformRule { this->renames_.clear(); for (const auto &keyword : KEYWORDS) { const std::string keyword_name{keyword}; - if (!schema.defines(keyword_name)) { + if (!schema.defines(keyword_name) || + this->is_left_alone(schema, keyword)) { continue; } @@ -64,5 +67,28 @@ class PrefixPromotedDraft7Keywords final : public SchemaTransformRule { {"$comment", "if", "then", "else", "readOnly", "writeOnly", "contentMediaType", "contentEncoding"}}; + // A keyword the caller asked to carry across as the keyword it already was, + // rather than as the author data it would otherwise be read as. + // + // The caller names the dialect of a document, not of each position in it, and + // a document may embed a resource written in another dialect whose members + // are exactly the author data this rule exists to protect. What tells the two + // apart is the value: the source dialect constrained the keyword it named, so + // one carrying a shape that dialect never allowed was never that keyword + [[nodiscard]] auto is_left_alone(const sourcemeta::core::JSON &schema, + const std::string_view keyword) const + -> bool { + if (std::ranges::find(this->keywords_left_alone_, keyword) == + this->keywords_left_alone_.cend()) { + return false; + } + + // Only a boolean is the annotation these two name. A value of any other + // shape was never that keyword, whatever it is called, and carrying it + // across as one would leave an output the target's own meta-schema rejects + return schema.at(sourcemeta::core::JSON::String{keyword}).is_boolean(); + } + + const std::vector keywords_left_alone_; mutable std::unordered_map renames_; }; diff --git a/vendor/blaze/src/convert/rules/prefix_promoted_openapi_3_1_keywords.h b/vendor/blaze/src/convert/rules/prefix_promoted_openapi_3_1_keywords.h index 90079a168..d39b66e1b 100644 --- a/vendor/blaze/src/convert/rules/prefix_promoted_openapi_3_1_keywords.h +++ b/vendor/blaze/src/convert/rules/prefix_promoted_openapi_3_1_keywords.h @@ -1,7 +1,9 @@ class PrefixPromotedOpenAPI31Keywords final : public SchemaTransformRule { public: - PrefixPromotedOpenAPI31Keywords() - : SchemaTransformRule{"prefix_promoted_openapi_3_1_keywords"} {}; + PrefixPromotedOpenAPI31Keywords( + std::vector &&keywords_left_alone = {}) + : SchemaTransformRule{"prefix_promoted_openapi_3_1_keywords"}, + keywords_left_alone_{std::move(keywords_left_alone)} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -24,8 +26,14 @@ class PrefixPromotedOpenAPI31Keywords final : public SchemaTransformRule { ONLY_CONTINUE_IF( promoted_keyword_is_author_data(schema, OPENAPI_3_1_DIALECT)); - return schema.defines_any( - {"example", "discriminator", "externalDocs", "xml"}); + for (const auto &keyword : KEYWORDS) { + if (schema.defines(sourcemeta::core::JSON::String{keyword}) && + !this->is_left_alone(schema, keyword)) { + return true; + } + } + + return false; } auto transform(sourcemeta::core::JSON &schema, const Site &) const @@ -33,7 +41,8 @@ class PrefixPromotedOpenAPI31Keywords final : public SchemaTransformRule { this->renames_.clear(); for (const auto &keyword : KEYWORDS) { const std::string keyword_name{keyword}; - if (!schema.defines(keyword_name)) { + if (!schema.defines(keyword_name) || + this->is_left_alone(schema, keyword)) { continue; } @@ -63,5 +72,30 @@ class PrefixPromotedOpenAPI31Keywords final : public SchemaTransformRule { static inline const std::array KEYWORDS{ {"example", "discriminator", "externalDocs", "xml"}}; + // A keyword the caller asked to carry across as the keyword it already was, + // rather than as the author data it would otherwise be read as. + // + // The caller names the dialect of a document, not of each position in it, and + // a document may embed a resource written in another dialect whose members + // are exactly the author data this rule exists to protect. What tells the two + // apart is the value: the source dialect constrained the keyword it named, so + // one carrying a shape that dialect never allowed was never that keyword + [[nodiscard]] auto is_left_alone(const sourcemeta::core::JSON &schema, + const std::string_view keyword) const + -> bool { + if (std::ranges::find(this->keywords_left_alone_, keyword) == + this->keywords_left_alone_.cend()) { + return false; + } + + // Three of these name an object and `example` names anything at all. A + // value of another shape was never the keyword it is named after, and + // carrying it across as one would leave an output the target's own + // meta-schema rejects + return keyword == "example" || + schema.at(sourcemeta::core::JSON::String{keyword}).is_object(); + } + + const std::vector keywords_left_alone_; mutable std::unordered_map renames_; }; diff --git a/vendor/blaze/src/convert/rules/upgrade_openapi_3_0_to_draft_4.h b/vendor/blaze/src/convert/rules/upgrade_openapi_3_0_to_draft_4.h new file mode 100644 index 000000000..fa0efd4eb --- /dev/null +++ b/vendor/blaze/src/convert/rules/upgrade_openapi_3_0_to_draft_4.h @@ -0,0 +1,524 @@ +class UpgradeOpenAPI30ToDraft4 final : public SchemaTransformRule { +public: + UpgradeOpenAPI30ToDraft4(const ConvertTarget target) + : SchemaTransformRule{"upgrade_openapi_3_0_to_draft_4"}, + keeps_read_only_{names_read_only(target)}, + keeps_deprecated_{names_deprecated(target)}, + keeps_openapi_annotations_{names_openapi_annotations(target)}, + keeps_default_mapping_{target == ConvertTarget::OpenAPI32} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::SchemaVocabularies &vocabularies, + const Site &site, const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF( + vocabularies.contains(SchemaVocabularies::Known::OPENAPI_3_0_BASE) && + schema.is_object()); + + return holds_openapi_3_0_override(schema) || + holds_ignored_sibling(schema) || + this->has_pending_openapi_3_0_pattern(schema) || + holds_promoted_keyword(schema) || + holds_movable_definitions(schema) || holds_pending_shape(schema) || + at_dialect_declaration(schema, site); + } + + auto transform(sourcemeta::core::JSON &schema, const Site &site) const + -> void override { + this->renames_.clear(); + this->relocations_.clear(); + + // OpenAPI 3.0 section 4.7.23: a Reference Object cannot be extended with + // additional properties, and any property added to one SHALL be ignored. An + // object carrying `$ref` can only be a Reference Object, because the Schema + // Object closes itself to additional properties and does not name `$ref`, + // so every member beside it was ignored and has to go on being ignored. + // + // Carrying one across as the keyword a target names does two wrong things + // at once: it reads a constraint the source never applied, and it hands the + // target's own meta-schema a value to type-check that nothing ever checked. + // So none of the rewriting below applies to a Reference Object, and its + // members go across as the author data they already were + if (schema.defines("$ref")) { + for (const auto &name : member_names(schema)) { + if (name == "$ref" || name == "$schema" || name.starts_with("x-")) { + continue; + } + + this->shadow(schema, name); + } + } else { + // Before the shadowing below, so that a `nullable` this consumes is gone + // by the time the annotations are renamed and one it declines is still + // there to be renamed with them + fold_nullable_into_type(schema); + + // Comparing the two meta-schemas member by member, the OpenAPI 3.0 Schema + // Object is looser in exactly three places and equal or stricter + // everywhere else: `enum` states `uniqueItems: false`, the three + // applicator arrays carry no `minItems`, and it states none of the + // keyword dependencies Draft 4 does. The first two are shapes, rewritten + // here, and the third is answered for below + deduplicate_enum(schema); + fold_empty_applicators(schema); + + // Draft 4 requires each of these to accompany the bound it modifies, + // while the OpenAPI 3.0 Schema Object states no dependency between + // keywords at all. One standing on its own modifies a bound that is not + // there, and an `allOf` with no branches asserts nothing of the instance, + // so each goes across as the author data it was. None of the three can + // stay where it is: Draft 4 reads an incomplete bound and an empty + // applicator as keywords it will not accept + for (const auto &[keyword, companion] : UNACCOMPANIED) { + if (schema.defines(sourcemeta::core::JSON::String{keyword}) && + !schema.defines(sourcemeta::core::JSON::String{companion})) { + this->shadow(schema, keyword); + } + } + + // After the fold above, which puts what an empty `anyOf` or `oneOf` means + // into `allOf` and so reuses an empty one as the container it needs. An + // `allOf` still empty here is one the author wrote alone + if (schema.defines("allOf") && schema.at("allOf").is_array() && + schema.at("allOf").empty()) { + this->shadow(schema, "allOf"); + } + + for (const auto &keyword : KEYWORDS) { + if (!schema.defines(sourcemeta::core::JSON::String{keyword}) || + this->carries_as_keyword(schema, keyword)) { + continue; + } + + this->shadow(schema, keyword); + } + + // Taken out before the shadowing below, so that what moves is the map the + // author wrote under this name and not a `definitions` that the shadowing + // has since put there. Doing it the other way round hands the shadowed + // member straight back to the move, which promotes the author's data to + // the keyword the shadowing was there to protect it from + auto definitions{take_definitions(schema, this->relocations_)}; + + for (const auto &keyword : PROMOTED_BY_DRAFT_4) { + this->shadow(schema, keyword); + } + + if (!definitions.empty()) { + schema.assign("definitions", std::move(definitions)); + } + } + + bump_dialect(schema, site, DRAFT_4_URL); + } + + // A reference is rebased through these in order, so one whose source names a + // position another's destination sits under has to come first. Shadowing + // `definitions` names the very position the move below places its entries + // under, so the renames are published ahead of the moves: the other way + // round, the rename catches an entry the move has just put there and sends + // every reference to it somewhere nothing lives + [[nodiscard]] auto relocations() const -> std::vector override { + std::vector result; + result.reserve(this->renames_.size() + this->relocations_.size()); + for (const auto &[old_name, new_name] : this->renames_) { + result.emplace_back(sourcemeta::core::Pointer{old_name}, + sourcemeta::core::Pointer{new_name}); + } + + result.insert(result.cend(), this->relocations_.cbegin(), + this->relocations_.cend()); + return result; + } + +private: + // The only member the dialect reads a named subschema under, which is how a + // document on it keeps anything reusable at all. Its applicators hold + // subschemas too, but none of them by a name the author chooses + // NOLINTNEXTLINE(cert-err58-cpp,bugprone-throwing-static-initialization) + static inline const std::string DEFINITIONS{"x-definitions"}; + + // NOLINTNEXTLINE(cert-err58-cpp,bugprone-throwing-static-initialization) + static inline const std::string DRAFT_4_URL{ + "http://json-schema.org/draft-04/schema#"}; + + // What the OpenAPI 3.0 Schema Object adds that Draft 4 has no word for, less + // the `nullable` the rewrite above consumes. Every one of these annotates + // rather than asserts, so shadowing one changes no verdict, and Draft 4 reads + // an `x-` member as the unknown keyword it is. All but `nullable` are + // shadowed only where the target has no word for them, which + // `carries_as_keyword` settles: three became keywords of a later draft, and + // four are named by the OpenAPI dialects above 3.0 + // Which targets name each of the keywords this edge can carry across. Spelled + // out rather than compared, because `ConvertTarget` is a public enumeration + // that promises no ordering, so reading one out of the declaration order + // would make rearranging that header change what this rule does + static auto names_read_only(const ConvertTarget target) -> bool { + return target == ConvertTarget::Draft7 || + target == ConvertTarget::Draft201909 || + target == ConvertTarget::Draft202012 || + target == ConvertTarget::OpenAPI31 || + target == ConvertTarget::OpenAPI32; + } + + static auto names_deprecated(const ConvertTarget target) -> bool { + return target == ConvertTarget::Draft201909 || + target == ConvertTarget::Draft202012 || + target == ConvertTarget::OpenAPI31 || + target == ConvertTarget::OpenAPI32; + } + + static auto names_openapi_annotations(const ConvertTarget target) -> bool { + return target == ConvertTarget::OpenAPI31 || + target == ConvertTarget::OpenAPI32; + } + + static constexpr std::array KEYWORDS{ + {"nullable", "discriminator", "xml", "externalDocs", "example", + "deprecated", "readOnly", "writeOnly"}}; + + // Where a `mapping` entry names a schema inside the container this edge moves + // NOLINTNEXTLINE(cert-err58-cpp,bugprone-throwing-static-initialization) + static inline const std::string MAPPING_INTO_DEFINITIONS{"#/x-definitions/"}; + + // Each of these modifies the bound beside it, which Draft 4 requires to be + // there and OpenAPI 3.0 does not + static constexpr std::array, 2> + UNACCOMPANIED{ + {{"exclusiveMinimum", "minimum"}, {"exclusiveMaximum", "maximum"}}}; + + // The other direction: what Draft 4 names that the OpenAPI 3.0 Schema Object + // does not, so a document carrying one carries author data that the target + // would start reading as a keyword. `id` is the one that matters most, as + // Draft 4 takes it for the identifier that every reference in the document + // resolves against. + // + // Two positions can carry one of these, and the Schema Object is neither: + // there the dialect prohibits all five outright. A Reference Object + // constrains nothing beyond its `$ref`, though the branch above answers for + // that one by shadowing every member it holds. The other is an entry of an + // `x-definitions` map, which the dialect constrains with `^x-` and so not at + // all. That is the position this list is for, and it is narrow: an entry + // something references is read as a Schema Object and refused, so what + // survives to here sits in an entry nothing reads. It still has to be + // shadowed, because the member would otherwise reach the target as a keyword + // whose value makes the output invalid against the dialect it declares. + // + // `$schema` is left out because it is the one of the six that names a + // dialect. Where framing would read one, `bump_dialect` writes the rung's own + // in its place, and anywhere else `ShadowStrayDialectDeclaration` carries it + // across as the author data it is + static constexpr std::array PROMOTED_BY_DRAFT_4{ + {"id", "definitions", "dependencies", "patternProperties", + "additionalItems"}}; + + // Carry a member across as the author data it is, under a name the target + // reads as nothing. A name already taken gains another prefix rather than + // overwriting what is there + auto shadow(sourcemeta::core::JSON &schema, + const std::string_view keyword) const -> void { + const std::string keyword_name{keyword}; + if (!schema.defines(keyword_name)) { + return; + } + + std::string prefixed_name{"x-" + keyword_name}; + while (schema.defines(prefixed_name) || names_core_reads(prefixed_name)) { + prefixed_name.insert(0, "x-"); + } + + this->renames_.emplace(keyword_name, prefixed_name); + schema.rename(keyword_name, std::move(prefixed_name)); + } + + // Whether this subschema still carries something of the dialect that the + // transform will act on. Each of those is either consumed or shadowed, so the + // answer turns false once it has run and the conversion stops finding work + // here. One carried across as itself is not work, or the answer would never + // turn false + [[nodiscard]] auto + has_pending_openapi_3_0_pattern(const sourcemeta::core::JSON &subschema) const + -> bool { + for (const auto &keyword : KEYWORDS) { + if (subschema.defines(sourcemeta::core::JSON::String{keyword}) && + !this->carries_as_keyword(subschema, keyword)) { + return true; + } + } + + return false; + } + + // Three of these became keywords of a later draft with the meaning OpenAPI + // 3.0 already gave them, so a target defining one takes it as it stands + // rather than as the author data the shadowing would make of it. `readOnly` + // and `writeOnly` are native from Draft 7 and `deprecated` from 2019-09. + // + // This is the one place in the module where a rule reads the target the + // caller finally asked for rather than the rung in front of it. It has to: + // Draft 4 cannot express any of the three, so what the waypoint should carry + // is only answerable from where the document is going. The cost is that + // converting by way of Draft 4 no longer reproduces the direct result for + // these three, which `convertsuite` records per fixture + [[nodiscard]] auto carries_as_keyword(const sourcemeta::core::JSON &subschema, + const std::string_view keyword) const + -> bool { + if (keyword == "readOnly" || keyword == "writeOnly") { + return this->keeps_read_only_; + } + + if (keyword == "deprecated") { + return this->keeps_deprecated_; + } + + if (!this->keeps_openapi_annotations_) { + return false; + } + + // Of the four the OpenAPI dialects name for themselves, only this one is + // stated more loosely by 3.0 than by the dialects above it, so only this + // one has a shape to answer for + if (keyword == "discriminator") { + const auto &discriminator{ + subschema.at(sourcemeta::core::JSON::String{keyword})}; + return this->is_expressible_discriminator(discriminator) && + mapping_survives_the_move(discriminator); + } + + // `nullable` is not among them: no OpenAPI dialect above 3.0 has it, as a + // type union says it there instead + return keyword == "xml" || keyword == "externalDocs" || + keyword == "example"; + } + + // A `mapping` value may name a schema by reference, and the frame indexes no + // such reference, so nothing follows one when the schema it names moves. The + // one member this edge moves is the subschema container, so a mapping naming + // a position inside it would be left pointing where nothing is any more. + // Carrying that across as a keyword would publish a discriminator that lies + // about where its branches are, so it goes across as author data instead, + // which at least reads as the record of the author's intent that it is + static auto + mapping_survives_the_move(const sourcemeta::core::JSON &discriminator) + -> bool { + const auto *mapping{discriminator.try_at("mapping")}; + if (mapping == nullptr || !mapping->is_object()) { + return true; + } + + for (const auto &entry : mapping->as_object()) { + if (entry.second.is_string() && + entry.second.to_string().starts_with(MAPPING_INTO_DEFINITIONS)) { + return false; + } + } + + return true; + } + + // OpenAPI 3.0 leaves its Discriminator Object open, naming `propertyName` and + // `mapping` without closing the object to anything else, while 3.1 and 3.2 + // close theirs to those two and the extension prefix. One carrying anything + // more is a discriminator the target cannot read, so it goes across as the + // author data it has to be there rather than as a keyword that would make the + // output invalid against the dialect it declares. + // + // Which members are asked about depends on the target, because the two + // shapes differ: 3.1 names `propertyName` and `mapping`, and 3.2 names + // `defaultMapping` as well. A discriminator carrying that one is a + // discriminator 3.2 reads and 3.1 does not + [[nodiscard]] auto is_expressible_discriminator( + const sourcemeta::core::JSON &discriminator) const -> bool { + if (!discriminator.is_object()) { + return false; + } + + for (const auto &entry : discriminator.as_object()) { + if (entry.first == "propertyName" || entry.first == "mapping" || + entry.first.starts_with("x-")) { + continue; + } + + if (entry.first == "defaultMapping" && this->keeps_default_mapping_) { + continue; + } + + return false; + } + + return true; + } + + static auto holds_promoted_keyword(const sourcemeta::core::JSON &schema) + -> bool { + for (const auto &keyword : PROMOTED_BY_DRAFT_4) { + if (schema.defines(sourcemeta::core::JSON::String{keyword})) { + return true; + } + } + + return false; + } + + // The members of a Reference Object that the dialect ignored and that a + // target would start reading. One already carrying the extension prefix is + // read as nothing anywhere, and a `$schema` is answered for by the rule that + // settles a dialect declaration + static auto holds_ignored_sibling(const sourcemeta::core::JSON &schema) + -> bool { + if (!schema.defines("$ref")) { + return false; + } + + for (const auto &entry : schema.as_object()) { + if (entry.first != "$ref" && entry.first != "$schema" && + !entry.first.starts_with("x-")) { + return true; + } + } + + return false; + } + + // Taken as values, since shadowing renames the members being walked + static auto member_names(const sourcemeta::core::JSON &schema) + -> std::vector { + std::vector names; + names.reserve(schema.size()); + for (const auto &entry : schema.as_object()) { + names.push_back(entry.first); + } + + return names; + } + + // The extension prefix is not a free namespace: core reads two names in it, + // this dialect's subschema container and the marker that names a dialect. A + // member prefixed onto either would hand core a schema map or a dialect the + // author never wrote, so both count as taken and the prefix grows past them + static auto names_core_reads(const std::string_view name) -> bool { + return name == DEFINITIONS || name == DIALECT_OVERRIDE_KEYWORD; + } + + // Core reads the marker as the dialect of the position carrying it, so one + // naming OpenAPI 3.0 holds that position on this dialect until it is cleared. + // Nothing else can clear it: every rung of the ladder is gated on a + // vocabulary this dialect does not have, so the position would keep both the + // marker and whatever spelling the dialect gave its keywords + static auto holds_openapi_3_0_override(const sourcemeta::core::JSON &schema) + -> bool { + const auto *value{schema.try_at(DIALECT_OVERRIDE_KEYWORD)}; + return value != nullptr && value->is_string() && + names_openapi_3_0_dialect(value->to_string()); + } + + // Whether a shape only the OpenAPI 3.0 dialect admits is still here. Each is + // either rewritten or shadowed by the transform, so the answer turns false + // once it has run + static auto holds_pending_shape(const sourcemeta::core::JSON &schema) + -> bool { + const auto *options{schema.try_at("enum")}; + if (options != nullptr && options->is_array() && !options->unique()) { + return true; + } + + if ((schema.defines("exclusiveMinimum") && !schema.defines("minimum")) || + (schema.defines("exclusiveMaximum") && !schema.defines("maximum"))) { + return true; + } + + for (const auto &keyword : {"allOf", "anyOf", "oneOf"}) { + const auto *branches{ + schema.try_at(sourcemeta::core::JSON::String{keyword})}; + if (branches != nullptr && branches->is_array() && branches->empty()) { + return true; + } + } + + return false; + } + + // The extension prefix constrains nothing, so a member of this one holds + // whatever its author put there, while `definitions` reads every member of + // itself as a schema. Only what is one has somewhere to go + static auto holds_schema(const sourcemeta::core::JSON &entry) -> bool { + return entry.is_object() || entry.is_boolean(); + } + + // Neither OpenAPI 3.0 nor Draft 4 has a boolean schema, and yet the walker + // reads every member of this one as a subschema, so a reference naming a + // boolean here is asserting something and has to keep asserting it. Draft 4 + // says the same two things with the schema every instance satisfies and the + // schema none does + static auto as_object_schema(const sourcemeta::core::JSON &entry) + -> sourcemeta::core::JSON { + if (!entry.is_boolean()) { + return entry; + } + + auto result{sourcemeta::core::JSON::make_object()}; + if (!entry.to_boolean()) { + result.assign("not", sourcemeta::core::JSON::make_object()); + } + + return result; + } + + static auto holds_movable_definitions(const sourcemeta::core::JSON &schema) + -> bool { + if (!schema.defines(DEFINITIONS) || !schema.at(DEFINITIONS).is_object()) { + return false; + } + + for (const auto &entry : schema.at(DEFINITIONS).as_object()) { + if (holds_schema(entry.second)) { + return true; + } + } + + return false; + } + + // Draft 4 keeps under `definitions` what the OpenAPI 3.0 Schema Object keeps + // here, and reads nothing of a schema under a member it does not know, so + // every entry that is a schema is taken out for the caller to place and the + // references reaching one follow it. Anything else stays where it was rather + // than being carried somewhere that would read it as a schema it is not + static auto take_definitions(sourcemeta::core::JSON &schema, + std::vector &relocations) + -> sourcemeta::core::JSON { + auto moved{sourcemeta::core::JSON::make_object()}; + if (!holds_movable_definitions(schema)) { + return moved; + } + + auto kept{sourcemeta::core::JSON::make_object()}; + for (const auto &entry : schema.at(DEFINITIONS).as_object()) { + if (!holds_schema(entry.second)) { + kept.assign(entry.first, entry.second); + continue; + } + + relocations.emplace_back( + sourcemeta::core::Pointer{DEFINITIONS, entry.first}, + sourcemeta::core::Pointer{"definitions", entry.first}); + moved.assign(entry.first, as_object_schema(entry.second)); + } + + schema.erase(DEFINITIONS); + if (!kept.empty()) { + schema.assign(DEFINITIONS, std::move(kept)); + } + + return moved; + } + + const bool keeps_read_only_; + const bool keeps_deprecated_; + const bool keeps_openapi_annotations_; + const bool keeps_default_mapping_; + mutable std::unordered_map renames_; + mutable std::vector relocations_; +}; diff --git a/vendor/blaze/src/evaluator/evaluator_describe.cc b/vendor/blaze/src/evaluator/evaluator_describe.cc index 17a6c9823..4efe585db 100644 --- a/vendor/blaze/src/evaluator/evaluator_describe.cc +++ b/vendor/blaze/src/evaluator/evaluator_describe.cc @@ -1083,6 +1083,17 @@ auto describe(const bool valid, const Instruction &step, return "The value was expected to be an integer above the given minimum"; } + if (step.type == + sourcemeta::blaze::InstructionIndex::AssertionNumberIntegerBounded) { + std::ostringstream message; + const auto &value{instruction_value(step)}; + message << "The " << value_type_name(target) << " value "; + describe_stringify(target, message); + message << " was expected to be an integer between " << value.first + << " and " << value.second; + return message.str(); + } + if (step.type == sourcemeta::blaze::InstructionIndex::AssertionObjectPropertiesSimple) { return "The object value was expected to validate against the defined " @@ -1717,6 +1728,32 @@ auto describe(const bool valid, const Instruction &step, "string is short enough to satisfy a negative maximum length"; } + // The lower bounds below compile the same way when the bound is too large + // to hold, as nothing can be big enough to satisfy one + if (keyword == "minLength") { + return "The value was expected to be of a type other than string, as no " + "string is long enough to satisfy a minimum length that large"; + } + + if (keyword == "minItems") { + return "The value was expected to be of a type other than array, as no " + "array has enough items to satisfy a minimum size that large"; + } + + if (keyword == "minProperties") { + return "The value was expected to be of a type other than object, as no " + "object has enough properties to satisfy a minimum size that " + "large"; + } + + // `contains` compiles the same way when its `minContains` is too large to + // hold, as no array has that many items for the subschema to match + if (keyword == "contains") { + return "The value was expected to be of a type other than array, as no " + "array has enough matching items to satisfy a minimum count that " + "large"; + } + std::ostringstream message; describe_not_types_check(valid, target.type(), instruction_value(step), message); @@ -2291,6 +2328,23 @@ auto describe(const bool valid, const Instruction &step, return message.str(); } + if (step.type == sourcemeta::blaze::InstructionIndex::AssertionNumberType) { + std::ostringstream message; + message << "The " << value_type_name(target) << " value "; + describe_stringify(target, message); + message << " was expected to be exactly representable as an IEEE 754 "; + switch (instruction_value(step)) { + case ValueNumberType::Float: + message << "single precision floating point number"; + break; + case ValueNumberType::Double: + message << "double precision floating point number"; + break; + } + + return message.str(); + } + if (step.type == sourcemeta::blaze::InstructionIndex::AssertionStringType) { assert(target.is_string()); std::ostringstream message; @@ -2360,6 +2414,9 @@ auto describe(const bool valid, const Instruction &step, case ValueStringType::Color: message << " CSS 2 color"; break; + case ValueStringType::Base64: + message << " RFC 4648 Base64 string"; + break; default: return unknown(); } diff --git a/vendor/blaze/src/evaluator/evaluator_json.cc b/vendor/blaze/src/evaluator/evaluator_json.cc index 28d0f9f1d..7f159f9f1 100644 --- a/vendor/blaze/src/evaluator/evaluator_json.cc +++ b/vendor/blaze/src/evaluator/evaluator_json.cc @@ -44,6 +44,7 @@ auto value_from_json(const sourcemeta::core::JSON &wrapper) case 22: return sourcemeta::core::from_json(value); case 23: return sourcemeta::core::from_json(value); case 24: return sourcemeta::core::from_json(value); + case 25: return sourcemeta::core::from_json(value); // clang-format on default: std::unreachable(); diff --git a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator.h b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator.h index 60ba80c65..4ffdd52e9 100644 --- a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator.h +++ b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator.h @@ -46,7 +46,7 @@ struct Template { }; /// @ingroup evaluator -constexpr std::size_t JSON_VERSION{7}; +constexpr std::size_t JSON_VERSION{9}; /// @ingroup evaluator /// Parse a template from JSON diff --git a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h index dd606c188..339980962 100644 --- a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h +++ b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h @@ -13,6 +13,8 @@ #include #include +#include // assert + // TODO(C++23): Replace SOURCEMETA_ASSUME with [[assume]] when available // across all compilers (Clang 19, GCC 13) #if defined(__clang__) @@ -242,6 +244,33 @@ inline auto effective_type_strict_real(const JSON &instance) noexcept } } +// An integral number that no 64-bit integer can hold cannot be narrowed to one +// to be compared, and `as_integer` throws rather than saturating, so the +// comparison happens in a domain wide enough to hold any of them instead. +// A real converts through `exact_from` because the constructor spells a double +// with 17 significant digits, which is too few to tell apart two integers that +// a bound of this width can sit between +inline auto as_decimal(const JSON &instance) -> Decimal { + assert(instance.is_integral()); + return instance.is_decimal() ? instance.to_decimal() + : Decimal::exact_from(instance.to_real()); +} + +inline auto integer_within_bounds(const JSON &target, + const ValueIntegerBounds &value) -> bool { + if (target.is_integer()) { + const auto integer{target.to_integer()}; + return integer >= value.first && integer <= value.second; + } + + if (target.is_integral()) { + const auto decimal{as_decimal(target)}; + return decimal >= Decimal{value.first} && decimal <= Decimal{value.second}; + } + + return false; +} + template inline auto assume_value(const Value &variant) noexcept -> const T & { const auto *pointer{std::get_if(&variant)}; @@ -837,15 +866,7 @@ INSTRUCTION_HANDLER(AssertionDivisible) { } INSTRUCTION_DIRECT(AssertionTypeIntegerBounded, ValueIntegerBounds) { - if (target.is_integer()) { - const auto integer{target.to_integer()}; - return integer >= value.first && integer <= value.second; - } - if (target.is_integral()) { - const auto integer{target.as_integer()}; - return integer >= value.first && integer <= value.second; - } - return false; + return integer_within_bounds(target, value); } INSTRUCTION_HANDLER(AssertionTypeIntegerBounded) { @@ -875,7 +896,7 @@ INSTRUCTION_DIRECT(AssertionTypeIntegerLowerBound, ValueIntegerBounds) { return target.to_integer() >= value.first; } if (target.is_integral()) { - return target.as_integer() >= value.first; + return as_decimal(target) >= Decimal{value.first}; } return false; } @@ -891,6 +912,13 @@ INSTRUCTION_DIRECT(AssertionTypeIntegerLowerBoundStrict, ValueIntegerBounds) { if (target.is_integer()) { return target.to_integer() >= value.first; } + // An integer too large for 64 bits is held as a decimal, which the strict + // type check still reads as an integer when it was written as digits alone. + // The bounded counterpart needs no such branch, as a value out of that range + // cannot sit within bounds of that width either way + if (effective_type_strict_real(target) == JSON::Type::Integer) { + return target.to_decimal() >= Decimal{value.first}; + } return false; } @@ -901,6 +929,30 @@ INSTRUCTION_HANDLER(AssertionTypeIntegerLowerBoundStrict) { EVALUATE_END(AssertionTypeIntegerLowerBoundStrict); } +INSTRUCTION_DIRECT(AssertionNumberIntegerBounded, ValueIntegerBounds) { + return integer_within_bounds(target, value); +} + +INSTRUCTION_HANDLER(AssertionNumberIntegerBounded) { + EVALUATE_BEGIN_NON_STRING(AssertionNumberIntegerBounded, target.is_number()); + result = DIRECT(AssertionNumberIntegerBounded, target, + assume_value_copy(instruction.value)); + EVALUATE_END(AssertionNumberIntegerBounded); +} + +INSTRUCTION_HANDLER(AssertionNumberType) { + EVALUATE_BEGIN_NON_STRING(AssertionNumberType, target.is_number()); + switch (assume_value_copy(instruction.value)) { + case ValueNumberType::Float: + result = target.is_float(); + break; + case ValueNumberType::Double: + result = target.is_double(); + break; + } + EVALUATE_END(AssertionNumberType); +} + INSTRUCTION_HANDLER(AssertionStringType) { EVALUATE_BEGIN_IF_STRING(AssertionStringType); const auto value{assume_value_copy(instruction.value)}; @@ -968,6 +1020,9 @@ INSTRUCTION_HANDLER(AssertionStringType) { case ValueStringType::Color: result = is_css2_color(target); break; + case ValueStringType::Base64: + result = sourcemeta::core::is_base64(target); + break; default: std::unreachable(); } @@ -2484,12 +2539,12 @@ INSTRUCTION_HANDLER(LoopItemsPropertiesExactlyTypeStrictHash) { // Otherwise why emit this instruction? assert(!value.second.first.empty()); + result = true; + if (!target.is_array()) [[unlikely]] { EVALUATE_END(LoopItemsPropertiesExactlyTypeStrictHash); } - result = true; - const auto hashes_size{value.second.first.size()}; for (const auto &item : target.as_array()) { if (!item.is_object()) [[unlikely]] { @@ -2595,6 +2650,7 @@ INSTRUCTION_HANDLER(LoopItemsPropertiesExactlyTypeStrictHash3) { assert(!value.second.first.empty()); if (!target.is_array()) [[unlikely]] { + result = true; EVALUATE_END(LoopItemsPropertiesExactlyTypeStrictHash3); } @@ -2826,7 +2882,7 @@ using DispatchHandler = bool (*)( template // Must have same order as InstructionIndex // NOLINTNEXTLINE(modernize-avoid-c-arrays) -static constexpr DispatchHandler HANDLERS[101] = { +static constexpr DispatchHandler HANDLERS[103] = { AssertionFail, AssertionDefines, AssertionDefinesStrict, @@ -2867,6 +2923,8 @@ static constexpr DispatchHandler HANDLERS[101] = { AssertionTypeIntegerBoundedStrict, AssertionTypeIntegerLowerBound, AssertionTypeIntegerLowerBoundStrict, + AssertionNumberIntegerBounded, + AssertionNumberType, AssertionStringType, AssertionPropertyType, AssertionPropertyTypeEvaluate, diff --git a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h index 0c926a27d..8e08a4ed6 100644 --- a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h +++ b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h @@ -60,6 +60,8 @@ enum class InstructionIndex : std::uint8_t { AssertionTypeIntegerBoundedStrict, AssertionTypeIntegerLowerBound, AssertionTypeIntegerLowerBoundStrict, + AssertionNumberIntegerBounded, + AssertionNumberType, AssertionStringType, AssertionPropertyType, AssertionPropertyTypeEvaluate, @@ -166,6 +168,8 @@ constexpr std::string_view INSTRUCTION_NAMES[] = { "AssertionTypeIntegerBoundedStrict", "AssertionTypeIntegerLowerBound", "AssertionTypeIntegerLowerBoundStrict", + "AssertionNumberIntegerBounded", + "AssertionNumberType", "AssertionStringType", "AssertionPropertyType", "AssertionPropertyTypeEvaluate", diff --git a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_value.h b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_value.h index 54dbe2120..504758313 100644 --- a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_value.h +++ b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_value.h @@ -139,7 +139,8 @@ enum class ValueStringType : std::uint8_t { RelativeJSONPointer, UUID, Regex, - Color + Color, + Base64 }; /// @ingroup evaluator @@ -189,13 +190,19 @@ using ValueObjectProperties = std::vector< std::tuple>; /// @ingroup evaluator -using Value = std::variant< - ValueNone, ValueJSON, ValueSet, ValueString, ValueProperty, ValueStrings, - ValueStringSet, ValueTypes, ValueType, ValueRegex, ValueUnsignedInteger, - ValueRange, ValueBoolean, ValueNamedIndexes, ValueStringType, - ValueStringMap, ValuePropertyFilter, ValueIndexPair, ValuePointer, - ValueTypedProperties, ValueStringHashes, ValueTypedHashes, - ValueIntegerBounds, ValueIntegerBoundsWithSize, ValueObjectProperties>; +/// Represents a compiler step number logical type +enum class ValueNumberType : std::uint8_t { Float, Double }; + +/// @ingroup evaluator +using Value = + std::variant; } // namespace sourcemeta::blaze diff --git a/vendor/core/CMakeLists.txt b/vendor/core/CMakeLists.txt index 46301b87d..bad2eecb5 100644 --- a/vendor/core/CMakeLists.txt +++ b/vendor/core/CMakeLists.txt @@ -37,9 +37,9 @@ option(SOURCEMETA_CORE_JSONLD "Build the Sourcemeta Core JSON-LD library" ON) option(SOURCEMETA_CORE_JSONL "Build the Sourcemeta Core JSONL library" ON) option(SOURCEMETA_CORE_YAML "Build the Sourcemeta Core YAML library" ON) option(SOURCEMETA_CORE_JSONRPC "Build the Sourcemeta Core JSON-RPC library" ON) -option(SOURCEMETA_CORE_MCP "Build the Sourcemeta Core MCP library" ON) option(SOURCEMETA_CORE_HTTP "Build the Sourcemeta Core HTTP library" ON) option(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL "Use system cURL for the Sourcemeta Core HTTP library" OFF) +option(SOURCEMETA_CORE_MCP "Build the Sourcemeta Core MCP library" ON) option(SOURCEMETA_CORE_OPENAPI "Build the Sourcemeta Core OpenAPI library" ON) option(SOURCEMETA_CORE_JOSE "Build the Sourcemeta Core JOSE library" ON) option(SOURCEMETA_CORE_OAUTH "Build the Sourcemeta Core OAuth library" ON) @@ -271,14 +271,14 @@ if(SOURCEMETA_CORE_JSONRPC) add_subdirectory(src/core/jsonrpc) endif() -if(SOURCEMETA_CORE_MCP) - add_subdirectory(src/core/mcp) -endif() - if(SOURCEMETA_CORE_HTTP) add_subdirectory(src/core/http) endif() +if(SOURCEMETA_CORE_MCP) + add_subdirectory(src/core/mcp) +endif() + if(SOURCEMETA_CORE_OPENAPI) add_subdirectory(src/core/openapi) endif() @@ -470,14 +470,14 @@ if(SOURCEMETA_CORE_TESTS) add_subdirectory(test/jsonrpc) endif() - if(SOURCEMETA_CORE_MCP) - add_subdirectory(test/mcp) - endif() - if(SOURCEMETA_CORE_HTTP) add_subdirectory(test/http) endif() + if(SOURCEMETA_CORE_MCP) + add_subdirectory(test/mcp) + endif() + if(SOURCEMETA_CORE_OPENAPI) add_subdirectory(test/openapi) endif() diff --git a/vendor/core/config.cmake.in b/vendor/core/config.cmake.in index 88ea4047a..7b62dd62a 100644 --- a/vendor/core/config.cmake.in +++ b/vendor/core/config.cmake.in @@ -32,8 +32,8 @@ if(NOT SOURCEMETA_CORE_COMPONENTS) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonld) list(APPEND SOURCEMETA_CORE_COMPONENTS yaml) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonrpc) - list(APPEND SOURCEMETA_CORE_COMPONENTS mcp) list(APPEND SOURCEMETA_CORE_COMPONENTS http) + list(APPEND SOURCEMETA_CORE_COMPONENTS mcp) list(APPEND SOURCEMETA_CORE_COMPONENTS openapi) list(APPEND SOURCEMETA_CORE_COMPONENTS jose) list(APPEND SOURCEMETA_CORE_COMPONENTS oauth) @@ -222,12 +222,26 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonrpc.cmake") elseif(component STREQUAL "mcp") - include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") + if("@SOURCEMETA_CORE_CRYPTO_BACKEND@" STREQUAL "openssl") + find_dependency(OpenSSL 3.0) + endif() + if(@SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL@) + find_dependency(CURL) + endif() include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_crypto.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_punycode.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_idna.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_dns.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_time.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_ip.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_uri.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_http.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonrpc.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_mcp.cmake") elseif(component STREQUAL "http") diff --git a/vendor/core/src/core/crypto/crypto_aes_block.h b/vendor/core/src/core/crypto/crypto_aes_block.h index 9b877fc1a..ac7c3d88d 100644 --- a/vendor/core/src/core/crypto/crypto_aes_block.h +++ b/vendor/core/src/core/crypto/crypto_aes_block.h @@ -153,16 +153,18 @@ inline auto aes_expand_key(const std::string_view key) -> AesKeySchedule { return schedule; } +// AddRoundKey of FIPS 197 Section 5.1.4, which adds the round key of the given +// round into the state a byte at a time over the field of two elements +inline auto aes_add_round_key(const AesKeySchedule &schedule, AesBlock &state, + const std::size_t round) -> void { + for (std::size_t index = 0; index < 16; ++index) { + state[index] ^= schedule.round_keys[(round * 16) + index]; + } +} + inline auto aes_encrypt_block(const AesKeySchedule &schedule, AesBlock state) -> AesBlock { - const auto &round_keys{schedule.round_keys}; - const auto add_round_key{[&round_keys, &state](const std::size_t round) { - for (std::size_t index = 0; index < 16; ++index) { - state[index] ^= round_keys[(round * 16) + index]; - } - }}; - - add_round_key(0); + aes_add_round_key(schedule, state, 0); for (std::size_t round = 1; round <= schedule.rounds; ++round) { for (auto &byte : state) { byte = AES_SUBSTITUTION[byte]; @@ -194,7 +196,7 @@ inline auto aes_encrypt_block(const AesKeySchedule &schedule, AesBlock state) } } - add_round_key(round); + aes_add_round_key(schedule, state, round); } return state; @@ -202,14 +204,7 @@ inline auto aes_encrypt_block(const AesKeySchedule &schedule, AesBlock state) inline auto aes_decrypt_block(const AesKeySchedule &schedule, AesBlock state) -> AesBlock { - const auto &round_keys{schedule.round_keys}; - const auto add_round_key{[&round_keys, &state](const std::size_t round) { - for (std::size_t index = 0; index < 16; ++index) { - state[index] ^= round_keys[(round * 16) + index]; - } - }}; - - add_round_key(schedule.rounds); + aes_add_round_key(schedule, state, schedule.rounds); for (std::size_t round = schedule.rounds; round-- > 0;) { // InvShiftRows over the column-major state (FIPS 197 Section 5.3.1) const AesBlock shifted{{state[0], state[13], state[10], state[7], state[4], @@ -222,7 +217,7 @@ inline auto aes_decrypt_block(const AesKeySchedule &schedule, AesBlock state) byte = AES_INVERSE_SUBSTITUTION[byte]; } - add_round_key(round); + aes_add_round_key(schedule, state, round); if (round != 0) { // InvMixColumns (FIPS 197 Section 5.3.3) diff --git a/vendor/core/src/core/crypto/crypto_base64.cc b/vendor/core/src/core/crypto/crypto_base64.cc index 959863207..9fb28b0aa 100644 --- a/vendor/core/src/core/crypto/crypto_base64.cc +++ b/vendor/core/src/core/crypto/crypto_base64.cc @@ -19,7 +19,9 @@ constexpr std::string_view BASE64_ALPHABET{ constexpr std::string_view BASE64URL_ALPHABET{ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"}; -constexpr std::uint8_t INVALID_SEXTET{0xFF}; +// The greatest value either alphabet denotes, so that a table entry above it +// stands for an octet outside the alphabet +constexpr std::uint32_t MAX_SEXTET{0x3F}; // The inverse of each alphabet, mapping every octet to the sextet it denotes, // or to the invalid sextet when the octet is not part of that alphabet @@ -106,23 +108,20 @@ auto encode(const std::string_view input, const std::string_view alphabet, } } -template -auto decode_into(const std::string_view input, - const std::array &table, const bool padding, - Output &output) -> bool { - // Decoding appends to the output, so a failure after some bytes were written - // rolls the output back to its original length rather than leaving a partial - // decode in a reused buffer - const auto base{output.size()}; +// The characters that carry data, with any trailing padding removed, for an +// input whose overall shape is accepted. RFC 4648 Section 4: "Special +// processing is performed if fewer than 24 bits are available at the end of the +// data being encoded. A full encoding quantum is always completed at the end of +// a quantity", hence the padded form must be a multiple of four characters. A +// trailing quantum of a single character carries no whole octet and is never +// produced by either form +auto significant(const std::string_view input, const bool padding) + -> std::optional { auto data{input}; if (padding) { - // RFC 4648 Section 4: "Special processing is performed if fewer than 24 - // bits are available at the end of the data being encoded. A full encoding - // quantum is always completed at the end of a quantity", hence the padded - // form must be a multiple of four characters if (data.size() % 4 != 0) { - return false; + return std::nullopt; } if (data.ends_with('=')) { @@ -134,9 +133,64 @@ auto decode_into(const std::string_view input, } if (data.size() % 4 == 1) { + return std::nullopt; + } + + return data; +} + +// RFC 4648 Section 3.5: "Implementations MAY chose to reject the encoding if +// the pad bits have not been set to zero". We reject so that every value has +// exactly one accepted encoding. Only the last character of a trailing partial +// quantum carries such bits +auto canonical(const std::string_view data, + const std::array &table) -> bool { + const auto remaining{data.size() % 4}; + if (remaining == 0) { + return true; + } + + const std::uint32_t last{table[static_cast(data.back())]}; + if (remaining == 2) { + return (last & 0x0FU) == 0; + } + + return (last & 0x03U) == 0; +} + +// Deciding validity never needs an assembled octet, so the characters are only +// looked up to accumulate whether any of them fell outside the alphabet +auto validate(const std::string_view input, + const std::array &table, const bool padding) + -> bool { + const auto data{significant(input, padding)}; + if (!data.has_value()) { return false; } + std::uint32_t combined{0}; + for (const auto character : data.value()) { + combined |= table[static_cast(character)]; + } + + return combined <= MAX_SEXTET && canonical(data.value(), table); +} + +template +auto decode_into(const std::string_view input, + const std::array &table, const bool padding, + Output &output) -> bool { + const auto accepted{significant(input, padding)}; + if (!accepted.has_value()) { + return false; + } + + const auto data{accepted.value()}; + + // Decoding appends to the output, so a failure after some bytes were written + // rolls the output back to its original length rather than leaving a partial + // decode in a reused buffer + const auto base{output.size()}; output.reserve(output.size() + ((data.size() / 4) * 3) + 2); std::size_t index{0}; @@ -148,8 +202,7 @@ auto decode_into(const std::string_view input, table[static_cast(data[index + 2])]}; const std::uint32_t fourth{ table[static_cast(data[index + 3])]}; - if (first == INVALID_SEXTET || second == INVALID_SEXTET || - third == INVALID_SEXTET || fourth == INVALID_SEXTET) { + if ((first | second | third | fourth) > MAX_SEXTET) { output.resize(base, '\0'); return false; } @@ -162,16 +215,12 @@ auto decode_into(const std::string_view input, index += 4; } - // RFC 4648 Section 3.5: "Implementations MAY chose to reject the encoding - // if the pad bits have not been set to zero". We reject so that every value - // has exactly one accepted encoding const auto remaining{data.size() - index}; if (remaining == 2) { const std::uint32_t first{table[static_cast(data[index])]}; const std::uint32_t second{ table[static_cast(data[index + 1])]}; - if (first == INVALID_SEXTET || second == INVALID_SEXTET || - (second & 0x0FU) != 0) { + if ((first | second) > MAX_SEXTET || !canonical(data, table)) { output.resize(base, '\0'); return false; } @@ -183,8 +232,7 @@ auto decode_into(const std::string_view input, table[static_cast(data[index + 1])]}; const std::uint32_t third{ table[static_cast(data[index + 2])]}; - if (first == INVALID_SEXTET || second == INVALID_SEXTET || - third == INVALID_SEXTET || (third & 0x03U) != 0) { + if ((first | second | third) > MAX_SEXTET || !canonical(data, table)) { output.resize(base, '\0'); return false; } @@ -237,6 +285,10 @@ auto base64_decode(const std::string_view input, SecureString &output) -> bool { output); } +auto is_base64(const std::string_view input) -> bool { + return validate(input, BASE64_DECODE_TABLE, true); +} + auto base64url_encode(const std::string_view input, std::ostream &output) -> void { output << base64url_encode(input); @@ -280,4 +332,8 @@ auto base64url_decode(const std::string_view input) return output; } +auto is_base64url(const std::string_view input) -> bool { + return validate(input, BASE64URL_DECODE_TABLE, false); +} + } // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_bignum.h b/vendor/core/src/core/crypto/crypto_bignum.h index 61e23fe91..4f9dae95e 100644 --- a/vendor/core/src/core/crypto/crypto_bignum.h +++ b/vendor/core/src/core/crypto/crypto_bignum.h @@ -15,6 +15,7 @@ #include // std::max, std::min #include // std::array #include // std::bit_width +#include // assert #include // std::size_t #include // std::uint8_t, std::uint64_t #include // std::optional @@ -115,12 +116,11 @@ inline auto bignum_from_u64(const std::uint64_t value) noexcept template inline auto bignum_from_hex(const std::string_view hex) -> BasicBignum { - // An odd length is decoded as if a zero nibble had been prepended + // An odd length is decoded as if a zero nibble had been prepended. Every + // number read this way is one written out in this source, so a spelling that + // is no number at all is a mistake here rather than anything a caller did const auto bytes{hex_to_bytes(hex, true)}; - if (!bytes.has_value()) { - return BasicBignum{}; - } - + assert(bytes.has_value()); return bignum_from_bytes(bytes.value()); } @@ -249,8 +249,10 @@ inline auto bignum_reduce(BasicBignum &value, const auto divisor_words{modulus.size}; // A zero modulus defines no residue to reduce into, and the division below - // reads the top two divisor words. An RSA modulus is only bounded from above - // on import, so a key carrying a one- or two-octet modulus reaches here + // would read past the divisor looking for its top two words. Nothing here + // reduces into one, as every modulus is the prime or the order of a curve or + // the modulus of a key, but turning it away is what holds those reads inside + // the divisor whether or not assertions are compiled in if (divisor_words == 0) { return; } diff --git a/vendor/core/src/core/crypto/crypto_ecc.h b/vendor/core/src/core/crypto/crypto_ecc.h index a9df10557..158b16f62 100644 --- a/vendor/core/src/core/crypto/crypto_ecc.h +++ b/vendor/core/src/core/crypto/crypto_ecc.h @@ -12,6 +12,7 @@ #include "crypto_bignum.h" #include // std::array +#include // assert #include // std::size_t #include // std::uint8_t, std::uint64_t #include // std::string_view @@ -316,10 +317,14 @@ inline auto point_is_infinity(const JacobianPoint &point) noexcept -> bool { inline auto point_double(const JacobianPoint &point, const EllipticCurveParameters &curve) -> JacobianPoint { - if (point_is_infinity(point) || bignum_is_zero(point.y)) { + if (point_is_infinity(point)) { return {}; } + // A point whose ordinate is zero has order two, which a curve of prime order + // does not have + assert(!bignum_is_zero(point.y)); + // The doubling formula for curves with coefficient -3 (EFD dbl-2001-b), // which trades the coefficient multiplication and a squaring for one more // subtraction, and computes every small multiple as a chain of modular @@ -359,13 +364,11 @@ inline auto point_double(const JacobianPoint &point, // Point addition in Jacobian coordinates inline auto point_add(const JacobianPoint &left, const JacobianPoint &right, const EllipticCurveParameters &curve) -> JacobianPoint { - if (point_is_infinity(left)) { - return right; - } - - if (point_is_infinity(right)) { - return left; - } + // The one caller combines two points it built with a unit third coordinate, + // so neither is the point at infinity. The mixed addition below is the one + // that accumulates from there + assert(!point_is_infinity(left)); + assert(!point_is_infinity(right)); const auto &prime{curve.prime}; const auto left_z_squared{field_mod_multiply(left.z, left.z, curve)}; diff --git a/vendor/core/src/core/crypto/crypto_eddsa.h b/vendor/core/src/core/crypto/crypto_eddsa.h index 84bd1acee..980b7fe17 100644 --- a/vendor/core/src/core/crypto/crypto_eddsa.h +++ b/vendor/core/src/core/crypto/crypto_eddsa.h @@ -16,6 +16,7 @@ #include "crypto_shake256.h" #include // std::array +#include // assert #include // std::size_t #include // std::uint8_t #include // std::optional, std::nullopt @@ -345,10 +346,8 @@ inline auto edwards_public_key_point(const CurveBignum &scalar, inline auto edwards25519_decode_point(const std::string_view encoding, const EdwardsParameters ¶meters) -> std::optional { - if (encoding.size() != 32) { - return std::nullopt; - } - + // Whoever hands an encoding over has already held it to the width of one + assert(encoding.size() == 32); // The final bit holds the sign of x, the remaining bits the little-endian y std::string bytes{encoding}; const auto sign_bit{ @@ -542,10 +541,8 @@ inline auto edwards25519_prune_scalar(std::string &scalar_bytes) noexcept // Section 5.1.5) inline auto edwards25519_public_key(const std::string_view secret) -> std::optional { - if (secret.size() != 32) { - return std::nullopt; - } - + // A key of this kind only comes about once its seed was read at this width + assert(secret.size() == 32); const auto ¶meters{edwards25519()}; auto hashed{sha512_digest(secret)}; const SecureBufferScope hashed_scope{hashed.data(), hashed.size()}; @@ -562,10 +559,8 @@ inline auto edwards25519_public_key(const std::string_view secret) inline auto edwards25519_sign(const std::string_view secret, const std::string_view message) -> std::optional { - if (secret.size() != 32) { - return std::nullopt; - } - + // A key of this kind only comes about once its seed was read at this width + assert(secret.size() == 32); const auto ¶meters{edwards25519()}; // The key derivation hash carries both the secret scalar and the nonce // prefix, so it and everything derived from it below is wiped before @@ -638,10 +633,8 @@ inline auto edwards25519_sign(const std::string_view secret, inline auto edwards448_decode_point(const std::string_view encoding, const EdwardsParameters ¶meters) -> std::optional { - if (encoding.size() != 57) { - return std::nullopt; - } - + // Whoever hands an encoding over has already held it to the width of one + assert(encoding.size() == 57); // The final bit holds the sign of x, the remaining bits the little-endian y std::string bytes{encoding}; const auto sign_bit{ @@ -815,10 +808,8 @@ inline auto edwards448_prune_scalar(std::string &scalar_bytes) noexcept // Section 5.2.5) inline auto edwards448_public_key(const std::string_view secret) -> std::optional { - if (secret.size() != 57) { - return std::nullopt; - } - + // A key of this kind only comes about once its seed was read at this width + assert(secret.size() == 57); const auto ¶meters{edwards448()}; auto digest{shake256(secret, 114)}; const SecureStringScope digest_scope{digest}; @@ -833,10 +824,8 @@ inline auto edwards448_public_key(const std::string_view secret) inline auto edwards448_sign(const std::string_view secret, const std::string_view message) -> std::optional { - if (secret.size() != 57) { - return std::nullopt; - } - + // A key of this kind only comes about once its seed was read at this width + assert(secret.size() == 57); const auto ¶meters{edwards448()}; // The key derivation hash carries both the secret scalar and the nonce // prefix, so it and everything derived from it below is wiped before diff --git a/vendor/core/src/core/crypto/crypto_pkcs8.h b/vendor/core/src/core/crypto/crypto_pkcs8.h index dba3939f4..a3453dd2c 100644 --- a/vendor/core/src/core/crypto/crypto_pkcs8.h +++ b/vendor/core/src/core/crypto/crypto_pkcs8.h @@ -51,6 +51,21 @@ enum class PKCS8KeyKind : std::uint8_t { RSA, EllipticCurve, Edwards }; // The parsed shape of an RFC 5958 PrivateKeyInfo, where `key` views the // algorithm specific privateKey octets that each backend parses further +// The components of a PKCS#1 RSAPrivateKey, as views into the key they were +// read from. Handing these back is what keeps whatever consumes the key from +// walking the same structure a second time, where a second walk can only +// repeat what the gate below already turned away +struct RSAPrivateKeyParts { + std::string_view version{}; + std::string_view modulus{}; + std::string_view public_exponent{}; + std::string_view private_exponent{}; + // Where the components of the two-prime form begin, which the gate below + // does not look at because what is required of them is left to whatever + // consumes the key + std::string_view rest{}; +}; + struct PKCS8Key { PKCS8KeyKind kind; EllipticCurve curve; @@ -59,6 +74,9 @@ struct PKCS8Key { // Set when the algorithm is id-RSASSA-PSS rather than rsaEncryption, so that // such a key is refused for RSASSA-PKCS1-v1_5 signing (RFC 8017 Appendix A.2) bool rsa_pss_restricted{false}; + // Set when the algorithm is one of the RSA ones, where what the key holds + // has already been read apart to admit it + RSAPrivateKeyParts rsa{}; }; // Whether a PKCS#1 RSAPrivateKey carries usable components (RFC 8017 Appendix @@ -71,34 +89,35 @@ struct PKCS8Key { // in each backend, since only the reference one reads these components itself // and the rest hand the blob straight to a platform that does not apply the // rule -inline auto rsa_private_key_acceptable(const std::string_view key) -> bool { +inline auto read_rsa_private_key(const std::string_view key) + -> std::optional { // A canonical RSAPrivateKey is exactly one SEQUENCE, so bytes trailing it // mark a malformed encoding (X.690 Section 10.1), the same rule the enclosing // PrivateKeyInfo is held to const auto sequence{der_read(key)}; if (!sequence.has_value() || sequence->tag != 0x30 || !sequence->rest.empty()) { - return false; + return std::nullopt; } const auto version{der_read(sequence->content)}; if (!version.has_value() || version->tag != 0x02) { - return false; + return std::nullopt; } const auto modulus{der_read(version->rest)}; if (!modulus.has_value() || modulus->tag != 0x02) { - return false; + return std::nullopt; } const auto public_exponent{der_read(modulus->rest)}; if (!public_exponent.has_value() || public_exponent->tag != 0x02) { - return false; + return std::nullopt; } const auto private_exponent{der_read(public_exponent->rest)}; if (!private_exponent.has_value() || private_exponent->tag != 0x02) { - return false; + return std::nullopt; } const auto modulus_value{der_unsigned_integer(modulus->content)}; @@ -106,13 +125,55 @@ inline auto rsa_private_key_acceptable(const std::string_view key) -> bool { der_unsigned_integer(public_exponent->content)}; const auto private_exponent_value{ der_unsigned_integer(private_exponent->content)}; - return modulus_value.has_value() && public_exponent_value.has_value() && - private_exponent_value.has_value() && !modulus_value->empty() && - !private_exponent_value->empty() && - modulus_value->size() <= MAXIMUM_KEY_BYTES && - private_exponent_value->size() <= MAXIMUM_KEY_BYTES && - rsa_public_exponent_acceptable(public_exponent_value.value(), - modulus_value.value()); + if (!modulus_value.has_value() || !public_exponent_value.has_value() || + !private_exponent_value.has_value() || modulus_value->empty() || + private_exponent_value->empty() || + modulus_value->size() > MAXIMUM_KEY_BYTES || + private_exponent_value->size() > MAXIMUM_KEY_BYTES || + !rsa_public_exponent_acceptable(public_exponent_value.value(), + modulus_value.value())) { + return std::nullopt; + } + + return RSAPrivateKeyParts{.version = version->content, + .modulus = modulus_value.value(), + .public_exponent = public_exponent_value.value(), + .private_exponent = private_exponent_value.value(), + .rest = private_exponent->rest}; +} + +// Whether a SEC 1 ECPrivateKey carries a usable private scalar, which RFC 5915 +// Section 3 writes as the OCTET STRING following the version. SEC 1 Section 3.2 +// draws the private key from the interval [1, n-1], so a zero scalar, or one at +// or above the group order, names no key. Checked here rather than in each +// backend, as the platform ones hand the blob straight to a system that does +// not apply the rule +inline auto ec_private_key_acceptable(const std::string_view key, + const EllipticCurve curve) -> bool { + // A canonical ECPrivateKey is exactly one SEQUENCE, so bytes trailing it mark + // a malformed encoding (X.690 Section 10.1), the same rule the enclosing + // PrivateKeyInfo is held to + const auto sequence{der_read(key)}; + if (!sequence.has_value() || sequence->tag != 0x30 || + !sequence->rest.empty()) { + return false; + } + + // RFC 5915 Section 3 constrains the version to ecPrivkeyVer1, saying it + // "SHALL be set to ecPrivkeyVer1, which is of type INTEGER and whose value is + // one (1)", so no other value names a key this reads + const auto version{der_read(sequence->content)}; + if (!version.has_value() || version->tag != 0x02 || + version->content.size() != 1 || version->content.front() != '\x01') { + return false; + } + + const auto scalar{der_read(version->rest)}; + if (!scalar.has_value() || scalar->tag != 0x04) { + return false; + } + + return ec_private_scalar_in_range(scalar->content, curve); } // Parse an RFC 5958 PrivateKeyInfo, identifying the algorithm from its object @@ -160,7 +221,8 @@ inline auto parse_pkcs8(const std::string_view der) -> std::optional { // NOLINTEND(modernize-raw-string-literal) if (oid->content == RSA || oid->content == RSA_PSS) { - if (!rsa_private_key_acceptable(private_key->content)) { + const auto parts{read_rsa_private_key(private_key->content)}; + if (!parts.has_value()) { return std::nullopt; } @@ -168,7 +230,8 @@ inline auto parse_pkcs8(const std::string_view der) -> std::optional { .curve = {}, .edwards_curve = {}, .key = private_key->content, - .rsa_pss_restricted = oid->content == RSA_PSS}; + .rsa_pss_restricted = oid->content == RSA_PSS, + .rsa = parts.value()}; } if (oid->content == ED25519 || oid->content == ED448) { @@ -202,6 +265,10 @@ inline auto parse_pkcs8(const std::string_view der) -> std::optional { return std::nullopt; } + if (!ec_private_key_acceptable(private_key->content, curve)) { + return std::nullopt; + } + return PKCS8Key{.kind = PKCS8KeyKind::EllipticCurve, .curve = curve, .edwards_curve = {}, diff --git a/vendor/core/src/core/crypto/crypto_random_other.cc b/vendor/core/src/core/crypto/crypto_random_other.cc index 36a9b319f..aba6a0568 100644 --- a/vendor/core/src/core/crypto/crypto_random_other.cc +++ b/vendor/core/src/core/crypto/crypto_random_other.cc @@ -24,8 +24,6 @@ #include // std::min #include // std::size_t #include // std::uint8_t -#include // std::ifstream -#include // std::ios::binary #include // std::span #include // std::runtime_error #endif @@ -50,37 +48,21 @@ auto fill_random_bytes(std::span bytes) -> void { } #else // getentropy draws from the kernel cryptographic generator and fails closed, - // never returning low-quality bytes, but fills at most 256 bytes per call + // never returning low-quality bytes, but fills at most 256 bytes per call. + // + // There is no second source behind it. The call is reached through the + // kernel one that every system this builds on carries, and a refusal from + // it is a refusal by whoever set the policy that produced it rather than a + // gap to route around: reading the random device instead would walk past a + // sandbox that turned the call away on purpose. So a failure here is final, + // which is how the generator on the other side of this file answers too constexpr std::size_t maximum_per_call{256}; std::size_t offset{0}; while (offset < bytes.size()) { const auto chunk{std::min(bytes.size() - offset, maximum_per_call)}; if (getentropy(bytes.data() + offset, chunk) != 0) { - // Read the rest from the kernel random device, which is only needed on - // the rare platforms where getentropy is present but the kernel call is - // unavailable - std::ifstream device{"/dev/urandom", std::ios::binary}; - if (!device) { - throw std::runtime_error("Could not open the system random device"); - } - - // A single read can return fewer bytes than requested when interrupted - // by a signal, so the buffer is filled across as many reads as it takes, - // failing only when a read makes no progress or the device faults - while (offset < bytes.size()) { - device.read(reinterpret_cast(bytes.data() + offset), - static_cast(bytes.size() - offset)); - const auto count{device.gcount()}; - if (device.bad() || count <= 0) { - throw std::runtime_error( - "Could not read from the system random device"); - } - - offset += static_cast(count); - device.clear(); - } - - return; + throw std::runtime_error( + "Could not generate random bytes with the system entropy call"); } offset += chunk; diff --git a/vendor/core/src/core/crypto/crypto_sign_apple.cc b/vendor/core/src/core/crypto/crypto_sign_apple.cc index 887db8217..1288584a9 100644 --- a/vendor/core/src/core/crypto/crypto_sign_apple.cc +++ b/vendor/core/src/core/crypto/crypto_sign_apple.cc @@ -142,13 +142,13 @@ auto native_ec_private_key(const std::string_view sec1, rest = element->rest; } - const auto stripped_scalar{ - sourcemeta::core::strip_left(scalar->content, '\x00')}; - if (point.empty() || stripped_scalar.empty() || - stripped_scalar.size() > field_bytes) { + if (point.empty()) { return nullptr; } + const auto stripped_scalar{ + sourcemeta::core::strip_left(scalar->content, '\x00')}; + std::string data{point}; data.append(sourcemeta::core::pad_left(stripped_scalar, field_bytes, '\x00')); return native_private_key(kSecAttrKeyTypeECSECPrimeRandom, data); diff --git a/vendor/core/src/core/crypto/crypto_sign_other.cc b/vendor/core/src/core/crypto/crypto_sign_other.cc index 4e4dd2a9c..cca367452 100644 --- a/vendor/core/src/core/crypto/crypto_sign_other.cc +++ b/vendor/core/src/core/crypto/crypto_sign_other.cc @@ -192,8 +192,8 @@ auto hmac(const SignatureHashFunction hash, const std::string_view key, return secure_digest_message(hash, outer_input); } -// RFC 6979 Section 2.3.2 bits2int, which is also the FIPS 186-4 Section 6.4 -// truncation of a bit string to the leftmost order-length bits +// RFC 6979 Section 2.3.2 bits2int, which is also the FIPS 186-5 Section 6.4.1 +// step 2 truncation of a bit string to the leftmost order-length bits auto bits2int(const std::string_view bits, const std::size_t order_bits) -> CurveBignum { auto value{bignum_from_bytes(bits)}; @@ -223,7 +223,7 @@ auto sign_rsa(const PrivateKey::Internal &key, return bignum_to_bytes(representative, key.modulus.size()); } -// The signature for one nonce candidate (FIPS 186-4 Section 6.4.1), returning +// The signature for one nonce candidate (FIPS 186-5 Section 6.4.1), returning // no value when the candidate must be rejected and a fresh one drawn auto ecdsa_signature_for_nonce(const CurveBignum &nonce, const CurveBignum &digest_integer, @@ -242,7 +242,11 @@ auto ecdsa_signature_for_nonce(const CurveBignum &nonce, // The ladder leaves its projective output fixed-width rather than normalized, // so it does not leak the secret nonce through a value-dependent loop. The // nonce lies in [1, n), so the result is never the point at infinity, and the - // r == 0 rejection below is the FIPS 186-4 restart condition regardless + // r == 0 rejection below draws another candidate regardless, which is what + // RFC 6979 Section 3.2 step h asks for. FIPS 186-5 Section 6.4.1 step 11 + // instead ends in failure where the nonce came from its own deterministic + // procedure, that procedure yielding the same nonce again, where advancing + // the generator state the way RFC 6979 does yields a different one auto r{point_affine_x_constant_time(point, parameters)}; bignum_reduce(r, parameters.order); if (bignum_is_zero(r)) { @@ -275,7 +279,7 @@ auto ecdsa_signature_for_nonce(const CurveBignum &nonce, return signature; } -// ECDSA signature generation (FIPS 186-4 Section 6.4.1) with the per-signature +// ECDSA signature generation (FIPS 186-5 Section 6.4.1) with the per-signature // nonce derived deterministically from the private key and the message digest, // so that the signature never depends on the quality of the random generator // (RFC 6979 Section 3.2) @@ -337,10 +341,13 @@ auto sign_ecdsa(const EllipticCurve curve, const SignatureHashFunction hash, return signature; } - // RFC 6979 Section 3.2 step h: reseed before the next candidate - SecureString reseed{hmac_value}; - reseed.push_back('\x00'); - hmac_key = hmac(hash, hmac_key, reseed); + // RFC 6979 Section 3.2 step h, sub-step 3 advances the generator state + // before drawing the next candidate. Section 3.3 notes that the reseeding + // the underlying construction offers is never invoked here, so this is + // that state advance and not a reseed + SecureString key_input{hmac_value}; + key_input.push_back('\x00'); + hmac_key = hmac(hash, hmac_key, key_input); hmac_value = hmac(hash, hmac_key, hmac_value); } @@ -485,61 +492,22 @@ auto make_private_key(const std::string_view pem) -> std::optional { switch (parsed->kind) { case PKCS8KeyKind::RSA: { - // RFC 8017 Appendix A.1.2: RSAPrivateKey is a SEQUENCE of version, - // modulus, publicExponent, privateExponent, and the further primes - const auto sequence{der_read(parsed->key)}; - if (!sequence.has_value() || sequence->tag != 0x30) { - return std::nullopt; - } - - const auto version{der_read(sequence->content)}; - if (!version.has_value() || version->tag != 0x02) { - return std::nullopt; - } - - const auto modulus{der_read(version->rest)}; - if (!modulus.has_value() || modulus->tag != 0x02) { - return std::nullopt; - } - - const auto public_exponent{der_read(modulus->rest)}; - if (!public_exponent.has_value() || public_exponent->tag != 0x02) { - return std::nullopt; - } - - const auto private_exponent{der_read(public_exponent->rest)}; - if (!private_exponent.has_value() || private_exponent->tag != 0x02) { - return std::nullopt; - } - - // Decode each field as a canonical non-negative DER INTEGER, so a - // negative or non-canonically encoded value cannot be silently - // reinterpreted as a different positive number and used for signing - const auto modulus_value{der_unsigned_integer(modulus->content)}; - const auto public_exponent_value{ - der_unsigned_integer(public_exponent->content)}; - const auto private_exponent_value{ - der_unsigned_integer(private_exponent->content)}; - if (!modulus_value.has_value() || !public_exponent_value.has_value() || - !private_exponent_value.has_value() || modulus_value->empty() || - private_exponent_value->empty() || - modulus_value->size() > MAXIMUM_KEY_BYTES || - private_exponent_value->size() > MAXIMUM_KEY_BYTES || - !rsa_public_exponent_acceptable(public_exponent_value.value(), - modulus_value.value())) { - return std::nullopt; - } - + // RFC 8017 Appendix A.1.2 has RSAPrivateKey be a SEQUENCE of version, + // modulus, publicExponent, privateExponent and the further primes, and + // reading it apart is what admitted this key in the first place. Reading + // it again here would only repeat checks already made, which left this + // arm unable to turn anything away and the tests that name it covering + // the gate rather than the arm // RFC 8017 Appendix A.1.2: the two-prime form carries the CRT components // after the private exponent, which the private operation uses to take // the cheaper path of RFC 8017 Section 5.1.2 step 2.b const auto crt{ - read_rsa_crt_components(version->content, private_exponent->rest)}; + read_rsa_crt_components(parsed->rsa.version, parsed->rsa.rest)}; return PrivateKey{new PrivateKey::Internal{ .kind = PrivateKey::Type::RSA, - .modulus = std::string{modulus_value.value()}, - .public_exponent = std::string{public_exponent_value.value()}, - .private_exponent = std::string{private_exponent_value.value()}, + .modulus = std::string{parsed->rsa.modulus}, + .public_exponent = std::string{parsed->rsa.public_exponent}, + .private_exponent = std::string{parsed->rsa.private_exponent}, .prime1 = crt.has_value() ? std::string{crt->prime1} : std::string{}, .prime2 = crt.has_value() ? std::string{crt->prime2} : std::string{}, .exponent1 = @@ -575,19 +543,9 @@ auto make_private_key(const std::string_view pem) -> std::optional { } const auto stripped_scalar{strip_left(scalar->content, '\x00')}; - const auto scalar_width{curve_field_bytes(parsed->curve)}; - // A zero or oversized scalar is not a valid private key - if (stripped_scalar.empty() || stripped_scalar.size() > scalar_width) { - return std::nullopt; - } - - auto padded_scalar{pad_left(stripped_scalar, scalar_width, '\x00')}; + auto padded_scalar{ + pad_left(stripped_scalar, curve_field_bytes(parsed->curve), '\x00')}; const SecureStringScope padded_scalar_scope{padded_scalar}; - // A scalar at or above the curve order is not a valid private key, so it - // is rejected before it drives the public point recovery - if (!ec_private_scalar_in_range(padded_scalar, parsed->curve)) { - return std::nullopt; - } auto point{ec_public_from_scalar(parsed->curve, padded_scalar)}; return PrivateKey{ new PrivateKey::Internal{.kind = PrivateKey::Type::EllipticCurve, @@ -833,12 +791,9 @@ auto derive_public_key(const PrivateKey &key) -> std::optional { return make_rsa_public_key(internal->modulus, internal->public_exponent); case PrivateKey::Type::EllipticCurve: // The public coordinates are populated for every elliptic curve private - // key, whether supplied directly or recomputed from a PEM scalar at parse - // time, so the guard is a defensive check rather than a common path - if (internal->coordinate_x.empty()) { - return std::nullopt; - } - + // key, whether supplied directly or recomputed from a PEM scalar at + // parse time + assert(!internal->coordinate_x.empty()); return make_ec_public_key(internal->elliptic_curve, internal->coordinate_x, internal->coordinate_y); case PrivateKey::Type::Edwards: diff --git a/vendor/core/src/core/crypto/crypto_sign_windows.cc b/vendor/core/src/core/crypto/crypto_sign_windows.cc index a6fe5b679..70658f398 100644 --- a/vendor/core/src/core/crypto/crypto_sign_windows.cc +++ b/vendor/core/src/core/crypto/crypto_sign_windows.cc @@ -226,13 +226,13 @@ auto native_ec_private_key(const sourcemeta::core::EllipticCurve curve, rest = element->rest; } - const auto stripped_scalar{ - sourcemeta::core::strip_left(scalar->content, '\x00')}; - if (point.size() != 2 * field_bytes || stripped_scalar.empty() || - stripped_scalar.size() > field_bytes) { + if (point.size() != 2 * field_bytes) { return {.algorithm = nullptr, .key = nullptr}; } + const auto stripped_scalar{ + sourcemeta::core::strip_left(scalar->content, '\x00')}; + BCRYPT_ECCKEY_BLOB header{}; header.dwMagic = to_ecc_private_magic(curve); header.cbKey = static_cast(field_bytes); diff --git a/vendor/core/src/core/crypto/crypto_verify_other.cc b/vendor/core/src/core/crypto/crypto_verify_other.cc index f58990a46..97d6f4613 100644 --- a/vendor/core/src/core/crypto/crypto_verify_other.cc +++ b/vendor/core/src/core/crypto/crypto_verify_other.cc @@ -178,8 +178,8 @@ auto to_curve_parameters(const EllipticCurve curve) -> EllipticCurveParameters { return curve_p521(); } -// FIPS 186-4 Section 6.4 step 2, deriving the integer e from the leftmost bits -// of the message digest, truncated to the bit length of the order +// FIPS 186-5 Section 6.4.2 step 3, deriving the integer e from the leftmost +// bits of the message digest, truncated to the bit length of the order auto digest_to_integer(const SignatureHashFunction hash, const std::string_view message, const std::size_t order_bits) -> CurveBignum { @@ -268,7 +268,7 @@ auto verify_pss(const SignatureHashFunction hash, return emsa_pss_verify(hash, message, encoded_message, encoded_bits); } -// ECDSA verification (FIPS 186-4 Section 6.4) over the raw public point +// ECDSA verification (FIPS 186-5 Section 6.4.2) over the raw public point auto verify_ecdsa(const EllipticCurve curve, const SignatureHashFunction hash, const std::string_view coordinate_x, const std::string_view coordinate_y, @@ -288,19 +288,18 @@ auto verify_ecdsa(const EllipticCurve curve, const SignatureHashFunction hash, const auto s{ bignum_from_bytes(signature.substr(field_bytes))}; - // FIPS 186-4 Section 6.4.2 step 1: both integers must lie in [1, n - 1] + // FIPS 186-5 Section 6.4.2 step 1: both integers must lie in [1, n - 1] if (bignum_is_zero(r) || bignum_compare(r, parameters.order) >= 0 || bignum_is_zero(s) || bignum_compare(s, parameters.order) >= 0) { return false; } - // Reject coordinates wider than the field, matching the platform backends and - // preventing an oversized input from being truncated into a valid key + // Every coordinate that reaches here was stripped of its leading zeros and + // held to the width of the field when the key it belongs to was made, which + // is where an oversized input is turned away const auto stripped_x{strip_left(coordinate_x, '\x00')}; const auto stripped_y{strip_left(coordinate_y, '\x00')}; - if (stripped_x.size() > field_bytes || stripped_y.size() > field_bytes) { - return false; - } + assert(stripped_x.size() <= field_bytes && stripped_y.size() <= field_bytes); const auto public_x{bignum_from_bytes(stripped_x)}; const auto public_y{bignum_from_bytes(stripped_y)}; @@ -330,13 +329,13 @@ auto verify_ecdsa(const EllipticCurve curve, const SignatureHashFunction hash, const auto point{point_double_scalar_multiply(u1, generator, u2, public_point, parameters)}; - // FIPS 186-4 Section 6.4.2 step 6: reject when the combination is the point + // FIPS 186-5 Section 6.4.2 step 6: reject when the combination is the point // at infinity if (point_is_infinity(point)) { return false; } - // FIPS 186-4 Section 6.4.2 step 7: the signature is valid when the affine x + // FIPS 186-5 Section 6.4.2 step 9: the signature is valid when the affine x // coordinate, reduced modulo the order, equals r auto candidate{point_affine_x(point, parameters)}; bignum_reduce(candidate, parameters.order); diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_base64.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_base64.h index 51e1a3e23..63137cc95 100644 --- a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_base64.h +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_base64.h @@ -93,6 +93,21 @@ auto SOURCEMETA_CORE_CRYPTO_EXPORT base64_decode(const std::string_view input) auto SOURCEMETA_CORE_CRYPTO_EXPORT base64_decode(const std::string_view input, SecureString &output) -> bool; +/// @ingroup crypto +/// Check whether a string is a canonical padded Base64 encoding (RFC 4648 +/// Section 4) without decoding it, accepting exactly the inputs that decoding +/// accepts. The empty string encodes no bytes. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::is_base64("Zm9vYmFy")); +/// assert(!sourcemeta::core::is_base64("Zm9vYmF")); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT is_base64(const std::string_view input) + -> bool; + /// @ingroup crypto /// Encode a byte sequence using unpadded Base64url (RFC 4648 Section 5) into a /// stream. For example: @@ -167,6 +182,21 @@ base64url_encode(const std::string_view input, SecureString &output) -> void; auto SOURCEMETA_CORE_CRYPTO_EXPORT base64url_decode(const std::string_view input) -> std::optional; +/// @ingroup crypto +/// Check whether a string is a canonical unpadded Base64url encoding (RFC 4648 +/// Section 5) without decoding it, accepting exactly the inputs that decoding +/// accepts. The empty string encodes no bytes. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::is_base64url("Zm8")); +/// assert(!sourcemeta::core::is_base64url("Zm8=")); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT is_base64url(const std::string_view input) + -> bool; + } // namespace sourcemeta::core #endif diff --git a/vendor/core/src/core/diff/myers.h b/vendor/core/src/core/diff/myers.h index afb54c21f..5881f1adb 100644 --- a/vendor/core/src/core/diff/myers.h +++ b/vendor/core/src/core/diff/myers.h @@ -3,6 +3,7 @@ #include // assert #include // std::size_t, std::ptrdiff_t +#include // std::unreachable #include // std::vector namespace sourcemeta::core::internal { @@ -121,11 +122,12 @@ inline auto myers_middle_snake(const std::vector &original, } } + // A middle snake always lands within the bounds the search was given, so the + // loop above returns rather than running out. Unlike a function that ends + // after an exhaustive switch, the search is a loop whose returns are all + // conditional, so every compiler has to be told the end is never reached assert(false); - return {.original_start = original_start, - .modified_start = modified_start, - .original_end = original_end, - .modified_end = modified_end}; + std::unreachable(); } inline auto myers_compare(const std::vector &original, diff --git a/vendor/core/src/core/dns/idn_hostname.cc b/vendor/core/src/core/dns/idn_hostname.cc index 97f43464f..2dea961ca 100644 --- a/vendor/core/src/core/dns/idn_hostname.cc +++ b/vendor/core/src/core/dns/idn_hostname.cc @@ -4,6 +4,7 @@ #include #include +#include // assert #include // std::size_t #include // std::optional #include // std::string, std::u32string @@ -85,11 +86,9 @@ auto validate_idn_labels(const std::u32string_view codepoints) -> bool { } } - // RFC 5891 §4.2.4 / RFC 1035 §2.3.4: each label, in A-label form, - // must be 1-63 octets - if (a_label_octets > 63) { - return false; - } + // RFC 5891 §4.2.4 / RFC 1035 §2.3.4: each label, in A-label form, must be + // 1-63 octets, which classifying it above already held it to + assert(a_label_octets <= 63); if (!decoded_labels.empty()) { total_octets += 1; diff --git a/vendor/core/src/core/email/email.cc b/vendor/core/src/core/email/email.cc index fd15ad1cd..21c078106 100644 --- a/vendor/core/src/core/email/email.cc +++ b/vendor/core/src/core/email/email.cc @@ -6,6 +6,8 @@ #include "helpers.h" +#include // assert + namespace sourcemeta::core { // RFC 5321 §4.1.2 Mailbox grammar, returning the position of the separator @@ -74,7 +76,8 @@ static auto mailbox_separator(const std::string_view value) while (position < value.size() && value[position] != '@') { const auto character{value[position]}; if (character == '.') { - if (!atom_started || previous_was_dot) { + assert(!atom_started || !previous_was_dot); + if (!atom_started) { return std::nullopt; } previous_was_dot = true; diff --git a/vendor/core/src/core/email/helpers.h b/vendor/core/src/core/email/helpers.h index 8a53dc766..6b38f93f8 100644 --- a/vendor/core/src/core/email/helpers.h +++ b/vendor/core/src/core/email/helpers.h @@ -4,6 +4,7 @@ #include #include +#include // assert #include // std::uint8_t, std::uint16_t #include // std::string #include // std::string_view @@ -106,10 +107,9 @@ inline auto is_address_literal(const std::string_view domain) -> bool { if (domain.back() != ']') { return false; } - // RFC 5321 §4.5.3.1.2: 255-octet cap on a domain "name or number" - if (domain.size() > 255) { - return false; - } + // RFC 5321 §4.5.3.1.2 caps a domain "name or number" at 255 octets, which + // the 254-octet mailbox total of §4.5.3.1.3 already leaves out of reach + assert(domain.size() <= 255); const auto inner{domain.substr(1, domain.size() - 2)}; // RFC 5321 §4.1.3: IPv6-address-literal = "IPv6:" IPv6-addr. The tag names // the syntax that the rest of the literal follows, so a payload that is not diff --git a/vendor/core/src/core/gzip/deflate.h b/vendor/core/src/core/gzip/deflate.h index 3fcbc67e1..7017302eb 100644 --- a/vendor/core/src/core/gzip/deflate.h +++ b/vendor/core/src/core/gzip/deflate.h @@ -781,9 +781,6 @@ class DeflateEncoder { std::size_t distance{0}; std::size_t length{this->find(input, size, position, distance)}; this->insert(input, position); - if (length > 0 && length < 4) { - length = 0; - } if (previous_available && previous_length >= 4 && length <= previous_length) { diff --git a/vendor/core/src/core/html/include/sourcemeta/core/html_escape.h b/vendor/core/src/core/html/include/sourcemeta/core/html_escape.h index 3a4171695..28ffb8dd9 100644 --- a/vendor/core/src/core/html/include/sourcemeta/core/html_escape.h +++ b/vendor/core/src/core/html/include/sourcemeta/core/html_escape.h @@ -9,6 +9,7 @@ #include #include // std::array +#include // assert #include // std::same_as #include // std::size_t #include // std::uint8_t, std::uint64_t @@ -138,9 +139,10 @@ inline auto html_escape_append(Output &output, const std::string_view input) break; default: // The no-break space is replaced by its named entity (HTML Living - // Standard "escaping a string" step 2) - if (static_cast(input[position]) == 0xC2 && - position + 1 < size && + // Standard "escaping a string" step 2). The table above admits only + // its lead byte here, the five characters above having their own arm + assert(static_cast(input[position]) == 0xC2); + if (position + 1 < size && static_cast(input[position + 1]) == 0xA0) { replacement = " "; consumed = 2; diff --git a/vendor/core/src/core/html/include/sourcemeta/core/html_writer.h b/vendor/core/src/core/html/include/sourcemeta/core/html_writer.h index 46d059362..c46438778 100644 --- a/vendor/core/src/core/html/include/sourcemeta/core/html_writer.h +++ b/vendor/core/src/core/html/include/sourcemeta/core/html_writer.h @@ -45,7 +45,6 @@ class SOURCEMETA_CORE_HTML_EXPORT HTMLWriter { /// has no effect. SOURCEMETA_FORCEINLINE auto close() -> HTMLWriter & { this->tag_open_ = false; - assert(!this->tag_stack_.empty()); if (this->tag_stack_.empty()) [[unlikely]] { return *this; } diff --git a/vendor/core/src/core/http/CMakeLists.txt b/vendor/core/src/core/http/CMakeLists.txt index 6758a8370..a9e7f27f9 100644 --- a/vendor/core/src/core/http/CMakeLists.txt +++ b/vendor/core/src/core/http/CMakeLists.txt @@ -10,14 +10,17 @@ endif() sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME http PRIVATE_HEADERS problem.h status.h method.h message.h error.h system.h - aws_sigv4.h syntax.h - SOURCES helpers.h problem.cc match_accept.cc match_accept_language.cc + aws_sigv4.h syntax.h tracecontext.h baggage.h + SOURCES helpers.h entropy.h problem.cc match_accept.cc match_accept_language.cc negotiate_encoding.cc from_date.cc format_link.cc format_vary.cc field_list.cc accept_includes_all.cc content_type_matches.cc parse_bearer.cc parse_media_type.cc match_media_range.cc cache_control_max_age.cc serialize_cookie.cc aws_sigv4.cc serialize_challenge.cc parse_challenges.cc + is_traceparent.cc parse_traceparent.cc is_tracestate.cc + make_traceparent.cc continue_traceparent.cc + is_baggage.cc is_baggage_value.cc serialize_cache_control.cc scan_quoted_string.cc ${SOURCEMETA_CORE_HTTP_CLIENT_SOURCE}) diff --git a/vendor/core/src/core/http/client_curl.cc b/vendor/core/src/core/http/client_curl.cc index ae4ea0ea8..52be89fa9 100644 --- a/vendor/core/src/core/http/client_curl.cc +++ b/vendor/core/src/core/http/client_curl.cc @@ -54,6 +54,7 @@ auto curl_slist_free_all(curl_slist *list) -> void; // Option ids are a type-class base plus an index in libcurl's headers; the // resolved values are reproduced here (see the trailing comments) constexpr CURLcode CURLE_OK{0}; +constexpr CURLcode CURLE_OPERATION_TIMEDOUT{28}; constexpr long CURL_GLOBAL_ALL{3}; // SSL(1<<0) | WIN32(1<<1) constexpr CURLoption CURLOPT_URL{10002}; // STRINGPOINT + 2 constexpr CURLoption CURLOPT_FOLLOWLOCATION{52}; // LONG + 52 @@ -434,6 +435,13 @@ auto HTTPSystemRequest::send() const -> HTTPResponse { std::string{HTTP_RESPONSE_TOO_LARGE_MESSAGE}}; } + // libcurl reports an expired CURLOPT_TIMEOUT_MS and an expired + // CURLOPT_CONNECTTIMEOUT_MS with the same code, so which of the two + // bounds elapsed cannot be recovered here + if (code == CURLE_OPERATION_TIMEDOUT) { + throw HTTPTimeoutError{this->method_, this->url_, this->timeout_}; + } + throw HTTPError{this->method_, this->url_, api.easy_strerror(code)}; } diff --git a/vendor/core/src/core/http/client_darwin.mm b/vendor/core/src/core/http/client_darwin.mm index 9d01800db..ba606e6b1 100644 --- a/vendor/core/src/core/http/client_darwin.mm +++ b/vendor/core/src/core/http/client_darwin.mm @@ -29,6 +29,7 @@ auto to_nsstring(const std::string_view input) -> NSString * { @interface SourcemetaCoreHTTPDelegate : NSObject @property(nonatomic, assign) sourcemeta::core::HTTPResponse *response; @property(nonatomic, assign) std::string *failure; +@property(nonatomic, assign) bool *timedOut; @property(nonatomic, strong) dispatch_semaphore_t semaphore; @property(nonatomic, assign) BOOL hasMaximumResponseSize; @property(nonatomic, assign) std::size_t maximumResponseSize; @@ -90,6 +91,11 @@ - (void)URLSession:(NSURLSession *)session if (self.failure->empty()) { if (error != nil) { self.failure->assign([error.localizedDescription UTF8String]); + // NSURLSession reports an expired timeoutIntervalForRequest and an + // expired timeoutIntervalForResource with the same code, so which of + // the two bounds elapsed cannot be recovered here + *self.timedOut = [error.domain isEqualToString:NSURLErrorDomain] && + error.code == NSURLErrorTimedOut; } else if (![task.response isKindOfClass:[NSHTTPURLResponse class]]) { self.failure->assign("The response is not an HTTP response"); } else { @@ -124,6 +130,7 @@ - (void)URLSession:(NSURLSession *)session // terminate the process, so failures are recorded here and thrown // from the calling thread once the request settles std::string failure; + bool timed_out{false}; @autoreleasepool { NSURL *target{[NSURL URLWithString:to_nsstring(this->url_)]}; @@ -164,6 +171,7 @@ - (void)URLSession:(NSURLSession *)session [[SourcemetaCoreHTTPDelegate alloc] init]}; delegate.response = &response; delegate.failure = &failure; + delegate.timedOut = &timed_out; delegate.semaphore = dispatch_semaphore_create(0); delegate.hasMaximumResponseSize = this->maximum_response_size_.has_value() ? YES : NO; @@ -185,6 +193,10 @@ - (void)URLSession:(NSURLSession *)session } if (!failure.empty()) { + if (timed_out) { + throw HTTPTimeoutError{this->method_, this->url_, this->timeout_}; + } + throw HTTPError{this->method_, this->url_, failure}; } diff --git a/vendor/core/src/core/http/client_windows.cc b/vendor/core/src/core/http/client_windows.cc index 44a261f41..ff514de74 100644 --- a/vendor/core/src/core/http/client_windows.cc +++ b/vendor/core/src/core/http/client_windows.cc @@ -48,6 +48,21 @@ auto to_winhttp_timeout(const std::chrono::milliseconds value) -> int { return static_cast(value.count()); } +// WinHTTP reports an expired resolution, connection, send, or receive bound +// from WinHttpSetTimeouts with the same error, so which of the configured +// bounds elapsed cannot be recovered here +[[noreturn]] auto +throw_transfer_failure(const sourcemeta::core::HTTPMethod method, + const std::string &url, + const std::chrono::milliseconds timeout, + const DWORD error, const char *message) -> void { + if (error == ERROR_WINHTTP_TIMEOUT) { + throw sourcemeta::core::HTTPTimeoutError{method, url, timeout}; + } + + throw sourcemeta::core::HTTPError{method, url, message}; +} + class WinHTTPHandle { public: WinHTTPHandle(const HINTERNET handle) : handle_{handle} {} @@ -224,15 +239,19 @@ auto HTTPSystemRequest::send() const -> HTTPResponse { : request_headers.c_str(), request_headers.empty() ? 0 : static_cast(request_headers.size()), body_data, body_size, body_size, 0)}; + // The last error is captured before wiping the serialised headers, so the + // classification never reads a code some later call has replaced + const auto send_error{sent ? ERROR_SUCCESS : GetLastError()}; secure_zero(request_headers.data(), request_headers.size() * sizeof(wchar_t)); if (!sent) { - throw HTTPError{this->method_, this->url_, - "Failed to send the HTTP request"}; + throw_transfer_failure(this->method_, this->url_, this->timeout_, + send_error, "Failed to send the HTTP request"); } if (!WinHttpReceiveResponse(request_handle.get(), nullptr)) { - throw HTTPError{this->method_, this->url_, - "Failed to receive the HTTP response"}; + throw_transfer_failure(this->method_, this->url_, this->timeout_, + GetLastError(), + "Failed to receive the HTTP response"); } DWORD status_code{0}; @@ -252,8 +271,9 @@ auto HTTPSystemRequest::send() const -> HTTPResponse { while (true) { DWORD available{0}; if (!WinHttpQueryDataAvailable(request_handle.get(), &available)) { - throw HTTPError{this->method_, this->url_, - "Failed to read the HTTP response body"}; + throw_transfer_failure(this->method_, this->url_, this->timeout_, + GetLastError(), + "Failed to read the HTTP response body"); } if (available == 0) { @@ -273,8 +293,9 @@ auto HTTPSystemRequest::send() const -> HTTPResponse { DWORD read{0}; if (!WinHttpReadData(request_handle.get(), response.body.data() + offset, available, &read)) { - throw HTTPError{this->method_, this->url_, - "Failed to read the HTTP response body"}; + throw_transfer_failure(this->method_, this->url_, this->timeout_, + GetLastError(), + "Failed to read the HTTP response body"); } response.body.resize(offset + read); diff --git a/vendor/core/src/core/http/continue_traceparent.cc b/vendor/core/src/core/http/continue_traceparent.cc new file mode 100644 index 000000000..2064afe82 --- /dev/null +++ b/vendor/core/src/core/http/continue_traceparent.cc @@ -0,0 +1,52 @@ +#include +#include + +#include // std::ranges::copy +#include // std::array +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::span +#include // std::string_view + +#include "entropy.h" + +namespace sourcemeta::core { + +auto http_continue_traceparent(const std::string_view value, const bool sampled) + -> std::optional> { + // The value that arrived is the only source for the trace, so one that does + // not parse leaves nothing to carry on from + if (!http_is_traceparent(value)) { + return std::nullopt; + } + + std::array parent{}; + http_draw_identifier(parent); + + // The revision is the one this library implements rather than the one that + // was read, since a value is reconstructed "according to the highest version + // of the specification known to the implementation", and whatever a later + // one appended goes with it + // https://www.w3.org/TR/trace-context/#versioning-of-traceparent + std::array result{}; + result[0] = '0'; + result[1] = '0'; + result[2] = '-'; + + // The trace carries on, so its identifier is the one that arrived + std::ranges::copy(value.substr(3, 32), result.begin() + 3); + result[35] = '-'; + + // The operation is this one rather than the caller's, and drawing a fresh + // identifier is also what lets the sampling decision differ from the one + // that arrived: "The parent-id field MUST be set to a new value with the + // sampled flag update" + // https://www.w3.org/TR/trace-context/#mutating-the-traceparent-field + bytes_to_hex(parent, std::span{result}.subspan(36, 16)); + result[52] = '-'; + result[53] = '0'; + result[54] = sampled ? '1' : '0'; + return result; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/entropy.h b/vendor/core/src/core/http/entropy.h new file mode 100644 index 000000000..5dd7c1add --- /dev/null +++ b/vendor/core/src/core/http/entropy.h @@ -0,0 +1,33 @@ +#ifndef SOURCEMETA_CORE_HTTP_ENTROPY_H_ +#define SOURCEMETA_CORE_HTTP_ENTROPY_H_ + +#include + +#include // std::ranges::all_of +#include // std::uint8_t +#include // std::span +#include // std::runtime_error + +namespace sourcemeta::core { + +// An identifier of all zeroes is invalid, so a draw that lands on one cannot +// be written out. Taking the draw again covers that, and giving up after a +// few attempts reports a provider that only ever returns nothing rather than +// trusting it or spinning on it +// https://www.w3.org/TR/trace-context/#trace-id +inline auto http_draw_identifier(const std::span bytes) -> void { + for (auto attempt{0}; attempt < 8; ++attempt) { + random_bytes(bytes); + if (!std::ranges::all_of( + bytes, [](const std::uint8_t byte) { return byte == 0; })) { + return; + } + } + + throw std::runtime_error{ + "Could not draw a non-zero trace context identifier"}; +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/http/helpers.h b/vendor/core/src/core/http/helpers.h index 18cbc07da..999432010 100644 --- a/vendor/core/src/core/http/helpers.h +++ b/vendor/core/src/core/http/helpers.h @@ -7,6 +7,7 @@ #include // assert #include // std::size_t #include // std::uint8_t, std::uint16_t +#include // std::span #include // std::string_view #include // std::pair @@ -130,9 +131,11 @@ inline auto http_for_each_parameter(const std::string_view parameters, Visitor visit) -> void { std::size_t position{0}; while (position < parameters.size()) { - if (parameters[position] == ';') { - ++position; - } + // Splitting an entry from its parameters keeps the semicolon with them, + // and the scan of each parameter ends on the next one, so one always + // stands where a parameter begins + assert(parameters[position] == ';'); + ++position; while (position < parameters.size() && http_is_ows(parameters[position])) { ++position; } @@ -339,9 +342,8 @@ inline auto http_split_media_range(const std::string_view parameters) noexcept float quality{1.0F}; while (position < parameters.size()) { const std::size_t separator{position}; - if (parameters[position] == ';') { - ++position; - } + assert(parameters[position] == ';'); + ++position; while (position < parameters.size() && http_is_ows(parameters[position])) { ++position; } diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http.h b/vendor/core/src/core/http/include/sourcemeta/core/http.h index 6b439d5c8..215572230 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http.h @@ -7,6 +7,7 @@ // NOLINTBEGIN(misc-include-cleaner) #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include // NOLINTEND(misc-include-cleaner) #include // std::chrono::system_clock diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_baggage.h b/vendor/core/src/core/http/include/sourcemeta/core/http_baggage.h new file mode 100644 index 000000000..62b5b8f4c --- /dev/null +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_baggage.h @@ -0,0 +1,232 @@ +#ifndef SOURCEMETA_CORE_HTTP_BAGGAGE_H_ +#define SOURCEMETA_CORE_HTTP_BAGGAGE_H_ + +#ifndef SOURCEMETA_CORE_HTTP_EXPORT +#include +#endif + +#include +#include + +#include // assert +#include // std::invocable +#include // std::optional +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup http +/// Whether the input is a well-formed baggage value, which is the part of a +/// member after its equals sign. The grammar is in W3C Baggage Section 3.3.1, +/// and this is stricter than it on one point, noted below. +/// +/// The permitted octets leave out the control characters, whitespace, the +/// double quote, the comma, the semicolon and the backslash, so a value +/// carrying any of those has to percent-encode it. This also requires that a +/// percent introduces an encoded octet, which is stricter than the grammar, +/// where the percent is an octet like any other. The prose is what forbids a +/// bare one. An empty value is well formed, where a tracestate value has to +/// end in a mandatory non-blank character and so cannot be empty. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_is_baggage_value("alice")); +/// assert(sourcemeta::core::http_is_baggage_value("DF%2028")); +/// assert(sourcemeta::core::http_is_baggage_value("")); +/// assert(!sourcemeta::core::http_is_baggage_value("DF 28")); +/// assert(!sourcemeta::core::http_is_baggage_value("100%")); +/// ``` +/// +/// @see https://www.w3.org/TR/baggage/#value +/// @see https://www.w3.org/TR/trace-context/#value +SOURCEMETA_CORE_HTTP_EXPORT +auto http_is_baggage_value(const std::string_view value) noexcept -> bool; + +/// @ingroup http +/// Whether the input is a well-formed baggage field value. The grammar is in +/// W3C Baggage Section 3.3.1, and whitespace at the two outer ends of the +/// input is accepted although the list production does not place any there, +/// since a field value does not carry such whitespace to begin with. +/// +/// A member is a key and a value, optionally trailed by a semicolon-delimited +/// property set whose own entries may be a bare key. Whitespace around a key +/// or a value is allowed and is not part of it. A repeated key is allowed, +/// since "Uniqueness of keys between multiple list-members in a +/// baggage-string is not guaranteed", where tracestate holds that "Only one +/// entry per key is allowed". +/// +/// The grammar caps the list at 180 members. The separate figures of 64 +/// members and 8192 bytes are the point past which a platform "MAY drop +/// list-members" rather than a bound on what is well formed, so they are not +/// checked here. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_is_baggage( +/// "userId=alice,serverNode=DF%2028,isProduction=false")); +/// assert(sourcemeta::core::http_is_baggage("key1=value1;property1;k=v")); +/// assert(sourcemeta::core::http_is_baggage("userId = alice")); +/// assert(sourcemeta::core::http_is_baggage("a=1,a=2")); +/// assert(!sourcemeta::core::http_is_baggage("")); +/// assert(!sourcemeta::core::http_is_baggage("novalue")); +/// ``` +/// +/// @see https://www.w3.org/TR/baggage/#definition +/// @see https://www.w3.org/TR/baggage/#baggage-string +/// @see https://www.w3.org/TR/baggage/#limits +/// @see https://www.w3.org/TR/trace-context/#combined-header-value +/// @see https://www.rfc-editor.org/rfc/rfc9110#section-5.5 +SOURCEMETA_CORE_HTTP_EXPORT +auto http_is_baggage(const std::string_view value) noexcept -> bool; + +/// @ingroup http +/// Visit each baggage member in order as a key, a value, and the property set +/// trailing it, which is empty when the member carries none. The value is +/// handed over still encoded, since decoding it is a separate concern with a +/// rule of its own: "percent-encoded octet sequences that do not match the +/// UTF-8 encoding scheme MUST be replaced with the replacement code point +/// (U+FFFD)". The whole input is +/// checked before anything is visited, so a caller never acts on part of a +/// malformed value, which for baggage means never propagating a pair out of a +/// value it then discards. +/// +/// A key may repeat and the order of members is significant, so a caller +/// collecting these wants a sequence rather than a map. Every view borrows the +/// input, so the value they came from must outlive them. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::string flat; +/// assert(sourcemeta::core::http_parse_baggage( +/// "userId=alice,serverNode=DF%2028", +/// [&flat](const auto key, const auto value, const auto properties) { +/// flat += std::string{key} + "=" + std::string{value} + "[" + +/// std::string{properties} + "];"; +/// })); +/// assert(flat == "userId=alice[];serverNode=DF%2028[];"); +/// ``` +/// +/// @see https://www.w3.org/TR/baggage/#definition +/// @see https://www.w3.org/TR/baggage/#value +template + requires std::invocable +inline auto http_parse_baggage(const std::string_view value, Callback callback) + -> bool { + if (!http_is_baggage(value)) { + return false; + } + + auto rest{value}; + while (true) { + const auto comma{rest.find(',')}; + const auto member{trim(rest.substr(0, comma), http_is_ows)}; + const auto semicolon{member.find(';')}; + const auto pair{trim(member.substr(0, semicolon), http_is_ows)}; + const auto equal{pair.find('=')}; + + // Every member holds an equals sign, which the check above has settled, so + // the position below is never the one that stands for not found + assert(equal != std::string_view::npos); + callback(trim(pair.substr(0, equal), http_is_ows), + trim(pair.substr(equal + 1), http_is_ows), + semicolon == std::string_view::npos + ? member.substr(member.size()) + : member.substr(semicolon + 1)); + if (comma == std::string_view::npos) { + return true; + } + + rest.remove_prefix(comma + 1); + } +} + +/// @ingroup http +/// Visit each entry of a baggage property set in order, as a key and a value +/// that is absent when the entry is a bare key. That distinction is why the +/// value is optional rather than empty: the grammar admits an entry with no +/// value at all and an entry whose value is the empty string, and they are not +/// the same thing. +/// +/// The input is the property set handed over when visiting a member, which is +/// what follows the first semicolon and so does not include it. An empty set +/// is a set of no entries and parses. Note that the specification writes a set +/// with that leading semicolon still attached, where this does not take one. +/// +/// The input is checked before anything is visited, and nothing is visited +/// when it does not parse. Every view borrows the input. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::string flat; +/// assert(sourcemeta::core::http_parse_baggage_properties( +/// "property1;propertyKey=propertyValue", +/// [&flat](const auto key, const auto value) { +/// flat += std::string{key} + +/// (value.has_value() ? "=" + std::string{*value} : "") + ";"; +/// })); +/// assert(flat == "property1;propertyKey=propertyValue;"); +/// ``` +/// +/// @see https://www.w3.org/TR/baggage/#property +template + requires std::invocable> +inline auto http_parse_baggage_properties(const std::string_view properties, + Callback callback) -> bool { + // The repetition that carries properties may run zero times, so a member + // without any hands over nothing, and nothing is a set of no entries rather + // than a malformed one + // https://www.w3.org/TR/baggage/#definition + if (properties.empty()) { + return true; + } + + auto pending{properties}; + while (true) { + const auto semicolon{pending.find(';')}; + const auto entry{trim(pending.substr(0, semicolon), http_is_ows)}; + const auto equal{entry.find('=')}; + if (!http_is_token(trim(entry.substr(0, equal), http_is_ows)) || + (equal != std::string_view::npos && + !http_is_baggage_value(trim(entry.substr(equal + 1), http_is_ows)))) { + return false; + } + + if (semicolon == std::string_view::npos) { + break; + } + + pending.remove_prefix(semicolon + 1); + } + + auto rest{properties}; + while (true) { + const auto semicolon{rest.find(';')}; + const auto entry{trim(rest.substr(0, semicolon), http_is_ows)}; + const auto equal{entry.find('=')}; + callback(trim(entry.substr(0, equal), http_is_ows), + equal == std::string_view::npos + ? std::optional{} + : trim(entry.substr(equal + 1), http_is_ows)); + if (semicolon == std::string_view::npos) { + return true; + } + + rest.remove_prefix(semicolon + 1); + } +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_error.h b/vendor/core/src/core/http/include/sourcemeta/core/http_error.h index bfd64d03a..65344f135 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http_error.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_error.h @@ -8,6 +8,7 @@ #include #include +#include // std::chrono::milliseconds #include // std::uint16_t #include // std::runtime_error #include // std::string @@ -59,6 +60,39 @@ class SOURCEMETA_CORE_HTTP_EXPORT HTTPError : public std::runtime_error { std::string url_; }; +/// @ingroup http +/// An error for a request that did not complete within its configured +/// timeout. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const sourcemeta::core::HTTPTimeoutError error{ +/// sourcemeta::core::HTTPMethod::GET, +/// "https://example.com", std::chrono::seconds{30}}; +/// assert(error.timeout() == std::chrono::seconds{30}); +/// ``` +class SOURCEMETA_CORE_HTTP_EXPORT HTTPTimeoutError : public HTTPError { +public: + /// Construct an error from the request method, URL, and the total timeout + /// that was configured for the request + HTTPTimeoutError(const HTTPMethod method, std::string url, + const std::chrono::milliseconds timeout) + : HTTPError{method, std::move(url), "The HTTP request timed out"}, + timeout_{timeout} {} + + /// Get the total timeout that was configured for the request. A backend + /// does not report which of the configured bounds elapsed, so this is the + /// total timeout even when a narrower connection timeout was the one hit + [[nodiscard]] auto timeout() const noexcept -> std::chrono::milliseconds { + return this->timeout_; + } + +private: + std::chrono::milliseconds timeout_; +}; + /// @ingroup http /// An error for a response with an unsuccessful status code, owning a copy /// of the status data. For example: diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_method.h b/vendor/core/src/core/http/include/sourcemeta/core/http_method.h index f9d5465fe..e5b6ff7a7 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http_method.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_method.h @@ -64,7 +64,11 @@ constexpr auto http_method_string(const HTTPMethod method) noexcept return "PATCH"; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } } // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_syntax.h b/vendor/core/src/core/http/include/sourcemeta/core/http_syntax.h index 135da803a..1d773853d 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http_syntax.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_syntax.h @@ -63,6 +63,31 @@ inline auto http_is_tchar(const char character) noexcept -> bool { } } +/// @ingroup http +/// Whether the character is one a field value may carry. RFC 9110 Section 5.5 +/// admits the visible characters and obsolete text, along with the spaces and +/// horizontal tabs between them, and nothing else: +/// "field-vchar = VCHAR / obs-text" +/// Every other control character is excluded, and a recipient of one of those +/// "MUST either reject the message or replace each of those characters with SP +/// before further processing". For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_is_field_octet('a')); +/// assert(sourcemeta::core::http_is_field_octet(' ')); +/// assert(sourcemeta::core::http_is_field_octet('\t')); +/// assert(!sourcemeta::core::http_is_field_octet('\n')); +/// ``` +/// +/// @see https://www.rfc-editor.org/rfc/rfc9110#section-5.5 +inline auto http_is_field_octet(const char character) noexcept -> bool { + const auto byte{static_cast(character)}; + return byte == 0x09 || (byte >= 0x20 && byte != 0x7F); +} + /// @ingroup http /// Whether a character may appear in the alphabet of a token68 (RFC 7235 /// Section 2.1) or b64token (RFC 6750 Section 2.1) credential, before its @@ -157,12 +182,11 @@ inline auto http_is_token(const std::string_view value) noexcept -> bool { /// ``` inline auto http_encode_quoted_string(const std::string_view value, std::string &sink) -> bool { + // RFC 9110 Section 5.6.4: quoted text and a quoted-pair admit what a field + // value admits, so a control character beyond the horizontal tab makes the + // value unencodable, carriage return and line feed among them for (const auto character : value) { - const auto byte{static_cast(character)}; - // RFC 9110 Section 5.6.4: qdtext and quoted-pair admit HTAB, SP through - // VCHAR, and obs-text, so every other control character (CR and LF among - // them) makes the value unencodable - if (byte != 0x09 && (byte < 0x20 || byte == 0x7F)) { + if (!http_is_field_octet(character)) { return false; } } diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_tracecontext.h b/vendor/core/src/core/http/include/sourcemeta/core/http_tracecontext.h new file mode 100644 index 000000000..705a1a8da --- /dev/null +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_tracecontext.h @@ -0,0 +1,298 @@ +#ifndef SOURCEMETA_CORE_HTTP_TRACECONTEXT_H_ +#define SOURCEMETA_CORE_HTTP_TRACECONTEXT_H_ + +#ifndef SOURCEMETA_CORE_HTTP_EXPORT +#include +#endif + +#include +#include + +#include // std::array +#include // std::invocable +#include // std::uint8_t +#include // std::optional +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup http +/// The fields of a traceparent field value. Every view borrows the input, so +/// the value these came from must outlive them. +/// +/// @see https://www.w3.org/TR/trace-context/#traceparent-header +struct HTTPTraceParent { + /// The revision of the field format this value was written to. Note that a + /// value being forwarded is not sent on under this revision but under "the + /// highest version of the specification known to the implementation", so a + /// higher one read here is reported rather than endorsed. + /// + /// @see https://www.w3.org/TR/trace-context/#versioning-of-traceparent + std::uint8_t version{0}; + /// Identifies the trace this request belongs to, as 32 lowercase hexadecimal + /// characters + std::string_view trace_id; + /// Identifies the operation that made this request, as 16 lowercase + /// hexadecimal characters + std::string_view parent_id; + /// An eight bit field of which one bit is currently defined + std::uint8_t flags{0}; +}; + +/// @ingroup http +/// Whether the sampled bit is set, which records that the caller may have +/// recorded trace data rather than that it did. The remaining bits are +/// reserved and a reader is told to mask rather than compare: "As this is a +/// bit field, you cannot interpret flags by decoding the hex value and looking +/// at the resulting number". For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_traceparent_sampled(0x01)); +/// assert(!sourcemeta::core::http_traceparent_sampled(0x00)); +/// assert(sourcemeta::core::http_traceparent_sampled(0x05)); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#trace-flags +/// @see https://www.w3.org/TR/trace-context/#sampled-flag +constexpr auto http_traceparent_sampled(const std::uint8_t flags) noexcept + -> bool { + return (flags & 0x01U) != 0; +} + +/// @ingroup http +/// Whether the input is a well-formed traceparent field value. The grammar in +/// W3C Trace Context Section 3.2.2 is written for version `00`, and a higher +/// version is admitted under the rules for reading one in Section 3.2.4. +/// +/// Version `00` is exactly 55 characters and admits nothing further. A later +/// version may append, since a reader is told to "check that the 2 characters +/// are either the end of the string or a dash", so a longer value must carry a +/// dash where the flags end. Whatever follows that dash is left +/// uninterpreted, because "Vendors MUST NOT parse or assume anything about +/// unknown fields for this version", but it is still held to the octets a +/// field value may carry, so a carriage return or a line feed there is +/// refused. Version `ff` is invalid, and an identifier of all zeroes is +/// "considered an invalid value". For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_is_traceparent( +/// "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")); +/// assert(!sourcemeta::core::http_is_traceparent( +/// "ff-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")); +/// assert(!sourcemeta::core::http_is_traceparent( +/// "00-00000000000000000000000000000000-b7ad6b7169203331-01")); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#traceparent-header-field-values +SOURCEMETA_CORE_HTTP_EXPORT +auto http_is_traceparent(const std::string_view value) noexcept -> bool; + +/// @ingroup http +/// Parse a traceparent field value into its fields, or nothing when it is not +/// well formed. The identifiers borrow the input. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto parent{sourcemeta::core::http_parse_traceparent( +/// "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")}; +/// assert(parent.has_value()); +/// assert(parent->version == 0); +/// assert(parent->trace_id == "0af7651916cd43dd8448eb211c80319c"); +/// assert(parent->parent_id == "b7ad6b7169203331"); +/// assert(sourcemeta::core::http_traceparent_sampled(parent->flags)); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#traceparent-header-field-values +SOURCEMETA_CORE_HTTP_EXPORT +auto http_parse_traceparent(const std::string_view value) noexcept + -> std::optional; + +/// @ingroup http +/// Generate a traceparent field value that starts a new trace, drawing both +/// identifiers from the random provider of the crypto module. This is what a +/// request that arrived without one needs, and it is also the mutation that +/// regenerates every property, which a service acting as "a front gate into +/// secure networks" performs to shed what it was handed. +/// +/// Note that the identifiers are only unguessable to the degree that provider +/// is, which varies with how the library was built, and that the requirement +/// on a generator is strict: "Random number generators MUST NOT rely on any +/// information that can potentially be user-identifiable". For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto value{sourcemeta::core::http_make_traceparent(true)}; +/// const std::string_view view{value.data(), value.size()}; +/// assert(view.size() == 55); +/// assert(sourcemeta::core::http_is_traceparent(view)); +/// assert(view.starts_with("00-")); +/// assert(view.ends_with("-01")); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#mutating-the-traceparent-field +/// @see https://www.w3.org/TR/trace-context/#randomness-of-trace-id +/// @see https://www.w3.org/TR/trace-context/#privacy-of-traceparent-field +SOURCEMETA_CORE_HTTP_EXPORT +auto http_make_traceparent(const bool sampled) -> std::array; + +/// @ingroup http +/// Generate the traceparent field value to send onward for a request that +/// arrived carrying one. The trace continues, so its identifier is kept, while +/// the operation is this one rather than the caller's, which is the mutation +/// the specification calls "the most typical" and says "should be considered a +/// default". +/// +/// The input is the field value that arrived rather than its parsed fields, so +/// that what is carried over is known to be well formed, and nothing comes +/// back for one that does not parse. +/// +/// The sampling decision is the one being made here rather than the one that +/// arrived, since the flag "reflects the caller's recording behavior" and the +/// caller is now this service. Read the arriving decision off the parsed flags +/// to pass it on unchanged. A value read at a later revision is written back +/// at the revision this library implements, which is the downgrade a reader of +/// one owes, and whatever that value appended is dropped with it. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto value{sourcemeta::core::http_continue_traceparent( +/// "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", true)}; +/// assert(value.has_value()); +/// const std::string_view view{value->data(), value->size()}; +/// assert(sourcemeta::core::http_is_traceparent(view)); +/// assert(view.substr(3, 32) == "0af7651916cd43dd8448eb211c80319c"); +/// assert(view.substr(36, 16) != "b7ad6b7169203331"); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#mutating-the-traceparent-field +/// @see https://www.w3.org/TR/trace-context/#versioning-of-traceparent +SOURCEMETA_CORE_HTTP_EXPORT +auto http_continue_traceparent(const std::string_view value, const bool sampled) + -> std::optional>; + +/// @ingroup http +/// Whether the input is a well-formed tracestate field value (RFC 5234 ABNF in +/// W3C Trace Context Section 3.3.1). +/// +/// The grammar is permissive in ways that surprise: at most 32 members, and +/// "Empty and whitespace-only list members are allowed. Vendors MUST accept +/// empty tracestate headers". Since "Spaces and horizontal tabs surrounding +/// list-members are ignored", a member padded on either side is accepted and +/// so is one made only of padding. Padding at the two outer ends of the whole +/// value is a little more than the list production writes down, and is +/// accepted on that same sentence. A member's value, in contrast, cannot be +/// empty. Note that baggage differs on every one of those points. +/// +/// This rejects a repeated key, which the grammar alone does not forbid, on +/// the rule that "Only one entry per key is allowed because the entry +/// represents that last position in the trace". A reader is offered the +/// alternative of dropping just the repeated entry, but only in a section +/// marked non-normative. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_is_tracestate( +/// "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE")); +/// assert(sourcemeta::core::http_is_tracestate("")); +/// assert(!sourcemeta::core::http_is_tracestate("foo=")); +/// assert(!sourcemeta::core::http_is_tracestate("a=1,a=2")); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#tracestate-header +SOURCEMETA_CORE_HTTP_EXPORT +auto http_is_tracestate(const std::string_view value) noexcept -> bool; + +/// @ingroup http +/// Visit each tracestate member in order as a key and a value, skipping the +/// empty members the grammar allows. The whole value is checked before +/// anything is visited, so a caller never acts on part of a malformed value, +/// and nothing is visited when it does not parse. Every key and value borrows +/// the input, so the value they came from must outlive them. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::string flat; +/// assert(sourcemeta::core::http_parse_tracestate( +/// "rojo=1 , , congo=2", +/// [&flat](const auto key, const auto value) { +/// flat += std::string{key} + "=" + std::string{value} + ";"; +/// })); +/// assert(flat == "rojo=1;congo=2;"); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#list +template + requires std::invocable +inline auto http_parse_tracestate(const std::string_view value, + Callback callback) -> bool { + if (!http_is_tracestate(value)) { + return false; + } + auto rest{value}; + while (true) { + const auto comma{rest.find(',')}; + const auto entry{trim(rest.substr(0, comma), http_is_ows)}; + const auto equal{entry.find('=')}; + if (equal != std::string_view::npos) { + callback(entry.substr(0, equal), entry.substr(equal + 1)); + } + if (comma == std::string_view::npos) { + return true; + } + rest.remove_prefix(comma + 1); + } +} + +/// @ingroup http +/// Collect each tracestate member in order into a container of pairs. Every +/// key and value borrows the input, so the value they came from must outlive +/// the container. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// #include +/// +/// std::vector> members; +/// assert(sourcemeta::core::http_parse_tracestate("rojo=1,congo=2", members)); +/// assert(members.size() == 2); +/// assert(members.front().first == "rojo"); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#list +template + requires requires(Container container, std::string_view entry) { + container.emplace_back(entry, entry); + } +inline auto http_parse_tracestate(const std::string_view value, + Container &members) -> bool { + return http_parse_tracestate( + value, + [&members](const std::string_view key, const std::string_view entry) + -> void { members.emplace_back(key, entry); }); +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/http/is_baggage.cc b/vendor/core/src/core/http/is_baggage.cc new file mode 100644 index 000000000..76ba4314a --- /dev/null +++ b/vendor/core/src/core/http/is_baggage.cc @@ -0,0 +1,84 @@ +#include +#include + +#include // std::size_t +#include // std::string_view + +namespace sourcemeta::core { + +auto http_is_baggage(const std::string_view value) noexcept -> bool { + std::size_t members{0}; + auto rest{value}; + while (true) { + // The list production is one member followed by at most 179 more, so 180 + // is the cap. The separate figures of 64 members and 8192 bytes are the + // point past which a platform "MAY drop list-members" rather than a bound + // on what is well formed, so they are left alone + // https://www.w3.org/TR/baggage/#definition + // https://www.w3.org/TR/baggage/#limits + if (members >= 180) { + return false; + } + + ++members; + const auto comma{rest.find(',')}; + auto member{trim(rest.substr(0, comma), http_is_ows)}; + + // A member carries its own pair and then any number of properties, and + // only the pair must hold an equals sign, since "Additional metadata MAY + // be appended to values in the form of property set" whose entries are + // "keys and/or key-value pairs". The list production offers no alternative + // for an empty member, so one is malformed, where tracestate allows one: + // "Empty and whitespace-only list members are allowed" + // https://www.w3.org/TR/baggage/#property + // https://www.w3.org/TR/baggage/#definition + // https://www.w3.org/TR/trace-context/#tracestate-header-field-values + bool pair{true}; + while (true) { + const auto semicolon{member.find(';')}; + const auto entry{trim(member.substr(0, semicolon), http_is_ows)}; + const auto equal{entry.find('=')}; + if (pair && equal == std::string_view::npos) { + return false; + } + + // "Leading and trailing whitespaces (OWS) are allowed and are not + // considered to be a part of the key", which is itself the token this + // module already knows. The list production brackets whitespace around + // each key and value but not around the whole field value, so stripping + // at the two outer ends accepts a little more than the grammar writes + // down. It costs nothing, since "A field value does not include leading + // or trailing whitespace" in the first place + // https://www.w3.org/TR/baggage/#key + // https://www.rfc-editor.org/rfc/rfc9110#section-5.5 + if (!http_is_token(trim(entry.substr(0, equal), http_is_ows))) { + return false; + } + + // Only the first equals sign separates, since a value may carry more of + // them: "Parsers MUST NOT assume that the equal sign is only used to + // separate key and value". An absent one is a property given as a bare + // key, which the pair itself is not allowed to be + // https://www.w3.org/TR/baggage/#value + if (equal != std::string_view::npos && + !http_is_baggage_value(trim(entry.substr(equal + 1), http_is_ows))) { + return false; + } + + pair = false; + if (semicolon == std::string_view::npos) { + break; + } + + member.remove_prefix(semicolon + 1); + } + + if (comma == std::string_view::npos) { + return true; + } + + rest.remove_prefix(comma + 1); + } +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/is_baggage_value.cc b/vendor/core/src/core/http/is_baggage_value.cc new file mode 100644 index 000000000..a4925753b --- /dev/null +++ b/vendor/core/src/core/http/is_baggage_value.cc @@ -0,0 +1,47 @@ +#include +#include + +#include // std::size_t +#include // std::string_view + +namespace { + +// The octet production runs in five alternatives, a single character and four +// ranges, which its own comment explains as the US-ASCII characters other than +// the control ones, whitespace, the double quote, the comma, the semicolon and +// the backslash. The four gaps between those alternatives are where the last +// four of those sit +// https://www.w3.org/TR/baggage/#definition +auto is_baggage_octet(const char character) noexcept -> bool { + const auto byte{static_cast(character)}; + return byte >= 0x21 && byte <= 0x7E && byte != '"' && byte != ',' && + byte != ';' && byte != '\\'; +} + +} // namespace + +namespace sourcemeta::core { + +auto http_is_baggage_value(const std::string_view value) noexcept -> bool { + for (std::size_t index{0}; index < value.size(); ++index) { + if (!is_baggage_octet(value[index])) { + return false; + } + + // The percent is an octet like any other as far as the grammar is + // concerned, so this is stricter than the grammar on the strength of the + // prose: "The percent code point (U+0025) MUST be percent-encoded" + // https://www.w3.org/TR/baggage/#value + if (value[index] == '%') { + if (!is_percent_triplet(value, index)) { + return false; + } + + index += 2; + } + } + + return true; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/is_traceparent.cc b/vendor/core/src/core/http/is_traceparent.cc new file mode 100644 index 000000000..a93a7c071 --- /dev/null +++ b/vendor/core/src/core/http/is_traceparent.cc @@ -0,0 +1,72 @@ +#include +#include + +#include // std::all_of +#include // std::size_t +#include // std::string_view + +namespace sourcemeta::core { + +auto http_is_traceparent(const std::string_view value) noexcept -> bool { + // The size test comes first so that the indexing below stays in range, as a + // view offers no character past its end, and the dashes sit where the four + // fields meet: "If the size of the header is shorter than 55 characters, the + // vendor should not parse the header and should restart the trace". A value + // carrying fields of a later version must put a dash where the flags end: + // "Vendors MUST check that the 2 characters are either the end of the string + // or a dash". The lowest version is the whole value, so it admits nothing + // beyond the flags + // https://www.w3.org/TR/trace-context/#versioning-of-traceparent + // + // What a later version appends past that dash is left uninterpreted, since + // the rules for reading one enumerate three checks and no more, and "Vendors + // MUST NOT parse or assume anything about unknown fields for this version". + // Reading nothing into those bytes is not the same as taking any byte at + // all, though: whatever they turn out to mean, they travel in a field value, + // and the octets one may hold are settled below + if (value.size() < 55 || value[2] != '-' || value[35] != '-' || + value[52] != '-' || (value.starts_with("00") && value.size() != 55) || + (value.size() > 55 && value[55] != '-')) { + return false; + } + + // "Version ff is invalid" + // https://www.w3.org/TR/trace-context/#version + if (value.starts_with("ff")) { + return false; + } + + // Every field is lowercase hexadecimal, including the version prefix, whose + // failure to parse is what tells a reader to start a new trace: + // "it's not 2 hex characters followed by a dash". The flags are read as a + // bit field rather than compared, so no value of them is rejected here + // https://www.w3.org/TR/trace-context/#traceparent-header-field-values + for (std::size_t index{0}; index < 55; ++index) { + if (index != 2 && index != 35 && index != 52 && + (!is_hex_digit(value[index]) || !is_lowercase(value[index]))) { + return false; + } + } + + // Holding the appended bytes to the octets of a field value says nothing + // about what they mean, so it leaves them as opaque as the rule above + // demands. "Field values containing CR, LF, or NUL characters are invalid + // and dangerous", and a recipient of one "MUST either reject the message or + // replace each of those characters with SP before further processing", of + // which refusing the value is the first + // https://www.rfc-editor.org/rfc/rfc9110#section-5.5 + const auto appended{value.substr(55)}; + if (!std::all_of(appended.begin(), appended.end(), http_is_field_octet)) { + return false; + } + + // "If the trace-id value is invalid (for example if it contains non-allowed + // characters or all zeros), vendors MUST ignore the traceparent", and the + // same holds of the eight byte identifier + // https://www.w3.org/TR/trace-context/#trace-id + // https://www.w3.org/TR/trace-context/#parent-id + return value.substr(3, 32).find_first_not_of('0') != std::string_view::npos && + value.substr(36, 16).find_first_not_of('0') != std::string_view::npos; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/is_tracestate.cc b/vendor/core/src/core/http/is_tracestate.cc new file mode 100644 index 000000000..51534312d --- /dev/null +++ b/vendor/core/src/core/http/is_tracestate.cc @@ -0,0 +1,146 @@ +#include +#include + +#include // std::all_of, std::find +#include // std::array +#include // std::size_t +#include // std::string_view + +namespace { + +// A key is either a plain identifier or a tenant and a system joined by an at +// sign, and the three differ in how long they may be and in what may start +// them: only a tenant admits a leading digit, where the other two require a +// lowercase letter +// https://www.w3.org/TR/trace-context/#key +auto key_part(const std::string_view identifier, const bool tenant, + const std::size_t maximum) noexcept -> bool { + if (identifier.empty() || identifier.size() > maximum || + !((identifier.front() >= 'a' && identifier.front() <= 'z') || + (tenant && sourcemeta::core::is_digit(identifier.front())))) { + return false; + } + return std::all_of( + identifier.begin() + 1, identifier.end(), [](const char character) { + return (character >= 'a' && character <= 'z') || + sourcemeta::core::is_digit(character) || character == '_' || + character == '-' || character == '*' || character == '/'; + }); +} + +// "There can be a maximum of 32 list-members in a list", which bounds both the +// count and the keys kept to compare against +// https://www.w3.org/TR/trace-context/#tracestate-header-field-values +constexpr std::size_t MAXIMUM_MEMBERS{32}; + +} // namespace + +namespace sourcemeta::core { + +auto http_is_tracestate(const std::string_view value) noexcept -> bool { + // A list holds at most 32 members, so the keys seen so far fit in a fixed + // array that a linear scan searches, which keeps this free of allocation + std::array keys; + std::size_t seen{0}; + std::size_t members{0}; + auto rest{value}; + while (true) { + // The limit counts the empty members too, since each is a list-member of + // its own + if (members >= MAXIMUM_MEMBERS) { + return false; + } + const auto comma{rest.find(',')}; + + // "Spaces and horizontal tabs surrounding list-members are ignored", so + // they are stripped from every member wherever it sits, and what is left + // of one made of nothing else is the empty member the next rule allows. + // The list production brackets only its inner boundaries, so stripping at + // the two outer ends accepts a little more than the grammar writes down. + // That is deliberate and costs nothing, since "A field value does not + // include leading or trailing whitespace" in the first place, and + // stripping can never turn one well-formed member into another, because a + // key cannot begin with a space or a tab and a value cannot end with one + // https://www.w3.org/TR/trace-context/#tracestate-header-field-values + // https://www.w3.org/TR/trace-context/#list + // https://www.rfc-editor.org/rfc/rfc9110#section-5.5 + const auto entry{trim(rest.substr(0, comma), http_is_ows)}; + + // "Empty and whitespace-only list members are allowed. Vendors MUST accept + // empty tracestate headers but SHOULD avoid sending them", which is there + // because "some vendors automatically inject whitespace after a comma + // separator, even in the case of an empty header". Baggage has no such + // alternative in its own member production, so a reader coming from that + // parser should not expect this + // https://www.w3.org/TR/trace-context/#tracestate-header-field-values + // https://www.w3.org/TR/baggage/#definition + if (!entry.empty()) { + const auto equal{entry.find('=')}; + if (equal == std::string_view::npos) { + return false; + } + const auto key{entry.substr(0, equal)}; + const auto content{entry.substr(equal + 1)}; + const auto at_position{key.find('@')}; + + // A simple key is one character plus 255, a tenant 1 plus 240 and a + // system 1 plus 13, which is where the three bounds come from + // https://www.w3.org/TR/trace-context/#key + const bool valid_key{ + at_position == std::string_view::npos + ? key_part(key, false, 256) + : key_part(key.substr(0, at_position), true, 241) && + key_part(key.substr(at_position + 1), false, 14)}; + if (!valid_key) { + return false; + } + + // "Only one entry per key is allowed because the entry represents that + // last position in the trace", which the grammar itself cannot say. The + // obligation that follows it is addressed to a producer and is not in + // capitals, so it is not a requirement in the sense this document + // reserves for those: "vendors must overwrite their entry upon reentry + // to their tracing system". A reader is offered the alternative of + // dropping the repeated entry instead of the whole value, which this + // declines so that a caller is told rather than quietly corrected, and + // which is only offered in a section marked non-normative. Baggage is + // the opposite, where "Uniqueness of keys between multiple list-members + // in a baggage-string is not guaranteed" + // https://www.w3.org/TR/trace-context/#combined-header-value + // https://www.w3.org/TR/trace-context/#a-traceparent-is-received + // https://www.w3.org/TR/baggage/#baggage-string + if (std::find(keys.begin(), keys.begin() + seen, key) != + keys.begin() + seen) { + return false; + } + keys[seen] = key; + ++seen; + + // "The value is an opaque string containing up to 256 printable ASCII + // [RFC0020] characters (i.e., the range 0x20 to 0x7E) except comma (,) + // and (=)", and it ends in a mandatory non-blank character, which + // stripping the surrounding whitespace has already settled. Baggage + // instead counts zero or more of its own octets, so it permits an empty + // value + // https://www.w3.org/TR/trace-context/#value + // https://www.w3.org/TR/baggage/#definition + if (content.empty() || content.size() > 256 || + !std::all_of(content.begin(), content.end(), + [](const char character) { + const auto byte{static_cast(character)}; + return byte >= 0x20 && byte <= 0x7E && + character != ',' && character != '='; + })) { + return false; + } + } + + ++members; + if (comma == std::string_view::npos) { + return true; + } + rest.remove_prefix(comma + 1); + } +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/make_traceparent.cc b/vendor/core/src/core/http/make_traceparent.cc new file mode 100644 index 000000000..49125413f --- /dev/null +++ b/vendor/core/src/core/http/make_traceparent.cc @@ -0,0 +1,37 @@ +#include +#include + +#include // std::array +#include // std::uint8_t +#include // std::span + +#include "entropy.h" + +namespace sourcemeta::core { + +auto http_make_traceparent(const bool sampled) -> std::array { + // Sixteen bytes identify the trace and eight the operation within it, and + // neither may be all zeroes, so the two are drawn separately + std::array trace{}; + std::array parent{}; + http_draw_identifier(trace); + http_draw_identifier(parent); + + std::array result{}; + result[0] = '0'; + result[1] = '0'; + result[2] = '-'; + bytes_to_hex(trace, std::span{result}.subspan(3, 32)); + result[35] = '-'; + bytes_to_hex(parent, std::span{result}.subspan(36, 16)); + result[52] = '-'; + + // Only the lowest bit carries a meaning, and the rest are left clear because + // "Vendors MUST set those to zero" + // https://www.w3.org/TR/trace-context/#other-flags + result[53] = '0'; + result[54] = sampled ? '1' : '0'; + return result; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/match_accept.cc b/vendor/core/src/core/http/match_accept.cc index e3e4d1364..fc8394527 100644 --- a/vendor/core/src/core/http/match_accept.cc +++ b/vendor/core/src/core/http/match_accept.cc @@ -22,9 +22,6 @@ auto http_match_accept(const std::string_view accept_header, std::string_view best{}; float best_quality{0.0F}; std::uint8_t best_specificity{0}; - std::size_t best_order{candidates.size()}; - - std::size_t order{0}; for (const auto candidate : candidates) { assert(!candidate.empty()); // RFC 9110 §12.5.1: a candidate may itself carry media-type parameters @@ -55,18 +52,16 @@ auto http_match_accept(const std::string_view accept_header, candidate_specificity = specificity; } }); + // A candidate only displaces one already held by being better, so the + // first of several equally good ones is the one kept if (candidate_quality > 0.0F && (candidate_quality > best_quality || (candidate_quality == best_quality && - candidate_specificity > best_specificity) || - (candidate_quality == best_quality && - candidate_specificity == best_specificity && order < best_order))) { + candidate_specificity > best_specificity))) { best = candidate; best_quality = candidate_quality; best_specificity = candidate_specificity; - best_order = order; } - ++order; } return best; } diff --git a/vendor/core/src/core/http/match_accept_language.cc b/vendor/core/src/core/http/match_accept_language.cc index d3050f7e5..96db1b817 100644 --- a/vendor/core/src/core/http/match_accept_language.cc +++ b/vendor/core/src/core/http/match_accept_language.cc @@ -45,9 +45,6 @@ auto http_match_accept_language( std::string_view best{}; float best_quality{0.0F}; std::size_t best_specificity{0}; - std::size_t best_order{candidates.size()}; - - std::size_t order{0}; for (const auto candidate : candidates) { assert(!candidate.empty()); assert(candidate.find_first_of(" \t,;*/") == std::string_view::npos); @@ -73,18 +70,16 @@ auto http_match_accept_language( candidate_specificity = specificity; } }); + // A candidate only displaces one already held by being better, so the + // first of several equally good ones is the one kept if (candidate_quality > 0.0F && (candidate_quality > best_quality || (candidate_quality == best_quality && - candidate_specificity > best_specificity) || - (candidate_quality == best_quality && - candidate_specificity == best_specificity && order < best_order))) { + candidate_specificity > best_specificity))) { best = candidate; best_quality = candidate_quality; best_specificity = candidate_specificity; - best_order = order; } - ++order; } return best; } diff --git a/vendor/core/src/core/http/parse_media_type.cc b/vendor/core/src/core/http/parse_media_type.cc index d9033c8f9..d7a6ba40d 100644 --- a/vendor/core/src/core/http/parse_media_type.cc +++ b/vendor/core/src/core/http/parse_media_type.cc @@ -2,6 +2,7 @@ #include "helpers.h" +#include // assert #include // std::size_t #include // std::optional, std::nullopt #include // std::string @@ -24,11 +25,14 @@ auto http_is_parameters(const std::string_view parameters) -> bool { std::string_view value; std::size_t position{0}; while (position < span.size()) { - while (position < span.size() && http_is_ows(span[position])) { + // The tail was trimmed with this very predicate, so a run of optional + // whitespace always stops before the end of the span and a byte follows it + while (http_is_ows(span[position])) { position += 1; + assert(position < span.size()); } - if (position >= span.size() || span[position] != ';') { + if (span[position] != ';') { return false; } diff --git a/vendor/core/src/core/http/parse_traceparent.cc b/vendor/core/src/core/http/parse_traceparent.cc new file mode 100644 index 000000000..c94d19dfb --- /dev/null +++ b/vendor/core/src/core/http/parse_traceparent.cc @@ -0,0 +1,39 @@ +#include +#include + +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::string_view + +namespace { + +auto byte_at(const std::string_view value, + const std::string_view::size_type position) noexcept + -> std::uint8_t { + return static_cast( + (sourcemeta::core::hex_digit_value(value[position]) << 4) | + sourcemeta::core::hex_digit_value(value[position + 1])); +} + +} // namespace + +namespace sourcemeta::core { + +auto http_parse_traceparent(const std::string_view value) noexcept + -> std::optional { + if (!http_is_traceparent(value)) { + return std::nullopt; + } + + // The fields sit at fixed offsets, since a later version keeps the first + // four where version 00 puts them and appends beyond the flags, which a + // reader is told to ignore: "Vendors MUST NOT parse or assume anything about + // unknown fields for this version" + // https://www.w3.org/TR/trace-context/#versioning-of-traceparent + return HTTPTraceParent{.version = byte_at(value, 0), + .trace_id = value.substr(3, 32), + .parent_id = value.substr(36, 16), + .flags = byte_at(value, 53)}; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/scan_quoted_string.cc b/vendor/core/src/core/http/scan_quoted_string.cc index c0da05909..a75c1d42b 100644 --- a/vendor/core/src/core/http/scan_quoted_string.cc +++ b/vendor/core/src/core/http/scan_quoted_string.cc @@ -5,19 +5,6 @@ #include // std::string #include // std::string_view -namespace { - -// RFC 9110 Section 5.6.4: qdtext and the escaped octet of a quoted-pair both -// admit HTAB, SP, the visible characters, and obs-text, so every other control -// character is rejected. The double quote and the backslash are handled by the -// caller before this check -auto http_is_quoted_content(const char character) noexcept -> bool { - const auto byte{static_cast(character)}; - return byte == 0x09 || (byte >= 0x20 && byte != 0x7f); -} - -} // namespace - namespace sourcemeta::core { auto http_scan_quoted_string(const std::string_view input, @@ -28,14 +15,16 @@ auto http_scan_quoted_string(const std::string_view input, return std::nullopt; } + // RFC 9110 Section 5.6.4: quoted text and the escaped octet of a quoted-pair + // both admit what a field value admits, less the double quote and the + // backslash, and those two are what this scan is tracking anyway const std::size_t content_start{position + 1}; std::size_t scan{content_start}; bool escaped{false}; while (scan < input.size()) { const char character{input[scan]}; if (character == '\\') { - if (scan + 1 >= input.size() || - !http_is_quoted_content(input[scan + 1])) { + if (scan + 1 >= input.size() || !http_is_field_octet(input[scan + 1])) { return std::nullopt; } @@ -67,7 +56,7 @@ auto http_scan_quoted_string(const std::string_view input, return scan + 1; } - if (!http_is_quoted_content(character)) { + if (!http_is_field_octet(character)) { return std::nullopt; } diff --git a/vendor/core/src/core/http/serialize_cache_control.cc b/vendor/core/src/core/http/serialize_cache_control.cc index 65687ff32..adb148e5a 100644 --- a/vendor/core/src/core/http/serialize_cache_control.cc +++ b/vendor/core/src/core/http/serialize_cache_control.cc @@ -68,7 +68,11 @@ auto visibility_directive( return "private"; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } // RFC 9110 §5.1: field-name = token, so a name outside that set could not be diff --git a/vendor/core/src/core/http/serialize_cookie.cc b/vendor/core/src/core/http/serialize_cookie.cc index e0d3e06c6..743f1fdd6 100644 --- a/vendor/core/src/core/http/serialize_cookie.cc +++ b/vendor/core/src/core/http/serialize_cookie.cc @@ -81,7 +81,11 @@ auto same_site_token(const sourcemeta::core::HTTPCookieSameSite value) noexcept return COOKIE_SAME_SITE_NONE; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto required_size(const sourcemeta::core::HTTPCookie &cookie, diff --git a/vendor/core/src/core/ip/ipv6.cc b/vendor/core/src/core/ip/ipv6.cc index d16064804..ac0863a5c 100644 --- a/vendor/core/src/core/ip/ipv6.cc +++ b/vendor/core/src/core/ip/ipv6.cc @@ -4,6 +4,7 @@ #include "ip_helpers.h" #include // std::array +#include // assert #include // std::size_t #include // std::uint8_t, std::uint16_t #include // std::optional, std::nullopt @@ -86,9 +87,11 @@ auto is_ipv6(const std::string_view address) -> bool { continue; } position += 1; - if (position >= size) { - return false; - } + // An address whose last character is a colon is turned away above unless + // the compression takes the last two, and the branch over the + // compression is taken before this one, so a colon reached here always + // has a group after it + assert(position < size); } } diff --git a/vendor/core/src/core/jose/jose_jwks_provider.cc b/vendor/core/src/core/jose/jose_jwks_provider.cc index 528d4051e..001e81555 100644 --- a/vendor/core/src/core/jose/jose_jwks_provider.cc +++ b/vendor/core/src/core/jose/jose_jwks_provider.cc @@ -200,7 +200,7 @@ auto JWKSProvider::verify_at( refreshed = this->refetch_for_unknown_kid_locked(now); } - if (refreshed == nullptr || refreshed == snapshot) { + if (refreshed == nullptr) { return error; } diff --git a/vendor/core/src/core/json/construct.h b/vendor/core/src/core/json/construct.h index 731d3485d..e70b1869b 100644 --- a/vendor/core/src/core/json/construct.h +++ b/vendor/core/src/core/json/construct.h @@ -14,15 +14,26 @@ #include // std::size_t #include // std::uint64_t, std::uint32_t, std::uint8_t #include // std::reference_wrapper -#include // std::invalid_argument #include // std::string_view -#include // std::move +#include // std::move, std::unreachable #include // std::vector namespace sourcemeta::core { namespace internal { +// The four hexadecimal digits of a `\u` escape, read from the position given +// and leaving it on whatever follows them +inline auto parse_hex4(const char *&position) -> unsigned long { + unsigned long value{0}; + for (std::size_t index = 0; index < 4; index++) { + const auto digit{hex_digit_value(*position++)}; + value = (value << 4) | static_cast(digit < 0 ? 0 : digit); + } + + return value; +} + inline auto unescape_string(const char *data, const std::uint32_t length, const bool has_escape) -> JSON::String { JSON::String result; @@ -80,16 +91,6 @@ inline auto unescape_string(const char *data, const std::uint32_t length, result.push_back('\t'); break; case 'u': { - auto parse_hex4 = [](const char *&position) -> unsigned long { - unsigned long value{0}; - for (std::size_t index = 0; index < 4; index++) { - const auto digit{hex_digit_value(*position++)}; - value = (value << 4) | - static_cast(digit < 0 ? 0 : digit); - } - return value; - }; - auto code_point{parse_hex4(cursor)}; if (code_point >= 0xD800 && code_point <= 0xDBFF) { assert(cursor + 6 <= string_end); @@ -102,51 +103,51 @@ inline auto unescape_string(const char *data, const std::uint32_t length, result); break; } + // The scan accepted the escape, so what stands after the reverse solidus + // is one of the characters the grammar names default: - break; + std::unreachable(); } } return result; } +// A number literal that the scanner accepts can still name a value outside the +// range this module represents, and where it does the failure belongs at the +// literal rather than at the start of the document +inline auto construct_decimal(const std::string_view value, + const std::uint64_t line, + const std::uint64_t column) -> JSON { + try { + return JSON{Decimal{value}}; + } catch (const DecimalParseError &) { + throw JSONParseError(line, column); + } +} + inline auto construct_number(const char *data, const std::uint32_t length, const std::uint8_t flags, - const std::uint32_t significant_digits) -> JSON { + const std::uint32_t significant_digits, + const std::uint64_t line, + const std::uint64_t column) -> JSON { if ((flags & TAPE_FLAG_NUMBER_EXPONENT) != 0) { - try { - return JSON{Decimal{std::string_view{data, length}}}; - } catch (const DecimalParseError &) { - throw JSONParseError(1, 1); - } catch (const std::invalid_argument &) { - throw JSONParseError(1, 1); - } + return construct_decimal(std::string_view{data, length}, line, column); } if ((flags & TAPE_FLAG_NUMBER_DOT) != 0) { constexpr std::uint32_t MAX_SAFE_SIGNIFICANT_DIGITS{15}; if (significant_digits > MAX_SAFE_SIGNIFICANT_DIGITS) { - try { - return JSON{Decimal{std::string_view{data, length}}}; - } catch (const DecimalParseError &) { - throw JSONParseError(1, 1); - } catch (const std::invalid_argument &) { - throw JSONParseError(1, 1); - } + return construct_decimal(std::string_view{data, length}, line, column); } const std::string_view value{data, length}; - const auto double_result{sourcemeta::core::to_double(value)}; + const auto double_result{sourcemeta::core::to_double_exact(value)}; if (double_result.has_value()) { return JSON{double_result.value()}; } - try { - return JSON{Decimal{value}}; - } catch (const DecimalParseError &) { - throw JSONParseError(1, 1); - } catch (const std::invalid_argument &) { - throw JSONParseError(1, 1); - } + + return construct_decimal(value, line, column); } auto digit_length = length; @@ -154,62 +155,71 @@ inline auto construct_number(const char *data, const std::uint32_t length, digit_length--; } + // A literal of digits alone stands for an exponent of zero, which is inside + // the range the storage holds, so reading one back cannot fail the way a + // literal that carries an exponent of its own can if (digit_length <= 19) { const std::string_view value{data, length}; const auto int_result{sourcemeta::core::to_int64_t(value)}; if (int_result.has_value()) { return JSON{int_result.value()}; } - try { - return JSON{Decimal{value}}; - } catch (const DecimalParseError &) { - throw JSONParseError(1, 1); - } catch (const std::invalid_argument &) { - throw JSONParseError(1, 1); - } - } - try { - return JSON{Decimal{std::string_view{data, length}}}; - } catch (const DecimalParseError &) { - throw JSONParseError(1, 1); - } catch (const std::invalid_argument &) { - throw JSONParseError(1, 1); + return JSON{Decimal{value}}; } + + return JSON{Decimal{std::string_view{data, length}}}; } -inline auto post_column_for(const TapeEntry &entry) -> std::uint64_t { +// Every value that is not a container is read the same way wherever it stands, +// and the kind a number literal names is settled by reading it rather than by +// what the scan recorded +inline auto construct_leaf(const char *buffer, const TapeEntry &entry) -> JSON { switch (entry.type) { case TapeType::True: - return entry.column + 3; + return JSON{true}; case TapeType::False: - return entry.column + 4; + return JSON{false}; case TapeType::Null: - return entry.column + 3; + return JSON{nullptr}; case TapeType::String: - case TapeType::Key: - return entry.column + entry.length + 1; - case TapeType::Number: - return entry.column + entry.length - 1; + return JSON{ + unescape_string(buffer + entry.offset, entry.length, + (entry.flags & TAPE_FLAG_STRING_ESCAPE) != 0)}; + // A container is read by the walk itself rather than here, so what is left + // for this to answer for is a number default: - return entry.column; + assert(entry.type == TapeType::Number); + return construct_number(buffer + entry.offset, entry.length, entry.flags, + entry.count, entry.line, entry.column); } } +inline auto post_column_for(const TapeEntry &entry) -> std::uint64_t { + // A string carries the length of what stands between its quotation marks, so + // the mark closing it stands one past that, while every other value carries + // the length of the whole literal and so ends on its last character + if (entry.type == TapeType::String || entry.type == TapeType::Key) { + return entry.column + entry.length + 1; + } + + return entry.column + entry.length - 1; +} + } // namespace internal // NOLINTBEGIN(cppcoreguidelines-avoid-goto,bugprone-use-after-move) #define CALLBACK_PRE(value_type, entry_ref, context, index, property) \ if (callback) { \ - callback(JSON::ParsePhase::Pre, JSON::Type::value_type, (entry_ref).line, \ + callback(JSON::ParsePhase::Pre, value_type, (entry_ref).line, \ (entry_ref).column, context, index, property); \ } #define CALLBACK_POST(value_type, post_line, post_column) \ if (callback) { \ - callback(JSON::ParsePhase::Post, JSON::Type::value_type, post_line, \ - post_column, JSON::ParseContext::Root, 0, empty_property); \ + callback(JSON::ParsePhase::Post, value_type, post_line, post_column, \ + JSON::ParseContext::Root, 0, empty_property); \ } inline auto construct_json(const char *buffer, @@ -229,64 +239,33 @@ inline auto construct_json(const char *buffer, std::size_t tape_index{0}; static const JSON::String empty_property; - if (tape.empty()) { - throw JSONParseError(1, 1); - } + assert(!tape.empty()); const auto &entry{tape[tape_index]}; - switch (entry.type) { - case TapeType::True: - CALLBACK_PRE(Boolean, entry, JSON::ParseContext::Root, 0, empty_property); - CALLBACK_POST(Boolean, entry.line, internal::post_column_for(entry)); - output = JSON{true}; - return; - case TapeType::False: - CALLBACK_PRE(Boolean, entry, JSON::ParseContext::Root, 0, empty_property); - CALLBACK_POST(Boolean, entry.line, internal::post_column_for(entry)); - output = JSON{false}; - return; - case TapeType::Null: - CALLBACK_PRE(Null, entry, JSON::ParseContext::Root, 0, empty_property); - CALLBACK_POST(Null, entry.line, internal::post_column_for(entry)); - output = JSON{nullptr}; - return; - case TapeType::String: { - CALLBACK_PRE(String, entry, JSON::ParseContext::Root, 0, empty_property); - auto value{Result{internal::unescape_string( - buffer + entry.offset, entry.length, - (entry.flags & TAPE_FLAG_STRING_ESCAPE) != 0)}}; - CALLBACK_POST(String, entry.line, internal::post_column_for(entry)); - output = std::move(value); - return; - } - case TapeType::Number: { - auto value = internal::construct_number( - buffer + entry.offset, entry.length, entry.flags, entry.count); - if (value.is_integer()) { - CALLBACK_PRE(Integer, entry, JSON::ParseContext::Root, 0, - empty_property); - CALLBACK_POST(Integer, entry.line, internal::post_column_for(entry)); - } else if (value.is_decimal()) { - CALLBACK_PRE(Decimal, entry, JSON::ParseContext::Root, 0, - empty_property); - CALLBACK_POST(Decimal, entry.line, internal::post_column_for(entry)); - } else { - CALLBACK_PRE(Real, entry, JSON::ParseContext::Root, 0, empty_property); - CALLBACK_POST(Real, entry.line, internal::post_column_for(entry)); - } - output = std::move(value); - return; - } - case TapeType::ArrayStart: - CALLBACK_PRE(Array, entry, JSON::ParseContext::Root, 0, empty_property); - goto do_construct_array; - case TapeType::ObjectStart: - CALLBACK_PRE(Object, entry, JSON::ParseContext::Root, 0, empty_property); - goto do_construct_object; - default: - throw JSONParseError(1, 1); + if (entry.type == TapeType::ArrayStart) { + CALLBACK_PRE(JSON::Type::Array, entry, JSON::ParseContext::Root, 0, + empty_property); + goto do_construct_array; } + if (entry.type == TapeType::ObjectStart) { + CALLBACK_PRE(JSON::Type::Object, entry, JSON::ParseContext::Root, 0, + empty_property); + goto do_construct_object; + } + + output = internal::construct_leaf(buffer, entry); + if (callback) { + const auto value_type{output.type()}; + callback(JSON::ParsePhase::Pre, value_type, entry.line, entry.column, + JSON::ParseContext::Root, 0, empty_property); + callback(JSON::ParsePhase::Post, value_type, entry.line, + internal::post_column_for(entry), JSON::ParseContext::Root, 0, + empty_property); + } + + return; + /* * Construct an array */ @@ -323,7 +302,7 @@ do_construct_array: { assert(tape[tape_index].type == TapeType::ArrayEnd); const auto &end_entry{tape[tape_index]}; tape_index++; - CALLBACK_POST(Array, end_entry.line, end_entry.column); + CALLBACK_POST(JSON::Type::Array, end_entry.line, end_entry.column); goto do_construct_container_end; } @@ -335,89 +314,35 @@ do_construct_array_item: { assert(levels.back() == Container::Array); const auto &item_entry{tape[tape_index]}; - switch (item_entry.type) { - case TapeType::ArrayStart: - CALLBACK_PRE(Array, item_entry, JSON::ParseContext::Index, - frames.back().get().size(), empty_property); - goto do_construct_array; - case TapeType::ObjectStart: - CALLBACK_PRE(Object, item_entry, JSON::ParseContext::Index, - frames.back().get().size(), empty_property); - goto do_construct_object; - case TapeType::True: - CALLBACK_PRE(Boolean, item_entry, JSON::ParseContext::Index, - frames.back().get().size(), empty_property); - frames.back().get().push_back(JSON{true}); - tape_index++; - CALLBACK_POST(Boolean, item_entry.line, - internal::post_column_for(item_entry)); - goto do_construct_array_item_separator; - case TapeType::False: - CALLBACK_PRE(Boolean, item_entry, JSON::ParseContext::Index, - frames.back().get().size(), empty_property); - frames.back().get().push_back(JSON{false}); - tape_index++; - CALLBACK_POST(Boolean, item_entry.line, - internal::post_column_for(item_entry)); - goto do_construct_array_item_separator; - case TapeType::Null: - CALLBACK_PRE(Null, item_entry, JSON::ParseContext::Index, - frames.back().get().size(), empty_property); - frames.back().get().push_back(JSON{nullptr}); - tape_index++; - CALLBACK_POST(Null, item_entry.line, - internal::post_column_for(item_entry)); - goto do_construct_array_item_separator; - case TapeType::String: - CALLBACK_PRE(String, item_entry, JSON::ParseContext::Index, - frames.back().get().size(), empty_property); - frames.back().get().push_back(Result{internal::unescape_string( - buffer + item_entry.offset, item_entry.length, - (item_entry.flags & TAPE_FLAG_STRING_ESCAPE) != 0)}); - tape_index++; - CALLBACK_POST(String, item_entry.line, - internal::post_column_for(item_entry)); - goto do_construct_array_item_separator; - case TapeType::Number: { - const auto current_index{frames.back().get().size()}; - auto value = internal::construct_number( - buffer + item_entry.offset, item_entry.length, item_entry.flags, - item_entry.count); - if (value.is_integer()) { - CALLBACK_PRE(Integer, item_entry, JSON::ParseContext::Index, - current_index, empty_property); - } else if (value.is_decimal()) { - CALLBACK_PRE(Decimal, item_entry, JSON::ParseContext::Index, - current_index, empty_property); - } else { - CALLBACK_PRE(Real, item_entry, JSON::ParseContext::Index, current_index, - empty_property); - } - const auto value_type{value.type()}; - frames.back().get().push_back(std::move(value)); - tape_index++; - if (value_type == JSON::Type::Integer) { - CALLBACK_POST(Integer, item_entry.line, - internal::post_column_for(item_entry)); - } else if (value_type == JSON::Type::Decimal) { - CALLBACK_POST(Decimal, item_entry.line, - internal::post_column_for(item_entry)); - } else { - CALLBACK_POST(Real, item_entry.line, - internal::post_column_for(item_entry)); - } - goto do_construct_array_item_separator; - } - default: - throw JSONParseError(1, 1); + if (item_entry.type == TapeType::ArrayStart) { + CALLBACK_PRE(JSON::Type::Array, item_entry, JSON::ParseContext::Index, + frames.back().get().size(), empty_property); + goto do_construct_array; } + + if (item_entry.type == TapeType::ObjectStart) { + CALLBACK_PRE(JSON::Type::Object, item_entry, JSON::ParseContext::Index, + frames.back().get().size(), empty_property); + goto do_construct_object; + } + + const auto item_index{frames.back().get().size()}; + auto item_value{internal::construct_leaf(buffer, item_entry)}; + const auto item_type{item_value.type()}; + CALLBACK_PRE(item_type, item_entry, JSON::ParseContext::Index, item_index, + empty_property); + frames.back().get().push_back(std::move(item_value)); + tape_index++; + CALLBACK_POST(item_type, item_entry.line, + internal::post_column_for(item_entry)); + goto do_construct_array_item_separator; } do_construct_array_item_separator: if (tape[tape_index].type == TapeType::ArrayEnd) { const auto &end_entry{tape[tape_index]}; tape_index++; - CALLBACK_POST(Array, end_entry.line, end_entry.column); + CALLBACK_POST(JSON::Type::Array, end_entry.line, end_entry.column); goto do_construct_container_end; } @@ -459,7 +384,7 @@ do_construct_object: { assert(tape[tape_index].type == TapeType::ObjectEnd); const auto &end_entry{tape[tape_index]}; tape_index++; - CALLBACK_POST(Object, end_entry.line, end_entry.column); + CALLBACK_POST(JSON::Type::Object, end_entry.line, end_entry.column); goto do_construct_container_end; } @@ -489,108 +414,34 @@ do_construct_object_key: { do_construct_object_value: { const auto &value_entry{tape[tape_index]}; - switch (value_entry.type) { - case TapeType::ArrayStart: - goto do_construct_array; - case TapeType::ObjectStart: - goto do_construct_object; - case TapeType::True: - frames.back().get().assign_assume_new(std::move(key), JSON{true}, - key_hash); - if (callback) { - callback(JSON::ParsePhase::Pre, JSON::Type::Boolean, key_line, - key_column, JSON::ParseContext::Property, 0, - frames.back().get().as_object().back_key()); - } - tape_index++; - CALLBACK_POST(Boolean, value_entry.line, - internal::post_column_for(value_entry)); - goto do_construct_object_property_end; - case TapeType::False: - frames.back().get().assign_assume_new(std::move(key), JSON{false}, - key_hash); - if (callback) { - callback(JSON::ParsePhase::Pre, JSON::Type::Boolean, key_line, - key_column, JSON::ParseContext::Property, 0, - frames.back().get().as_object().back_key()); - } - tape_index++; - CALLBACK_POST(Boolean, value_entry.line, - internal::post_column_for(value_entry)); - goto do_construct_object_property_end; - case TapeType::Null: - frames.back().get().assign_assume_new(std::move(key), JSON{nullptr}, - key_hash); - if (callback) { - callback(JSON::ParsePhase::Pre, JSON::Type::Null, key_line, key_column, - JSON::ParseContext::Property, 0, - frames.back().get().as_object().back_key()); - } - tape_index++; - CALLBACK_POST(Null, value_entry.line, - internal::post_column_for(value_entry)); - goto do_construct_object_property_end; - case TapeType::String: - frames.back().get().assign_assume_new( - std::move(key), - Result{internal::unescape_string( - buffer + value_entry.offset, value_entry.length, - (value_entry.flags & TAPE_FLAG_STRING_ESCAPE) != 0)}, - key_hash); - if (callback) { - callback(JSON::ParsePhase::Pre, JSON::Type::String, key_line, - key_column, JSON::ParseContext::Property, 0, - frames.back().get().as_object().back_key()); - } - tape_index++; - CALLBACK_POST(String, value_entry.line, - internal::post_column_for(value_entry)); - goto do_construct_object_property_end; - case TapeType::Number: { - auto value = internal::construct_number( - buffer + value_entry.offset, value_entry.length, value_entry.flags, - value_entry.count); - const auto value_type{value.type()}; - frames.back().get().assign_assume_new(std::move(key), std::move(value), - key_hash); - if (callback) { - if (value_type == JSON::Type::Integer) { - callback(JSON::ParsePhase::Pre, JSON::Type::Integer, key_line, - key_column, JSON::ParseContext::Property, 0, - frames.back().get().as_object().back_key()); - } else if (value_type == JSON::Type::Decimal) { - callback(JSON::ParsePhase::Pre, JSON::Type::Decimal, key_line, - key_column, JSON::ParseContext::Property, 0, - frames.back().get().as_object().back_key()); - } else { - callback(JSON::ParsePhase::Pre, JSON::Type::Real, key_line, - key_column, JSON::ParseContext::Property, 0, - frames.back().get().as_object().back_key()); - } - } - tape_index++; - if (value_type == JSON::Type::Integer) { - CALLBACK_POST(Integer, value_entry.line, - internal::post_column_for(value_entry)); - } else if (value_type == JSON::Type::Decimal) { - CALLBACK_POST(Decimal, value_entry.line, - internal::post_column_for(value_entry)); - } else { - CALLBACK_POST(Real, value_entry.line, - internal::post_column_for(value_entry)); - } - goto do_construct_object_property_end; - } - default: - throw JSONParseError(1, 1); + if (value_entry.type == TapeType::ArrayStart) { + goto do_construct_array; + } + + if (value_entry.type == TapeType::ObjectStart) { + goto do_construct_object; + } + + auto value{internal::construct_leaf(buffer, value_entry)}; + const auto value_type{value.type()}; + frames.back().get().assign_assume_new(std::move(key), std::move(value), + key_hash); + if (callback) { + callback(JSON::ParsePhase::Pre, value_type, key_line, key_column, + JSON::ParseContext::Property, 0, + frames.back().get().as_object().back_key()); } + tape_index++; + CALLBACK_POST(value_type, value_entry.line, + internal::post_column_for(value_entry)); + goto do_construct_object_property_end; } do_construct_object_property_end: if (tape[tape_index].type == TapeType::ObjectEnd) { const auto &end_entry{tape[tape_index]}; tape_index++; - CALLBACK_POST(Object, end_entry.line, end_entry.column); + CALLBACK_POST(JSON::Type::Object, end_entry.line, end_entry.column); goto do_construct_container_end; } diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_value.h b/vendor/core/src/core/json/include/sourcemeta/core/json_value.h index 9b94f87af..712db24c8 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_value.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_value.h @@ -517,6 +517,66 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { return this->is_integer() || this->is_real() || this->is_decimal(); } + /// Check whether the number this document holds is exactly a value of the + /// IEEE 754-2019 binary64 interchange format, the double precision format of + /// that standard. A number that no binary significand reaches is not one of + /// its values however closely it sits. A number taken in as a native + /// floating point value is one of them already, while a number read from a + /// document is judged on what its digits denote, so every way of writing one + /// number agrees. The result of this method is undefined unless the JSON + /// instance holds a number. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::JSON half{sourcemeta::core::Decimal{"0.5"}}; + /// const sourcemeta::core::JSON tenth{sourcemeta::core::Decimal{"0.1"}}; + /// assert(half.is_double()); + /// assert(!tenth.is_double()); + /// assert(!sourcemeta::core::parse_json("3.14").is_double()); + /// assert(!sourcemeta::core::parse_json("314e-2").is_double()); + /// ``` + [[nodiscard]] SOURCEMETA_FORCEINLINE auto is_double() const noexcept -> bool { + assert(this->is_number()); + if (this->is_real()) { + return true; + } + if (this->is_integer()) { + return is_representable_as(this->to_integer()); + } + return this->to_decimal().is_double(); + } + + /// Check whether the number this document holds is exactly a value of the + /// IEEE 754-2019 binary32 interchange format, the single precision format of + /// that standard. As with the double precision format, a number read from a + /// document is judged on what its digits denote, so every way of writing one + /// number agrees. The result of this method is undefined unless the JSON + /// instance holds a number. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::JSON half{sourcemeta::core::Decimal{"0.5"}}; + /// const sourcemeta::core::JSON pi{3.14}; + /// assert(half.is_float()); + /// assert(!pi.is_float()); + /// assert(!sourcemeta::core::parse_json("3.14").is_float()); + /// assert(!sourcemeta::core::parse_json("314e-2").is_float()); + /// ``` + [[nodiscard]] SOURCEMETA_FORCEINLINE auto is_float() const noexcept -> bool { + assert(this->is_number()); + if (this->is_real()) { + return is_representable_as(this->to_real()); + } + if (this->is_integer()) { + return is_representable_as(this->to_integer()); + } + return this->to_decimal().is_float(); + } + /// Check if the input JSON document is either a positive integer or a /// positive real number. Zero is considered to be positive. For example: /// diff --git a/vendor/core/src/core/json/json.cc b/vendor/core/src/core/json/json.cc index bbb9e2ad1..37f32513d 100644 --- a/vendor/core/src/core/json/json.cc +++ b/vendor/core/src/core/json/json.cc @@ -8,7 +8,6 @@ #include "parser.h" #include "stringify.h" -#include // assert #include // std::uint64_t #include // std::filesystem #include // std::basic_istream @@ -16,16 +15,16 @@ #include // std::optional, std::nullopt #include // std::basic_ostream #include // std::conditional_t -#include // std::cmp_greater, std::move +#include // std::cmp_greater, std::move, std::unreachable #include // std::vector namespace sourcemeta::core { template -static auto internal_parse_json(const char *&cursor, const char *end, - std::uint64_t &line, std::uint64_t &column, - const JSON::ParseCallback &callback, - const bool track_positions, JSON &output) +static auto +internal_parse_json(const char *&cursor, const char *end, std::uint64_t &line, + std::uint64_t &column, const JSON::ParseCallback &callback, + [[maybe_unused]] const bool track_positions, JSON &output) -> std::conditional_t { const char *buffer_start{cursor}; // Tape entries address the input with 32-bit offsets and lengths, so a larger @@ -45,28 +44,42 @@ static auto internal_parse_json(const char *&cursor, const char *end, if constexpr (ShouldThrow) { if (callback || track_positions) { scan_json(cursor, end, buffer_start, line, column, tape); + construct_json(buffer_start, tape, callback, output); } else { - // Re-scan with position tracking on failure for a precise error message + // Re-scan with position tracking on failure for a precise error message. + // Turning what was read into a value fails over a literal that naming it + // alone cannot settle, such as one past the range that can be held, so + // the retry has to cover that second phase as well. Leaving it out of + // the retry reports every such failure at the start of the document. + // + // Counting positions all along would spare the retry, but it would + // charge every document that parses for what only a document that fails + // needs, and this is the path everything else goes through. So a failure + // reads the input a second time and builds a second time, which is twice + // the work on a document that is about to be thrown away. What is + // counted never decides what is accepted, so the second attempt fails in + // the same place as the first and differs only in being able to say + // where that place is try { scan_json(cursor, end, buffer_start, line, column, tape); + construct_json(buffer_start, tape, callback, output); } catch (const JSONParseError &) { cursor = buffer_start; tape.clear(); line = 1; column = 0; scan_json(cursor, end, buffer_start, line, column, tape); + construct_json(buffer_start, tape, callback, output); } } - construct_json(buffer_start, tape, callback, output); } else { // Both the scanning and the construction phases signal failure by throwing, - // so a single boundary around them reports either as no value + // so a single boundary around them reports either as no value. Nothing + // asks the overload that answers this way to count positions or to + // announce what it reads, so the cheaper scan is the only one it needs + assert(!callback && !track_positions); try { - if (callback || track_positions) { - scan_json(cursor, end, buffer_start, line, column, tape); - } else { - scan_json(cursor, end, buffer_start, line, column, tape); - } + scan_json(cursor, end, buffer_start, line, column, tape); construct_json(buffer_start, tape, callback, output); } catch (const JSONParseError &) { return false; @@ -313,11 +326,13 @@ auto operator<<(std::basic_ostream &stream, return stream << "array"; case sourcemeta::core::JSON::Type::Object: return stream << "object"; - default: - // Should never happen, but some compilers are not happy without this - assert(false); - return stream; } + + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) + std::unreachable(); +#endif } } // namespace sourcemeta::core diff --git a/vendor/core/src/core/json/json_value.cc b/vendor/core/src/core/json/json_value.cc index cc037e03c..7670c8a72 100644 --- a/vendor/core/src/core/json/json_value.cc +++ b/vendor/core/src/core/json/json_value.cc @@ -16,7 +16,7 @@ #include // std::invalid_argument #include // std::to_string #include // std::basic_string_view -#include // std::exchange, std::move +#include // std::exchange, std::move, std::unreachable #include // std::vector // Searching a run of hashes for one value tests many lanes at once on any @@ -893,10 +893,13 @@ auto JSON::operator-=(const JSON &substractive) -> JSON & { return 5; } - default: - assert(false); - return 0; } + + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) + std::unreachable(); +#endif } [[nodiscard]] auto JSON::divisible_by(const JSON &divisor) const -> bool { diff --git a/vendor/core/src/core/json/parser.h b/vendor/core/src/core/json/parser.h index 26a3910b7..3a73c67ec 100644 --- a/vendor/core/src/core/json/parser.h +++ b/vendor/core/src/core/json/parser.h @@ -447,6 +447,56 @@ inline auto scan_number(const std::uint64_t line, std::uint64_t &column, return facts; } +// What a container needs remembered while the scan walks its children +struct ContainerFrame { + std::size_t tape_index; + std::uint32_t child_count; +}; + +// A key and a string value differ in nothing but the entry each leaves behind, +// and the three places a string value can stand differ in nothing but where +// the scan goes next, so what all four do with what they read is spelled out +// once here +template +inline auto push_string(const char *&cursor, const char *end, + const char *buffer_start, std::uint64_t &line, + std::uint64_t &column, std::vector &tape) + -> void { + const auto start{static_cast(cursor - buffer_start)}; + const auto value_line{line}; + const auto value_column{column}; + const auto has_escape{scan_string(line, column, cursor, end)}; + tape.push_back( + {.type = Type, + .flags = has_escape ? TAPE_FLAG_STRING_ESCAPE : std::uint8_t{0}, + .offset = start, + .length = static_cast(cursor - buffer_start - start - 1), + .count = 0, + .line = value_line, + .column = value_column}); +} + +// Closing an array and closing an object differ in nothing but the entry each +// leaves behind, as both settle the count of what the container held before +// giving up the frame that was counting +template +inline auto push_container_end(const std::uint64_t line, + const std::uint64_t column, + std::vector &container_stack, + std::vector &tape) -> void { + assert(!container_stack.empty()); + auto &frame{container_stack.back()}; + tape[frame.tape_index].count = frame.child_count; + tape.push_back({.type = Type, + .flags = 0, + .offset = 0, + .length = 0, + .count = 0, + .line = line, + .column = column}); + container_stack.pop_back(); +} + } // namespace internal // NOLINTBEGIN(cppcoreguidelines-avoid-goto) @@ -456,14 +506,9 @@ inline auto scan_json(const char *&cursor, const char *end, const char *buffer_start, std::uint64_t &line, std::uint64_t &column, std::vector &tape) -> void { - struct ContainerFrame { - std::size_t tape_index; - std::uint32_t child_count; - }; - using CharT = JSON::Char; char character = 0; - std::vector container_stack; + std::vector container_stack; container_stack.reserve(32); internal::skip_whitespace(cursor, end, line, column); @@ -484,47 +529,44 @@ inline auto scan_json(const char *&cursor, const char *end, switch (character) { case internal::TOKEN_TRUE: internal::scan_true(line, column, cursor, end); - tape.push_back({.type = TapeType::True, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); + tape.push_back( + {.type = TapeType::True, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_TRUE.size()), + .count = 0, + .line = value_line, + .column = value_column}); return; case internal::TOKEN_FALSE: internal::scan_false(line, column, cursor, end); - tape.push_back({.type = TapeType::False, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); + tape.push_back( + {.type = TapeType::False, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_FALSE.size()), + .count = 0, + .line = value_line, + .column = value_column}); return; case internal::TOKEN_NULL: internal::scan_null(line, column, cursor, end); - tape.push_back({.type = TapeType::Null, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); - return; - case internal::TOKEN_STRING_QUOTE: { - const auto string_start{ - static_cast(cursor - buffer_start)}; - const auto string_has_escape{ - internal::scan_string(line, column, cursor, end)}; - const auto string_length{static_cast( - cursor - buffer_start - string_start - 1)}; tape.push_back( - {TapeType::String, - string_has_escape ? TAPE_FLAG_STRING_ESCAPE : std::uint8_t{0}, - string_start, string_length, 0, value_line, value_column}); + {.type = TapeType::Null, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_NULL.size()), + .count = 0, + .line = value_line, + .column = value_column}); + return; + case internal::TOKEN_STRING_QUOTE: + internal::push_string( + cursor, end, buffer_start, line, column, tape); return; - } case internal::TOKEN_ARRAY_BEGIN: goto do_scan_array; case internal::TOKEN_OBJECT_BEGIN: @@ -569,7 +611,7 @@ do_scan_array: { .count = 0, .line = line, .column = column}); - container_stack.push_back({start_index, 0}); + container_stack.push_back({.tape_index = start_index, .child_count = 0}); internal::skip_whitespace(cursor, end, line, column); if (cursor >= end) [[unlikely]] { @@ -625,47 +667,44 @@ do_scan_array: { goto do_scan_object; case internal::TOKEN_TRUE: internal::scan_true(line, column, cursor, end); - tape.push_back({.type = TapeType::True, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); + tape.push_back( + {.type = TapeType::True, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_TRUE.size()), + .count = 0, + .line = value_line, + .column = value_column}); goto do_scan_array_item_separator; case internal::TOKEN_FALSE: internal::scan_false(line, column, cursor, end); - tape.push_back({.type = TapeType::False, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); + tape.push_back( + {.type = TapeType::False, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_FALSE.size()), + .count = 0, + .line = value_line, + .column = value_column}); goto do_scan_array_item_separator; case internal::TOKEN_NULL: internal::scan_null(line, column, cursor, end); - tape.push_back({.type = TapeType::Null, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); - goto do_scan_array_item_separator; - case internal::TOKEN_STRING_QUOTE: { - const auto string_start{ - static_cast(cursor - buffer_start)}; - const auto string_has_escape{ - internal::scan_string(line, column, cursor, end)}; - const auto string_length{static_cast( - cursor - buffer_start - string_start - 1)}; tape.push_back( - {TapeType::String, - string_has_escape ? TAPE_FLAG_STRING_ESCAPE : std::uint8_t{0}, - string_start, string_length, 0, value_line, value_column}); + {.type = TapeType::Null, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_NULL.size()), + .count = 0, + .line = value_line, + .column = value_column}); + goto do_scan_array_item_separator; + case internal::TOKEN_STRING_QUOTE: + internal::push_string( + cursor, end, buffer_start, line, column, tape); goto do_scan_array_item_separator; - } case internal::TOKEN_NUMBER_MINUS: case internal::TOKEN_NUMBER_ZERO: case internal::TOKEN_NUMBER_ONE: @@ -708,20 +747,10 @@ do_scan_array: { switch (character) { case internal::TOKEN_ARRAY_DELIMITER: goto do_scan_array_item; - case internal::TOKEN_ARRAY_END: { - assert(!container_stack.empty()); - auto &frame{container_stack.back()}; - tape[frame.tape_index].count = frame.child_count; - tape.push_back({.type = TapeType::ArrayEnd, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = line, - .column = column}); - container_stack.pop_back(); + case internal::TOKEN_ARRAY_END: + internal::push_container_end(line, column, + container_stack, tape); goto do_scan_container_end; - } default: [[unlikely]] throw JSONParseError(line, column); } @@ -739,7 +768,7 @@ do_scan_object: { .count = 0, .line = line, .column = column}); - container_stack.push_back({start_index, 0}); + container_stack.push_back({.tape_index = start_index, .child_count = 0}); internal::skip_whitespace(cursor, end, line, column); if (cursor >= end) [[unlikely]] { @@ -785,20 +814,10 @@ do_scan_object: { } character = *cursor++; switch (character) { - case internal::TOKEN_STRING_QUOTE: { - const auto key_start{static_cast(cursor - buffer_start)}; - const auto key_line{line}; - const auto key_column{column}; - const auto key_has_escape{ - internal::scan_string(line, column, cursor, end)}; - const auto key_length{ - static_cast(cursor - buffer_start - key_start - 1)}; - tape.push_back( - {TapeType::Key, - key_has_escape ? TAPE_FLAG_STRING_ESCAPE : std::uint8_t{0}, - key_start, key_length, 0, key_line, key_column}); + case internal::TOKEN_STRING_QUOTE: + internal::push_string( + cursor, end, buffer_start, line, column, tape); goto do_scan_object_separator; - } default: [[unlikely]] throw JSONParseError(line, column); } @@ -845,47 +864,44 @@ do_scan_object: { goto do_scan_object; case internal::TOKEN_TRUE: internal::scan_true(line, column, cursor, end); - tape.push_back({.type = TapeType::True, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); + tape.push_back( + {.type = TapeType::True, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_TRUE.size()), + .count = 0, + .line = value_line, + .column = value_column}); goto do_scan_object_property_end; case internal::TOKEN_FALSE: internal::scan_false(line, column, cursor, end); - tape.push_back({.type = TapeType::False, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); + tape.push_back( + {.type = TapeType::False, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_FALSE.size()), + .count = 0, + .line = value_line, + .column = value_column}); goto do_scan_object_property_end; case internal::TOKEN_NULL: internal::scan_null(line, column, cursor, end); - tape.push_back({.type = TapeType::Null, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); - goto do_scan_object_property_end; - case internal::TOKEN_STRING_QUOTE: { - const auto string_start{ - static_cast(cursor - buffer_start)}; - const auto string_has_escape{ - internal::scan_string(line, column, cursor, end)}; - const auto string_length{static_cast( - cursor - buffer_start - string_start - 1)}; tape.push_back( - {TapeType::String, - string_has_escape ? TAPE_FLAG_STRING_ESCAPE : std::uint8_t{0}, - string_start, string_length, 0, value_line, value_column}); + {.type = TapeType::Null, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_NULL.size()), + .count = 0, + .line = value_line, + .column = value_column}); + goto do_scan_object_property_end; + case internal::TOKEN_STRING_QUOTE: + internal::push_string( + cursor, end, buffer_start, line, column, tape); goto do_scan_object_property_end; - } case internal::TOKEN_NUMBER_MINUS: case internal::TOKEN_NUMBER_ZERO: case internal::TOKEN_NUMBER_ONE: @@ -928,20 +944,10 @@ do_scan_object: { switch (character) { case internal::TOKEN_OBJECT_DELIMITER: goto do_scan_object_key; - case internal::TOKEN_OBJECT_END: { - assert(!container_stack.empty()); - auto &frame{container_stack.back()}; - tape[frame.tape_index].count = frame.child_count; - tape.push_back({.type = TapeType::ObjectEnd, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = line, - .column = column}); - container_stack.pop_back(); + case internal::TOKEN_OBJECT_END: + internal::push_container_end(line, column, + container_stack, tape); goto do_scan_container_end; - } default: [[unlikely]] throw JSONParseError(line, column); } diff --git a/vendor/core/src/core/json/stringify.h b/vendor/core/src/core/json/stringify.h index 97979da8d..06fd14866 100644 --- a/vendor/core/src/core/json/stringify.h +++ b/vendor/core/src/core/json/stringify.h @@ -16,7 +16,7 @@ #include // std::next, std::cbegin, std::cend, std::back_inserter #include // std::basic_ostream #include // std::ostringstream -#include // std::basic_string +#include // std::basic_string, std::string #include // std::vector namespace sourcemeta::core::internal { @@ -296,6 +296,27 @@ auto prettify(const typename JSON::Object &document, stream.put(internal::TOKEN_OBJECT_END); } +// A number the double precision format holds exactly would be read back as a +// double, so it keeps an exponent to be read back as a decimal instead. The +// rest are read back as decimals however they are written, so they take the +// plain form, which states them as they came in. Where that form needs an +// exponent of its own it groups the digits in threes, which states them +// further from how they came in than scientific notation does +inline auto +stringify_decimal(const Decimal &decimal, + std::basic_ostream &stream) + -> void { + if (!decimal.is_double()) { + const auto plain{decimal.to_string()}; + if (plain.find('e') == std::string::npos) { + stream << plain; + return; + } + } + + stream << decimal.to_scientific_string(); +} + template