diff --git a/DEPENDENCIES b/DEPENDENCIES index b47b5fe24..1e8320598 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,5 +1,5 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core d0bf624114232b8397bddad9a0c0e192c7315deb +core https://github.com/sourcemeta/core 9889529cd1d863eb39fc77289581414a183cce19 jsonbinpack https://github.com/sourcemeta/jsonbinpack 147050385dce7086a28ce93e18a4c3baa9719136 -blaze https://github.com/sourcemeta/blaze 1b09fa0898cc986cc501322db2680d3fd6f093bf +blaze https://github.com/sourcemeta/blaze 53b3d89652504c1d61f908c8eb25ee3b46921e0b ctrf https://github.com/ctrf-io/ctrf 93ea827d951390190171d37443bff169cf47c808 diff --git a/src/command_codegen.cc b/src/command_codegen.cc index a48ffeeb0..65379dd56 100644 --- a/src/command_codegen.cc +++ b/src/command_codegen.cc @@ -138,6 +138,17 @@ auto sourcemeta::jsonschema::codegen(const sourcemeta::core::Options &options) throw sourcemeta::core::FileError< sourcemeta::core::SchemaDialectImpossibleError>(schema_path, error); + } catch (const sourcemeta::core::SchemaReferenceError &error) { + const auto position{parsed_schema.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError< + sourcemeta::core::FileError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_path, error.identifier(), error.location(), error.what()); + } + + throw sourcemeta::core::FileError( + schema_path, error.identifier(), error.location(), error.what()); } catch (const sourcemeta::core::SchemaResolutionError &error) { throw sourcemeta::core::FileError( schema_path, error); diff --git a/src/command_encode.cc b/src/command_encode.cc index c47ccb3d6..84f21e515 100644 --- a/src/command_encode.cc +++ b/src/command_encode.cc @@ -75,8 +75,14 @@ auto sourcemeta::jsonschema::encode(const sourcemeta::core::Options &options) std::println(stderr, "compression ratio: {}%", total_size * 100 / original_size); } else { - const auto entry{ - sourcemeta::core::read_yaml_or_json(options.positional().front())}; + auto parsed{read_file(options.positional().front())}; + if (parsed.multidocument) { + throw MultiDocumentInputError{ + "This command does not support input with multiple documents", + options.positional().front()}; + } + + const auto entry{std::move(parsed.document)}; std::ofstream output_stream( sourcemeta::core::weakly_canonical(options.positional().at(1)), std::ios::binary); diff --git a/src/command_upgrade.cc b/src/command_upgrade.cc index ce5078b99..75efb94c4 100644 --- a/src/command_upgrade.cc +++ b/src/command_upgrade.cc @@ -123,7 +123,15 @@ auto sourcemeta::jsonschema::upgrade(const sourcemeta::core::Options &options) sourcemeta::jsonschema::default_id(schema_path, schema_from_stdin), schema_display_path, parsed_schema.positions); - sourcemeta::jsonschema::format_schema(schema, custom_resolver, dialect); + // A boolean schema is the one shape an upgrade hands over without framing it + // first, as there is nothing in it to rewrite, so ordering it is where a + // dialect that could never be determined is first asked for + try { + sourcemeta::jsonschema::format_schema(schema, custom_resolver, dialect); + } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaUnknownBaseDialectError>(schema_display_path); + } sourcemeta::jsonschema::write_schema(schema, std::cout, indentation, parsed_schema.roundtrip); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 70ce6c7bf..9736c80e9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1036,6 +1036,7 @@ add_jsonschema_test(codegen/fail_invalid_target) add_jsonschema_test(codegen/fail_schema_invalid_json) add_jsonschema_test(codegen/fail_schema_invalid_yaml) add_jsonschema_test(codegen/fail_yaml_multidocument) +add_jsonschema_test(codegen/fail_dangling_ref) add_jsonschema_test(codegen/fail_unknown_metaschema) add_jsonschema_test(codegen/fail_default_dialect_config_extension_mismatch) add_jsonschema_test(codegen/fail_unsupported_vocabulary) @@ -1438,6 +1439,7 @@ add_jsonschema_test(upgrade/pass_openapi_3_2_to_openapi_3_1_unchanged) add_jsonschema_test(upgrade/pass_openapi_3_2_to_2020_12_unchanged) add_jsonschema_test(upgrade/pass_2020_12_to_openapi_3_1_promoted_keywords) add_jsonschema_test(upgrade/fail_no_dialect) +add_jsonschema_test(upgrade/fail_boolean_no_dialect) add_jsonschema_test(upgrade/fail_no_schema) add_jsonschema_test(upgrade/fail_stdin_invalid_json) add_jsonschema_test(upgrade/fail_invalid_path) @@ -1608,6 +1610,7 @@ add_jsonschema_test(encode/pass_schema_less_jsonl) add_jsonschema_test(encode/pass_schema_less_jsonl_verbose) add_jsonschema_test(encode/fail_no_document) add_jsonschema_test(encode/fail_no_output) +add_jsonschema_test(encode/fail_yaml_multidocument) # Decode add_jsonschema_test(decode/pass_schema_less) diff --git a/test/codegen/fail_dangling_ref.clitest b/test/codegen/fail_dangling_ref.clitest new file mode 100644 index 000000000..e5442af07 --- /dev/null +++ b/test/codegen/fail_dangling_ref.clitest @@ -0,0 +1,41 @@ +WRITE schema.json UNTIL EOF +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/dangling", + "$ref": "#/$defs/missing" +} +EOF + +// Schema input error +RUN codegen schema.json --target typescript STDIN /dev/null IN . INTO result.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result.txt + +WRITE expected_0.txt UNTIL EOF +2> error: Could not resolve schema reference +2> at identifier https://example.com/dangling#/$defs/missing +2> at line 4 +2> at column 3 +2> at file path [CWD]/schema.json +2> at location "/$ref" +EOF + +COMPARE result.txt AGAINST expected_0.txt + +// Schema input error +RUN codegen schema.json --target typescript --json STDIN /dev/null IN . INTO result_json.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result_json.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "Could not resolve schema reference", +1> "identifier": "https://example.com/dangling#/$defs/missing", +1> "line": 4, +1> "column": 3, +1> "filePath": "[CWD]/schema.json", +1> "location": "/$ref" +1> } +EOF + +COMPARE result_json.txt AGAINST expected_1.txt diff --git a/test/encode/fail_yaml_multidocument.clitest b/test/encode/fail_yaml_multidocument.clitest new file mode 100644 index 000000000..22cd9c8a2 --- /dev/null +++ b/test/encode/fail_yaml_multidocument.clitest @@ -0,0 +1,35 @@ +WRITE document.yaml UNTIL EOF +$schema: https://json-schema.org/draft/2020-12/schema +type: string +--- +$schema: https://json-schema.org/draft/2020-12/schema +type: integer +EOF + +// Not supported +RUN encode document.yaml output.bin STDIN /dev/null IN . INTO result.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result.txt + +WRITE expected_0.txt UNTIL EOF +2> original file size: 139 bytes +2> error: This command does not support input with multiple documents +2> at file path [CWD]/document.yaml +EOF + +COMPARE result.txt AGAINST expected_0.txt + +// Not supported +RUN encode document.yaml output.bin --json STDIN /dev/null IN . INTO result_json.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_json.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "This command does not support input with multiple documents", +1> "filePath": "[CWD]/document.yaml" +1> } +2> original file size: 139 bytes +EOF + +COMPARE result_json.txt AGAINST expected_1.txt diff --git a/test/upgrade/fail_boolean_no_dialect.clitest b/test/upgrade/fail_boolean_no_dialect.clitest new file mode 100644 index 000000000..2ec50bd5d --- /dev/null +++ b/test/upgrade/fail_boolean_no_dialect.clitest @@ -0,0 +1,33 @@ +WRITE schema.json UNTIL EOF +true +EOF + +// Schema input error +RUN upgrade schema.json STDIN /dev/null IN . INTO result.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result.txt + +WRITE expected.txt UNTIL EOF +2> error: Could not determine the base dialect of the schema +2> at file path [CWD]/schema.json +2> +2> Are you sure the input is a valid JSON Schema and its base dialect is known? +2> If the input does not declare the `$schema` keyword, you might want to +2> explicitly declare a default dialect using `--default-dialect/-d` +EOF + +COMPARE result.txt AGAINST expected.txt + +// Schema input error +RUN upgrade schema.json --json STDIN /dev/null IN . INTO result_json.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result_json.txt + +WRITE expected_json.txt UNTIL EOF +1> { +1> "error": "Could not determine the base dialect of the schema", +1> "filePath": "[CWD]/schema.json" +1> } +EOF + +COMPARE result_json.txt AGAINST expected_json.txt diff --git a/vendor/blaze/CMakeLists.txt b/vendor/blaze/CMakeLists.txt index b6723e97e..c562a5fec 100644 --- a/vendor/blaze/CMakeLists.txt +++ b/vendor/blaze/CMakeLists.txt @@ -16,6 +16,7 @@ option(BLAZE_CANONICALIZER "Build the Blaze canonicalizer library" ON) option(BLAZE_CONVERT "Build the Blaze convert library" ON) option(BLAZE_EDITOR "Build the Blaze editor schema compatibility library" ON) option(BLAZE_DEPENDENCIES "Build the Blaze dependencies library" ON) +option(BLAZE_BINPACK "Build the Blaze BinPack serialization library" ON) option(BLAZE_TESTS "Build the Blaze tests" OFF) option(BLAZE_BENCHMARK "Build the Blaze benchmarks" OFF) option(BLAZE_CONTRIB "Build the Blaze contrib programs" OFF) @@ -99,6 +100,10 @@ if(BLAZE_DEPENDENCIES) add_subdirectory(src/dependencies) endif() +if(BLAZE_BINPACK) + add_subdirectory(src/binpack) +endif() + if(BLAZE_CONTRIB) add_subdirectory(contrib) endif() @@ -191,6 +196,10 @@ if(BLAZE_TESTS) add_subdirectory(test/dependencies) endif() + if(BLAZE_BINPACK) + add_subdirectory(test/binpack) + endif() + if(PROJECT_IS_TOP_LEVEL) # Otherwise we need the child project to link # against the sanitizers too. diff --git a/vendor/blaze/DEPENDENCIES b/vendor/blaze/DEPENDENCIES index cb5e4a31c..3feeb9892 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 5181fd3a3ea5f08df23c833a0cf60b08cd7be00f +core https://github.com/sourcemeta/core 9889529cd1d863eb39fc77289581414a183cce19 jsonschema-test-suite https://github.com/json-schema-org/JSON-Schema-Test-Suite 6648e8194c69697b2e1a15fe76a06a480b183a51 diff --git a/vendor/blaze/config.cmake.in b/vendor/blaze/config.cmake.in index 000bfb47f..0141d4c70 100644 --- a/vendor/blaze/config.cmake.in +++ b/vendor/blaze/config.cmake.in @@ -15,6 +15,7 @@ if(NOT BLAZE_COMPONENTS) list(APPEND BLAZE_COMPONENTS convert) list(APPEND BLAZE_COMPONENTS editor) list(APPEND BLAZE_COMPONENTS dependencies) + list(APPEND BLAZE_COMPONENTS binpack) endif() include(CMakeFindDependencyMacro) @@ -22,7 +23,7 @@ include(CMakeFindDependencyMacro) set(BLAZE_CORE_COMPONENTS unicode punycode idna regex uri uritemplate json jsonpointer jsonschema jsonld io yaml crypto email ip dns time css - memory) + memory numeric) # This installation ships its own copy of Core, and CMake never searches the # prefix of the package it is currently resolving, so a consumer that doesn't @@ -74,6 +75,13 @@ foreach(component ${BLAZE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_editor.cmake") elseif(component STREQUAL "dependencies") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_dependencies.cmake") + elseif(component STREQUAL "binpack") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_evaluator.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_compiler.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_output.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_alterschema.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_canonicalizer.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_binpack.cmake") else() message(FATAL_ERROR "Unknown Blaze component: ${component}") endif() diff --git a/vendor/blaze/schemas/canonical-draft4.json b/vendor/blaze/schemas/canonical-draft4.json index 7deb608b1..d397dbfc8 100644 --- a/vendor/blaze/schemas/canonical-draft4.json +++ b/vendor/blaze/schemas/canonical-draft4.json @@ -2,20 +2,24 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "tag:sourcemeta.com,2026:canonical:draft4", "title": "Canonical JSON Schema Draft 4", - "description": "Meta-schema that validates whether a given JSON Schema Draft 4 document is in canonical form", + "description": "Meta-schema that validates whether a given JSON Schema Draft 4 document is in canonical graph form", "examples": [ { - "$ref": "#/definitions/foo" - }, - { - "enum": [ 1, 2, 3 ] - }, - { - "type": "string", - "minLength": 0 + "$schema": "http://json-schema.org/draft-04/schema#", + "allOf": [ + { + "$ref": "#/definitions/0" + } + ], + "definitions": { + "0": { + "type": "string", + "minLength": 0 + } + } } ], - "$ref": "#/$defs/schema", + "$ref": "#/$defs/document", "$defs": { "metadata": { "properties": { @@ -31,25 +35,54 @@ "^x-": true } }, - "core": { + "document": { "x-lint-exclude": "simple_properties_identifiers", + "type": "object", + "required": [ "allOf", "definitions" ], "properties": { "$schema": { "type": "string" }, - "id": { - "type": "string" + "allOf": { + "type": "array", + "maxItems": 1, + "minItems": 1, + "items": { + "x-lint-exclude": "simple_properties_identifiers", + "$ref": "#/$defs/edge", + "properties": { + "$ref": { + "const": "#/definitions/0" + } + } + } }, "definitions": { "type": "object", "minProperties": 1, + "propertyNames": { + "pattern": "^(0|[1-9][0-9]*)$" + }, "additionalProperties": { - "$ref": "#/$defs/schema" + "$ref": "#/$defs/node" } } - } + }, + "unevaluatedProperties": false }, - "schema": { + "edge": { + "x-lint-exclude": "simple_properties_identifiers", + "type": "object", + "required": [ "$ref" ], + "properties": { + "$ref": { + "type": "string", + "pattern": "^#/definitions/(0|[1-9][0-9]*)$" + } + }, + "unevaluatedProperties": false + }, + "node": { "anyOf": [ { "const": {} @@ -58,6 +91,15 @@ "x-lint-exclude": "simple_properties_identifiers", "$ref": "#/$defs/metadata", "type": "object", + "not": { + "x-lint-exclude": "simple_properties_identifiers", + "maxProperties": 1, + "properties": { + "$ref": { + "pattern": "^#" + } + } + }, "required": [ "$ref" ], "properties": { "$ref": { @@ -68,15 +110,8 @@ }, { "x-lint-exclude": "simple_properties_identifiers", + "$ref": "#/$defs/metadata", "type": "object", - "allOf": [ - { - "$ref": "#/$defs/metadata" - }, - { - "$ref": "#/$defs/core" - } - ], "required": [ "enum" ], "properties": { "enum": { @@ -121,15 +156,8 @@ }, { "x-lint-exclude": "simple_properties_identifiers", + "$ref": "#/$defs/metadata", "type": "object", - "allOf": [ - { - "$ref": "#/$defs/metadata" - }, - { - "$ref": "#/$defs/core" - } - ], "required": [ "type", "minLength" ], "properties": { "type": { @@ -154,15 +182,8 @@ }, { "x-lint-exclude": "simple_properties_identifiers", + "$ref": "#/$defs/metadata", "type": "object", - "allOf": [ - { - "$ref": "#/$defs/metadata" - }, - { - "$ref": "#/$defs/core" - } - ], "required": [ "type", "multipleOf" ], "properties": { "type": { @@ -183,15 +204,8 @@ }, { "x-lint-exclude": "simple_properties_identifiers", + "$ref": "#/$defs/metadata", "type": "object", - "allOf": [ - { - "$ref": "#/$defs/metadata" - }, - { - "$ref": "#/$defs/core" - } - ], "required": [ "type" ], "properties": { "type": { @@ -218,15 +232,8 @@ }, { "x-lint-exclude": "simple_properties_identifiers", + "$ref": "#/$defs/metadata", "type": "object", - "allOf": [ - { - "$ref": "#/$defs/metadata" - }, - { - "$ref": "#/$defs/core" - } - ], "required": [ "type", "properties", @@ -241,19 +248,19 @@ "properties": { "type": "object", "additionalProperties": { - "$ref": "#/$defs/schema" + "$ref": "#/$defs/edge" } }, "patternProperties": { "type": "object", "additionalProperties": { - "$ref": "#/$defs/schema" + "$ref": "#/$defs/edge" } }, "additionalProperties": { "anyOf": [ { - "$ref": "#/$defs/schema" + "$ref": "#/$defs/edge" }, { "type": "boolean" @@ -281,22 +288,15 @@ }, { "x-lint-exclude": "simple_properties_identifiers", + "$ref": "#/$defs/metadata", "type": "object", - "allOf": [ - { - "$ref": "#/$defs/metadata" - }, - { - "$ref": "#/$defs/core" - } - ], "required": [ "type", "items", "minItems", "uniqueItems" ], "properties": { "type": { "const": "array" }, "items": { - "$ref": "#/$defs/schema" + "$ref": "#/$defs/edge" }, "minItems": { "type": "integer", @@ -314,15 +314,8 @@ }, { "x-lint-exclude": "simple_properties_identifiers", + "$ref": "#/$defs/metadata", "type": "object", - "allOf": [ - { - "$ref": "#/$defs/metadata" - }, - { - "$ref": "#/$defs/core" - } - ], "required": [ "type", "items", @@ -338,13 +331,13 @@ "type": "array", "minItems": 1, "items": { - "$ref": "#/$defs/schema" + "$ref": "#/$defs/edge" } }, "additionalItems": { "anyOf": [ { - "$ref": "#/$defs/schema" + "$ref": "#/$defs/edge" }, { "type": "boolean" @@ -367,15 +360,8 @@ }, { "x-lint-exclude": "simple_properties_identifiers", + "$ref": "#/$defs/metadata", "type": "object", - "allOf": [ - { - "$ref": "#/$defs/metadata" - }, - { - "$ref": "#/$defs/core" - } - ], "required": [ "anyOf" ], "properties": { "anyOf": { @@ -383,7 +369,7 @@ "minItems": 2, "uniqueItems": true, "items": { - "$ref": "#/$defs/schema" + "$ref": "#/$defs/edge" } } }, @@ -391,15 +377,8 @@ }, { "x-lint-exclude": "simple_properties_identifiers", + "$ref": "#/$defs/metadata", "type": "object", - "allOf": [ - { - "$ref": "#/$defs/metadata" - }, - { - "$ref": "#/$defs/core" - } - ], "required": [ "allOf" ], "properties": { "allOf": { @@ -407,7 +386,7 @@ "minItems": 2, "uniqueItems": true, "items": { - "$ref": "#/$defs/schema" + "$ref": "#/$defs/edge" } } }, @@ -415,22 +394,15 @@ }, { "x-lint-exclude": "simple_properties_identifiers", + "$ref": "#/$defs/metadata", "type": "object", - "allOf": [ - { - "$ref": "#/$defs/metadata" - }, - { - "$ref": "#/$defs/core" - } - ], "required": [ "oneOf" ], "properties": { "oneOf": { "type": "array", "minItems": 2, "items": { - "$ref": "#/$defs/schema" + "$ref": "#/$defs/edge" } } }, @@ -438,19 +410,12 @@ }, { "x-lint-exclude": "simple_properties_identifiers", + "$ref": "#/$defs/metadata", "type": "object", - "allOf": [ - { - "$ref": "#/$defs/metadata" - }, - { - "$ref": "#/$defs/core" - } - ], "required": [ "not" ], "properties": { "not": { - "$ref": "#/$defs/schema" + "$ref": "#/$defs/edge" } }, "unevaluatedProperties": false diff --git a/vendor/blaze/src/alterschema/common/unknown_local_ref.h b/vendor/blaze/src/alterschema/common/unknown_local_ref.h index ae7db851e..68b76343d 100644 --- a/vendor/blaze/src/alterschema/common/unknown_local_ref.h +++ b/vendor/blaze/src/alterschema/common/unknown_local_ref.h @@ -41,20 +41,29 @@ class UnknownLocalRef final : public SchemaTransformRule { frame.reference(SchemaReferenceType::Static, keyword_pointer)}; ONLY_CONTINUE_IF(reference_entry.has_value()); - // If the keyword has no fragment, continue - const auto &reference_fragment{reference_entry->get().fragment}; - ONLY_CONTINUE_IF(reference_fragment.has_value()); - // Only continue if the reference target does not exist ONLY_CONTINUE_IF( !frame.traverse(reference_entry->get().destination).has_value()); - // If there is a base beyond the fragment, the base must exist. - // Otherwise it is likely an external reference? + // Where the reference leads has to be this document for it to be local at + // all. An empty base is a destination that names no document of its own, + // which is how a reference spelled as a fragment alone or as nothing at + // all resolves in a schema that has no base URI, so it never left. + // Otherwise the base is either the one this schema already resolves + // against, or one that names a resource the document holds, such as an + // embedded identifier. + // + // A wrapper format puts the schemas it holds below a root that is no + // schema resource of its own, so nothing framed ever answers to the + // document's own base. A reference to that base never left the document + // all the same, which is why it is compared before the frame is asked. A + // reference spelled without a fragment reaches here for that case alone, + // as its destination is its base, and a base the frame knows would have + // been found as the destination just above const auto &reference_base{reference_entry->get().base}; - if (!reference_base.empty()) { - ONLY_CONTINUE_IF(frame.traverse(reference_base).has_value()); - } + ONLY_CONTINUE_IF(reference_base.empty() || + reference_base == location.base || + frame.traverse(reference_base).has_value()); return applies_to_keywords(KEYWORD); } diff --git a/vendor/blaze/src/alterschema/linter/invalid_external_ref.h b/vendor/blaze/src/alterschema/linter/invalid_external_ref.h index a5f08c09f..e89fe556d 100644 --- a/vendor/blaze/src/alterschema/linter/invalid_external_ref.h +++ b/vendor/blaze/src/alterschema/linter/invalid_external_ref.h @@ -45,10 +45,13 @@ class InvalidExternalRef final : public SchemaTransformRule { // Known official metaschemas are always resolvable ONLY_CONTINUE_IF(!schema_is_known(reference_base)); - // If the base exists in the frame, the reference is internal (e.g. an - // embedded $id). A bad fragment on an internal base is handled by the + // The reference is internal when it leads back to the base this schema + // resolves against, which is what a reference within a wrapper document + // does, or when the base names a resource the document holds, such as an + // embedded $id. A bad fragment on an internal base is handled by the // unknown_local_ref rule instead - ONLY_CONTINUE_IF(!frame.traverse(reference_base).has_value()); + ONLY_CONTINUE_IF(reference_base != location.base && + !frame.traverse(reference_base).has_value()); const auto &has_fragment{reference_entry->get().fragment.has_value()}; const JSON::String base_key{reference_base}; diff --git a/vendor/blaze/src/binpack/CMakeLists.txt b/vendor/blaze/src/binpack/CMakeLists.txt new file mode 100644 index 000000000..b581883a0 --- /dev/null +++ b/vendor/blaze/src/binpack/CMakeLists.txt @@ -0,0 +1,73 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME binpack + FOLDER "Blaze/BinPack" + PRIVATE_HEADERS + error.h + decoder.h + encoder.h + input_stream.h + output_stream.h + encoder_cache.h + encoding.h + SOURCES + compile.cc + mapper/enum_8_bit.h + mapper/enum_8_bit_top_level.h + mapper/enum_arbitrary.h + mapper/enum_singleton.h + mapper/integer_bounded_8_bit.h + mapper/integer_bounded_greater_than_8_bit.h + mapper/integer_bounded_multiplier_8_bit.h + mapper/integer_bounded_multiplier_greater_than_8_bit.h + mapper/integer_lower_bound.h + mapper/integer_lower_bound_multiplier.h + mapper/integer_unbound.h + mapper/integer_unbound_multiplier.h + mapper/integer_upper_bound.h + mapper/integer_upper_bound_multiplier.h + mapper/number_arbitrary.h + + input_stream.cc + output_stream.cc + unreachable.h + cache.cc + + loader.cc + loader_v1_any.h + loader_v1_array.h + loader_v1_integer.h + loader_v1_number.h + loader_v1_string.h + + decoder_any.cc + decoder_array.cc + decoder_common.cc + decoder_integer.cc + decoder_number.cc + decoder_object.cc + decoder_string.cc + encoder_any.cc + encoder_array.cc + encoder_common.cc + encoder_integer.cc + encoder_number.cc + encoder_object.cc + encoder_string.cc) + +if(BLAZE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT blaze NAME binpack) +endif() + +target_link_libraries(sourcemeta_blaze_binpack PUBLIC + sourcemeta::core::json) +target_link_libraries(sourcemeta_blaze_binpack PUBLIC + sourcemeta::core::jsonschema) +target_link_libraries(sourcemeta_blaze_binpack PUBLIC + sourcemeta::core::numeric) +target_link_libraries(sourcemeta_blaze_binpack PUBLIC + sourcemeta::core::io) +target_link_libraries(sourcemeta_blaze_binpack PRIVATE + sourcemeta::core::jsonpointer) +target_link_libraries(sourcemeta_blaze_binpack PRIVATE + sourcemeta::blaze::alterschema) +target_link_libraries(sourcemeta_blaze_binpack PRIVATE + sourcemeta::blaze::canonicalizer) diff --git a/vendor/blaze/src/binpack/cache.cc b/vendor/blaze/src/binpack/cache.cc new file mode 100644 index 000000000..5d2e8d8dc --- /dev/null +++ b/vendor/blaze/src/binpack/cache.cc @@ -0,0 +1,66 @@ +#include + +namespace sourcemeta::blaze { + +auto BinPackCache::record(const sourcemeta::core::JSON::String &value, + const std::uint64_t offset, const Type type) -> void { + // BinPackEncoding a shared string has some overhead, such as the + // shared string marker + the offset, so its not worth + // doing for strings that are too small. + constexpr auto MINIMUM_STRING_LENGTH{3}; + + // We don't want to allow the context to grow + // forever, otherwise an attacker could force the + // program to exhaust memory given an input + // document that contains a high number of large strings. + constexpr auto MAXIMUM_BYTE_SIZE{20971520}; + + const auto value_size{value.size()}; + if (value_size < MINIMUM_STRING_LENGTH || value_size >= MAXIMUM_BYTE_SIZE) { + return; + } + + // Remove the oldest entries to make space if needed + while (!this->data_.empty() && + this->byte_size_ + value_size >= MAXIMUM_BYTE_SIZE) { + this->remove_oldest(); + } + + auto result{this->data_.insert({std::make_pair(value, type), offset})}; + if (result.second) { + this->byte_size_ += value_size; + this->order_.emplace(offset, result.first->first); + } else if (offset > result.first->second) { + this->order_.erase(result.first->second); + // If the string already exists, we want to + // bump the offset for locality purposes. + result.first->second = offset; + this->order_.emplace(offset, result.first->first); + } + + // Otherwise we are doing something wrong + assert(this->order_.size() == this->data_.size()); +} + +auto BinPackCache::remove_oldest() -> void { + assert(!this->data_.empty()); + // std::map are by definition ordered by key, + // so the begin iterator points to the entry + // with the lowest offset, a.k.a. the oldest. + const auto iterator{this->order_.cbegin()}; + this->byte_size_ -= iterator->second.get().first.size(); + this->data_.erase(iterator->second.get()); + this->order_.erase(iterator); +} + +auto BinPackCache::find(const sourcemeta::core::JSON::String &value, + const Type type) const -> std::optional { + const auto result{this->data_.find(std::make_pair(value, type))}; + if (result == this->data_.cend()) { + return std::nullopt; + } + + return result->second; +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/compile.cc b/vendor/blaze/src/binpack/compile.cc new file mode 100644 index 000000000..8c20b1a45 --- /dev/null +++ b/vendor/blaze/src/binpack/compile.cc @@ -0,0 +1,133 @@ +#include + +#include +#include + +#include +#include +#include +#include + +#include // assert +#include // std::string +#include // std::string_view +#include // std::true_type + +namespace { + +constexpr auto ENCODING_V1{"tag:sourcemeta.com,2024:jsonbinpack/encoding/v1"}; + +auto make_encoding_resolver(const sourcemeta::core::SchemaResolver &fallback) + -> auto { + return [&fallback](std::string_view identifier) + -> sourcemeta::core::SchemaResolverResult { + if (identifier == ENCODING_V1) { + static const auto SCHEMA{sourcemeta::core::parse_json(R"JSON({ + "$id": "tag:sourcemeta.com,2024:jsonbinpack/encoding/v1", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "tag:sourcemeta.com,2024:jsonbinpack/encoding/v1": true + } + })JSON")}; + return SCHEMA; + } + + return fallback(identifier); + }; +} + +auto make_encoding(sourcemeta::core::JSON &document, + const std::string &encoding, + const sourcemeta::core::JSON &options) -> void { + document.into_object(); + document.assign("$schema", sourcemeta::core::JSON{ENCODING_V1}); + document.assign("binpackEncoding", sourcemeta::core::JSON{encoding}); + document.assign("binpackOptions", options); +} + +auto transformer_callback_noop( + [[maybe_unused]] const sourcemeta::core::Pointer &pointer, + [[maybe_unused]] const std::string_view name, + [[maybe_unused]] const std::string_view message, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result, + [[maybe_unused]] const bool applied) -> void { + assert(applied); +} + +} // namespace + +namespace sourcemeta::blaze { + +auto binpack_canonicalize(sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const std::string_view default_dialect) -> void { + sourcemeta::blaze::canonicalize( + schema, walker, make_encoding_resolver(resolver), default_dialect); +} + +#include "mapper/enum_8_bit.h" +#include "mapper/enum_8_bit_top_level.h" +#include "mapper/enum_arbitrary.h" +#include "mapper/enum_singleton.h" +#include "mapper/integer_bounded_8_bit.h" +#include "mapper/integer_bounded_greater_than_8_bit.h" +#include "mapper/integer_bounded_multiplier_8_bit.h" +#include "mapper/integer_bounded_multiplier_greater_than_8_bit.h" +#include "mapper/integer_lower_bound.h" +#include "mapper/integer_lower_bound_multiplier.h" +#include "mapper/integer_unbound.h" +#include "mapper/integer_unbound_multiplier.h" +#include "mapper/integer_upper_bound.h" +#include "mapper/integer_upper_bound_multiplier.h" +#include "mapper/number_arbitrary.h" + +auto binpack_compile(sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const std::string_view default_dialect) -> void { + sourcemeta::blaze::binpack_canonicalize(schema, walker, resolver, + default_dialect); + + sourcemeta::blaze::SchemaTransformer mapper; + + // Enums + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + + // Integers + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + + // Numbers + mapper.add(); + + [[maybe_unused]] const auto mapper_result = + mapper.apply(schema, walker, make_encoding_resolver(resolver), + transformer_callback_noop, default_dialect); + assert(mapper_result.first); + + // The "any" encoding is always the last resort + const auto *declared{schema.is_object() ? schema.try_at("$schema") : nullptr}; + const auto dialect{declared != nullptr && declared->is_string() + ? declared->to_string() + : std::string_view{}}; + if (dialect.empty() || dialect != ENCODING_V1) { + make_encoding(schema, "ANY_PACKED_TYPE_TAG_BYTE_PREFIX", + sourcemeta::core::JSON::make_object()); + } +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/decoder_any.cc b/vendor/blaze/src/binpack/decoder_any.cc new file mode 100644 index 000000000..970e40d38 --- /dev/null +++ b/vendor/blaze/src/binpack/decoder_any.cc @@ -0,0 +1,165 @@ +#include + +#include + +#include "unreachable.h" + +#include // assert +#include // std::uint8_t, std::uint16_t, std::uint64_t +#include // std::make_shared + +namespace sourcemeta::blaze { + +auto BinPackDecoder::BYTE_CHOICE_INDEX(const struct BYTE_CHOICE_INDEX &options) + -> sourcemeta::core::JSON { + assert(!options.choices.empty()); + assert(sourcemeta::core::is_byte(options.choices.size())); + const std::uint8_t index{this->get_byte()}; + assert(options.choices.size() > index); + return options.choices[index]; +} + +auto BinPackDecoder::LARGE_CHOICE_INDEX( + const struct LARGE_CHOICE_INDEX &options) -> sourcemeta::core::JSON { + assert(!options.choices.empty()); + const std::uint64_t index{this->get_varint()}; + assert(options.choices.size() > index); + return options.choices[index]; +} + +auto BinPackDecoder::TOP_LEVEL_BYTE_CHOICE_INDEX( + const struct TOP_LEVEL_BYTE_CHOICE_INDEX &options) + -> sourcemeta::core::JSON { + assert(!options.choices.empty()); + assert(sourcemeta::core::is_byte(options.choices.size())); + if (!this->has_more_data()) { + return options.choices.front(); + } + + const std::uint16_t index{static_cast(this->get_byte() + 1)}; + assert(options.choices.size() > index); + return options.choices[index]; +} + +auto BinPackDecoder::CONST_NONE(const struct CONST_NONE &options) + -> sourcemeta::core::JSON { + return options.value; +} + +auto BinPackDecoder::ANY_PACKED_TYPE_TAG_BYTE_PREFIX( + [[maybe_unused]] const struct ANY_PACKED_TYPE_TAG_BYTE_PREFIX &options) + -> sourcemeta::core::JSON { + using namespace any_packed_type_tag_byte_prefix; + const std::uint8_t byte{this->get_byte()}; + const std::uint8_t type{ + static_cast(byte & (0xff >> SUBTYPE_SIZE))}; + const std::uint8_t subtype{static_cast(byte >> TYPE_SIZE)}; + + if (type == TYPE_OTHER) { + switch (subtype) { + case SUBTYPE_NULL: + return sourcemeta::core::JSON{nullptr}; + case SUBTYPE_FALSE: + return sourcemeta::core::JSON{false}; + case SUBTYPE_TRUE: + return sourcemeta::core::JSON{true}; + case SUBTYPE_NUMBER: + return this->DOUBLE_VARINT_TUPLE({}); + case SUBTYPE_DECIMAL: + return this->NUMBER_VARINT_TUPLE({}); + case SUBTYPE_POSITIVE_REAL_INTEGER_BYTE: + return sourcemeta::core::JSON{static_cast(this->get_byte())}; + case SUBTYPE_POSITIVE_INTEGER: + return sourcemeta::core::JSON{ + static_cast(this->get_varint())}; + case SUBTYPE_NEGATIVE_INTEGER: + return sourcemeta::core::JSON{ + -static_cast(this->get_varint()) - 1}; + case SUBTYPE_LONG_STRING_BASE_EXPONENT_7: + return sourcemeta::core::JSON{ + this->get_string_utf8(this->get_varint() + 128)}; + case SUBTYPE_LONG_STRING_BASE_EXPONENT_8: + return sourcemeta::core::JSON{ + this->get_string_utf8(this->get_varint() + 256)}; + case SUBTYPE_LONG_STRING_BASE_EXPONENT_9: + return sourcemeta::core::JSON{ + this->get_string_utf8(this->get_varint() + 512)}; + case SUBTYPE_LONG_STRING_BASE_EXPONENT_10: + return sourcemeta::core::JSON{ + this->get_string_utf8(this->get_varint() + 1024)}; + default: + unreachable(); + } + } else { + switch (type) { + case TYPE_POSITIVE_INTEGER_BYTE: + return sourcemeta::core::JSON{subtype > 0 ? subtype - 1 + : this->get_byte()}; + case TYPE_NEGATIVE_INTEGER_BYTE: + return sourcemeta::core::JSON{ + subtype > 0 ? static_cast(-subtype) + : static_cast(-this->get_byte() - 1)}; + case TYPE_SHARED_STRING: { + const auto length = subtype == 0 + ? this->get_varint() - 1 + + (static_cast( + sourcemeta::core::uint_max<5>) * + 2) + : subtype - 1; + const std::uint64_t position{this->position()}; + const std::uint64_t current{this->rewind(this->get_varint(), position)}; + const sourcemeta::core::JSON value{this->get_string_utf8(length)}; + this->seek(current); + return value; + }; + case TYPE_STRING: + return subtype == 0 + ? this->FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED( + {static_cast( + sourcemeta::core::uint_max<5>) * + 2}) + : sourcemeta::core::JSON{this->get_string_utf8(subtype - 1)}; + case TYPE_LONG_STRING: + return sourcemeta::core::JSON{ + this->get_string_utf8(subtype + sourcemeta::core::uint_max<5>)}; + case TYPE_ARRAY: + return subtype == 0 + ? this->FIXED_TYPED_ARRAY( + {.size = this->get_varint() + + sourcemeta::core::uint_max<5>, + .encoding = std::make_shared( + sourcemeta::blaze:: + ANY_PACKED_TYPE_TAG_BYTE_PREFIX{}), + .prefix_encodings = {}}) + : this->FIXED_TYPED_ARRAY( + {.size = static_cast(subtype - 1), + .encoding = std::make_shared( + sourcemeta::blaze:: + ANY_PACKED_TYPE_TAG_BYTE_PREFIX{}), + .prefix_encodings = {}}); + case TYPE_OBJECT: + return subtype == 0 + ? this->FIXED_TYPED_ARBITRARY_OBJECT( + {.size = this->get_varint() + + sourcemeta::core::uint_max<5>, + .key_encoding = std::make_shared( + sourcemeta::blaze:: + PREFIX_VARINT_LENGTH_STRING_SHARED{}), + .encoding = std::make_shared( + sourcemeta::blaze:: + ANY_PACKED_TYPE_TAG_BYTE_PREFIX{})}) + : this->FIXED_TYPED_ARBITRARY_OBJECT( + {.size = static_cast(subtype - 1), + .key_encoding = std::make_shared( + sourcemeta::blaze:: + PREFIX_VARINT_LENGTH_STRING_SHARED{}), + .encoding = std::make_shared( + sourcemeta::blaze:: + ANY_PACKED_TYPE_TAG_BYTE_PREFIX{})}); + default: + unreachable(); + } + } +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/decoder_array.cc b/vendor/blaze/src/binpack/decoder_array.cc new file mode 100644 index 000000000..46581a14b --- /dev/null +++ b/vendor/blaze/src/binpack/decoder_array.cc @@ -0,0 +1,62 @@ +#include + +#include + +#include // assert +#include // std::uint8_t, std::uint64_t +#include // std::move + +namespace sourcemeta::blaze { + +auto BinPackDecoder::FIXED_TYPED_ARRAY(const struct FIXED_TYPED_ARRAY &options) + -> sourcemeta::core::JSON { + const auto prefix_encodings{options.prefix_encodings.size()}; + sourcemeta::core::JSON result = sourcemeta::core::JSON::make_array(); + for (std::size_t index = 0; index < options.size; index++) { + const BinPackEncoding &encoding{prefix_encodings > index + ? options.prefix_encodings[index] + : *(options.encoding)}; + result.push_back(this->read(encoding)); + } + + assert(result.size() == options.size); + return result; +}; + +auto BinPackDecoder::BOUNDED_8BITS_TYPED_ARRAY( + const struct BOUNDED_8BITS_TYPED_ARRAY &options) -> sourcemeta::core::JSON { + assert(options.maximum >= options.minimum); + assert(sourcemeta::core::is_byte(options.maximum - options.minimum)); + const std::uint8_t byte{this->get_byte()}; + const std::uint64_t size{byte + options.minimum}; + assert(sourcemeta::core::is_within(size, options.minimum, options.maximum)); + return this->FIXED_TYPED_ARRAY( + {.size = size, + .encoding = options.encoding, + .prefix_encodings = options.prefix_encodings}); +}; + +auto BinPackDecoder::FLOOR_TYPED_ARRAY(const struct FLOOR_TYPED_ARRAY &options) + -> sourcemeta::core::JSON { + const std::uint64_t value{this->get_varint()}; + const std::uint64_t size{value + options.minimum}; + assert(size >= value); + assert(size >= options.minimum); + return this->FIXED_TYPED_ARRAY( + {.size = size, + .encoding = options.encoding, + .prefix_encodings = options.prefix_encodings}); +}; + +auto BinPackDecoder::ROOF_TYPED_ARRAY(const struct ROOF_TYPED_ARRAY &options) + -> sourcemeta::core::JSON { + const std::uint64_t value{this->get_varint()}; + const std::uint64_t size{options.maximum - value}; + assert(size <= options.maximum); + return this->FIXED_TYPED_ARRAY( + {.size = size, + .encoding = options.encoding, + .prefix_encodings = options.prefix_encodings}); +}; + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/decoder_common.cc b/vendor/blaze/src/binpack/decoder_common.cc new file mode 100644 index 000000000..400300371 --- /dev/null +++ b/vendor/blaze/src/binpack/decoder_common.cc @@ -0,0 +1,48 @@ +#include + +#include "unreachable.h" + +#include // assert +#include // std::get + +namespace sourcemeta::blaze { + +BinPackDecoder::BinPackDecoder(Stream &input) : BinPackInputStream{input} {} + +auto BinPackDecoder::read(const BinPackEncoding &encoding) + -> sourcemeta::core::JSON { + switch (encoding.index()) { +#define HANDLE_DECODING(index, name) \ + case (index): \ + return this->name(std::get(encoding)); + HANDLE_DECODING(0, BOUNDED_MULTIPLE_8BITS_ENUM_FIXED) + HANDLE_DECODING(1, FLOOR_MULTIPLE_ENUM_VARINT) + HANDLE_DECODING(2, ROOF_MULTIPLE_MIRROR_ENUM_VARINT) + HANDLE_DECODING(3, ARBITRARY_MULTIPLE_ZIGZAG_VARINT) + HANDLE_DECODING(4, DOUBLE_VARINT_TUPLE) + HANDLE_DECODING(5, BYTE_CHOICE_INDEX) + HANDLE_DECODING(6, LARGE_CHOICE_INDEX) + HANDLE_DECODING(7, TOP_LEVEL_BYTE_CHOICE_INDEX) + HANDLE_DECODING(8, CONST_NONE) + HANDLE_DECODING(9, ANY_PACKED_TYPE_TAG_BYTE_PREFIX) + HANDLE_DECODING(10, UTF8_STRING_NO_LENGTH) + HANDLE_DECODING(11, FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED) + HANDLE_DECODING(12, ROOF_VARINT_PREFIX_UTF8_STRING_SHARED) + HANDLE_DECODING(13, BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED) + HANDLE_DECODING(14, RFC3339_DATE_INTEGER_TRIPLET) + HANDLE_DECODING(15, PREFIX_VARINT_LENGTH_STRING_SHARED) + HANDLE_DECODING(16, FIXED_TYPED_ARRAY) + HANDLE_DECODING(17, BOUNDED_8BITS_TYPED_ARRAY) + HANDLE_DECODING(18, FLOOR_TYPED_ARRAY) + HANDLE_DECODING(19, ROOF_TYPED_ARRAY) + HANDLE_DECODING(20, FIXED_TYPED_ARBITRARY_OBJECT) + HANDLE_DECODING(21, VARINT_TYPED_ARBITRARY_OBJECT) + HANDLE_DECODING(22, NUMBER_VARINT_TUPLE) +#undef HANDLE_DECODING + default: + // We should never get here. If so, it is definitely a bug + unreachable(); + } +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/decoder_integer.cc b/vendor/blaze/src/binpack/decoder_integer.cc new file mode 100644 index 000000000..789cc25cc --- /dev/null +++ b/vendor/blaze/src/binpack/decoder_integer.cc @@ -0,0 +1,93 @@ +#include + +#include + +#include // assert +#include // std::uint8_t, std::uint32_t, std::int64_t, std::uint64_t + +namespace sourcemeta::blaze { + +auto BinPackDecoder::BOUNDED_MULTIPLE_8BITS_ENUM_FIXED( + const struct BOUNDED_MULTIPLE_8BITS_ENUM_FIXED &options) + -> sourcemeta::core::JSON { + assert(options.multiplier > 0); + const std::uint8_t byte{this->get_byte()}; + const std::int64_t closest_minimum{ + sourcemeta::core::divide_ceil(options.minimum, options.multiplier)}; + if (closest_minimum >= 0) { + const std::uint64_t closest_minimum_multiple{ + static_cast(closest_minimum) * options.multiplier}; + // We trust the encoder that the data we are seeing + // corresponds to a valid 64-bit signed integer. + return sourcemeta::core::JSON{static_cast( + (byte * options.multiplier) + closest_minimum_multiple)}; + } + + const std::uint64_t closest_minimum_multiple{ + sourcemeta::core::abs(closest_minimum) * options.multiplier}; + // We trust the encoder that the data we are seeing + // corresponds to a valid 64-bit signed integer. + return sourcemeta::core::JSON{static_cast( + (byte * options.multiplier) - closest_minimum_multiple)}; +} + +auto BinPackDecoder::FLOOR_MULTIPLE_ENUM_VARINT( + const struct FLOOR_MULTIPLE_ENUM_VARINT &options) + -> sourcemeta::core::JSON { + assert(options.multiplier > 0); + const std::int64_t closest_minimum{ + sourcemeta::core::divide_ceil(options.minimum, options.multiplier)}; + if (closest_minimum >= 0) { + const std::uint64_t closest_minimum_multiple{ + static_cast(closest_minimum) * options.multiplier}; + // We trust the encoder that the data we are seeing + // corresponds to a valid 64-bit signed integer. + return sourcemeta::core::JSON{static_cast( + (this->get_varint() * options.multiplier) + closest_minimum_multiple)}; + } + + const std::uint64_t closest_minimum_multiple{ + sourcemeta::core::abs(closest_minimum) * options.multiplier}; + // We trust the encoder that the data we are seeing + // corresponds to a valid 64-bit signed integer. + return sourcemeta::core::JSON{static_cast( + (this->get_varint() * options.multiplier) - closest_minimum_multiple)}; +} + +auto BinPackDecoder::ROOF_MULTIPLE_MIRROR_ENUM_VARINT( + const struct ROOF_MULTIPLE_MIRROR_ENUM_VARINT &options) + -> sourcemeta::core::JSON { + assert(options.multiplier > 0); + const std::int64_t closest_maximum{ + sourcemeta::core::divide_floor(options.maximum, options.multiplier)}; + if (closest_maximum >= 0) { + const std::uint64_t closest_maximum_multiple{ + static_cast(closest_maximum) * options.multiplier}; + // We trust the encoder that the data we are seeing + // corresponds to a valid 64-bit signed integer. + return sourcemeta::core::JSON{static_cast( + -(static_cast(this->get_varint() * options.multiplier)) + + static_cast(closest_maximum_multiple))}; + } + + const std::uint64_t closest_maximum_multiple{ + sourcemeta::core::abs(closest_maximum) * options.multiplier}; + // We trust the encoder that the data we are seeing + // corresponds to a valid 64-bit signed integer. + return sourcemeta::core::JSON{static_cast( + -(static_cast(this->get_varint() * options.multiplier)) - + static_cast(closest_maximum_multiple))}; +} + +auto BinPackDecoder::ARBITRARY_MULTIPLE_ZIGZAG_VARINT( + const struct ARBITRARY_MULTIPLE_ZIGZAG_VARINT &options) + -> sourcemeta::core::JSON { + assert(options.multiplier > 0); + // We trust the encoder that the data we are seeing + // corresponds to a valid 64-bit signed integer. + return sourcemeta::core::JSON{ + static_cast(this->get_varint_zigzag() * + static_cast(options.multiplier))}; +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/decoder_number.cc b/vendor/blaze/src/binpack/decoder_number.cc new file mode 100644 index 000000000..3eafae16a --- /dev/null +++ b/vendor/blaze/src/binpack/decoder_number.cc @@ -0,0 +1,63 @@ +#include + +#include + +#include // std::isfinite +#include // std::int64_t, std::uint64_t + +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC optimize("no-reciprocal-math") +#endif + +namespace sourcemeta::blaze { + +auto BinPackDecoder::DOUBLE_VARINT_TUPLE( + [[maybe_unused]] const struct DOUBLE_VARINT_TUPLE &options) + -> sourcemeta::core::JSON { +#ifdef __clang__ +#pragma clang fp reciprocal(off) +#endif + const std::int64_t digits{this->get_varint_zigzag()}; + const std::uint64_t point{this->get_varint()}; + double divisor{1.0}; + for (std::uint64_t i = 0; i < point; ++i) { + divisor *= 10.0; + } + return sourcemeta::core::JSON{static_cast(digits) / divisor}; +} + +auto BinPackDecoder::NUMBER_VARINT_TUPLE( + [[maybe_unused]] const struct NUMBER_VARINT_TUPLE &options) + -> sourcemeta::core::JSON { +#ifdef __clang__ +#pragma clang fp reciprocal(off) +#endif + const std::int64_t coefficient{this->get_varint_zigzag()}; + const std::uint64_t tag{this->get_varint()}; + const std::int64_t exponent{ + sourcemeta::core::zigzag_decode(tag >> 1)}; + + if ((tag & 1) != 0) { + return sourcemeta::core::JSON{ + sourcemeta::core::Decimal{coefficient}.scale_by( + sourcemeta::core::Decimal{exponent})}; + } + + // A scale that has grown past the range of the type can only keep growing, + // so the loop stops there rather than counting out an exponent that a + // corrupt stream may have made arbitrarily large + const std::uint64_t shifts{exponent < 0 + ? static_cast(-exponent) + : static_cast(exponent)}; + double scale{1.0}; + for (std::uint64_t index = 0; index < shifts && std::isfinite(scale); + ++index) { + scale *= 10.0; + } + + return sourcemeta::core::JSON{exponent < 0 + ? static_cast(coefficient) / scale + : static_cast(coefficient) * scale}; +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/decoder_object.cc b/vendor/blaze/src/binpack/decoder_object.cc new file mode 100644 index 000000000..ed63bc7d2 --- /dev/null +++ b/vendor/blaze/src/binpack/decoder_object.cc @@ -0,0 +1,37 @@ +#include + +#include // assert +#include // std::uint64_t + +namespace sourcemeta::blaze { + +auto BinPackDecoder::FIXED_TYPED_ARBITRARY_OBJECT( + const struct FIXED_TYPED_ARBITRARY_OBJECT &options) + -> sourcemeta::core::JSON { + sourcemeta::core::JSON document = sourcemeta::core::JSON::make_object(); + for (std::size_t index = 0; index < options.size; index++) { + const sourcemeta::core::JSON key = this->read(*(options.key_encoding)); + assert(key.is_string()); + document.assign(key.to_string(), this->read(*(options.encoding))); + } + + assert(document.size() == options.size); + return document; +}; + +auto BinPackDecoder::VARINT_TYPED_ARBITRARY_OBJECT( + const struct VARINT_TYPED_ARBITRARY_OBJECT &options) + -> sourcemeta::core::JSON { + const std::uint64_t size{this->get_varint()}; + sourcemeta::core::JSON document = sourcemeta::core::JSON::make_object(); + for (std::size_t index = 0; index < size; index++) { + const sourcemeta::core::JSON key = this->read(*(options.key_encoding)); + assert(key.is_string()); + document.assign(key.to_string(), this->read(*(options.encoding))); + } + + assert(document.size() == size); + return document; +}; + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/decoder_string.cc b/vendor/blaze/src/binpack/decoder_string.cc new file mode 100644 index 000000000..e4c8f4d3a --- /dev/null +++ b/vendor/blaze/src/binpack/decoder_string.cc @@ -0,0 +1,119 @@ +#include + +#include // assert +#include // std::uint8_t, std::uint16_t, std::uint64_t +#include // std::setw, std::setfill +#include // std::basic_ostringstream + +namespace sourcemeta::blaze { + +auto BinPackDecoder::UTF8_STRING_NO_LENGTH( + const struct UTF8_STRING_NO_LENGTH &options) -> sourcemeta::core::JSON { + return sourcemeta::core::JSON{this->get_string_utf8(options.size)}; +} + +auto BinPackDecoder::FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED( + const struct FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED &options) + -> sourcemeta::core::JSON { + const std::uint64_t prefix{this->get_varint()}; + const bool is_shared{prefix == 0}; + const std::uint64_t length{(is_shared ? this->get_varint() : prefix) + + options.minimum - 1}; + assert(length >= options.minimum); + + if (is_shared) { + const std::uint64_t position{this->position()}; + const std::uint64_t current{this->rewind(this->get_varint(), position)}; + const sourcemeta::core::JSON value{this->get_string_utf8(length)}; + this->seek(current); + return value; + } + + return UTF8_STRING_NO_LENGTH({length}); +} + +auto BinPackDecoder::ROOF_VARINT_PREFIX_UTF8_STRING_SHARED( + const struct ROOF_VARINT_PREFIX_UTF8_STRING_SHARED &options) + -> sourcemeta::core::JSON { + const std::uint64_t prefix{this->get_varint()}; + const bool is_shared{prefix == 0}; + const std::uint64_t length{options.maximum - + (is_shared ? this->get_varint() : prefix) + 1}; + assert(length <= options.maximum); + + if (is_shared) { + const std::uint64_t position{this->position()}; + const std::uint64_t current{this->rewind(this->get_varint(), position)}; + const sourcemeta::core::JSON value{UTF8_STRING_NO_LENGTH({length})}; + this->seek(current); + return value; + } + + return UTF8_STRING_NO_LENGTH({length}); +} + +auto BinPackDecoder::BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED( + const struct BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED &options) + -> sourcemeta::core::JSON { + assert(options.minimum <= options.maximum); + assert(sourcemeta::core::is_byte(options.maximum - options.minimum)); + const std::uint8_t prefix{this->get_byte()}; + const bool is_shared{prefix == 0}; + const std::uint64_t length{(is_shared ? this->get_byte() : prefix) + + options.minimum - 1}; + assert(sourcemeta::core::is_within(length, options.minimum, options.maximum)); + + if (is_shared) { + const std::uint64_t position{this->position()}; + const std::uint64_t current{this->rewind(this->get_varint(), position)}; + const sourcemeta::core::JSON value{UTF8_STRING_NO_LENGTH({length})}; + this->seek(current); + return value; + } + + return UTF8_STRING_NO_LENGTH({length}); +} + +auto BinPackDecoder::RFC3339_DATE_INTEGER_TRIPLET( + [[maybe_unused]] const struct RFC3339_DATE_INTEGER_TRIPLET &options) + -> sourcemeta::core::JSON { + const std::uint16_t year{this->get_word()}; + const std::uint8_t month{this->get_byte()}; + const std::uint8_t day{this->get_byte()}; + + assert(year <= 9999); + assert(month >= 1 && month <= 12); + assert(day >= 1 && day <= 31); + + std::basic_ostringstream + output; + output << std::setfill('0'); + output << std::setw(4) << year; + output << "-"; + // Cast the bytes to a larger integer, otherwise + // they will be interpreted as characters. + output << std::setw(2) << static_cast(month); + output << "-"; + output << std::setw(2) << static_cast(day); + + return sourcemeta::core::JSON{output.str()}; +} + +auto BinPackDecoder::PREFIX_VARINT_LENGTH_STRING_SHARED( + const struct PREFIX_VARINT_LENGTH_STRING_SHARED &options) + -> sourcemeta::core::JSON { + const std::uint64_t prefix{this->get_varint()}; + if (prefix == 0) { + const std::uint64_t position{this->position()}; + const std::uint64_t current{this->rewind(this->get_varint(), position)}; + const sourcemeta::core::JSON value{ + PREFIX_VARINT_LENGTH_STRING_SHARED(options)}; + this->seek(current); + return value; + } + + return sourcemeta::core::JSON{this->get_string_utf8(prefix - 1)}; +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/encoder_any.cc b/vendor/blaze/src/binpack/encoder_any.cc new file mode 100644 index 000000000..b36efd443 --- /dev/null +++ b/vendor/blaze/src/binpack/encoder_any.cc @@ -0,0 +1,213 @@ +#include + +#include + +#include "unreachable.h" + +#include // std::find_if +#include // assert +#include // std::uint8_t, std::int64_t, std::uint64_t +#include // std::cbegin, std::cend, std::distance +#include // std::make_shared +#include // std::ranges +#include // std::move + +namespace sourcemeta::blaze { + +auto BinPackEncoder::BYTE_CHOICE_INDEX(const sourcemeta::core::JSON &document, + const struct BYTE_CHOICE_INDEX &options) + -> void { + assert(!options.choices.empty()); + assert(sourcemeta::core::is_byte(options.choices.size())); + const auto iterator{std::ranges::find(options.choices, document)}; + assert(iterator != std::cend(options.choices)); + const auto cursor{std::distance(std::cbegin(options.choices), iterator)}; + assert(sourcemeta::core::is_within( + cursor, 0, static_cast(options.choices.size()))); + this->put_byte(static_cast(cursor)); +} + +auto BinPackEncoder::LARGE_CHOICE_INDEX( + const sourcemeta::core::JSON &document, + const struct LARGE_CHOICE_INDEX &options) -> void { + assert(!options.choices.empty()); + const auto iterator{std::ranges::find_if( + options.choices, + [&document](const auto &choice) -> bool { return choice == document; })}; + assert(iterator != std::cend(options.choices)); + const auto cursor{std::distance(std::cbegin(options.choices), iterator)}; + assert(sourcemeta::core::is_within(cursor, static_cast(0), + options.choices.size() - 1)); + this->put_varint(static_cast(cursor)); +} + +auto BinPackEncoder::TOP_LEVEL_BYTE_CHOICE_INDEX( + const sourcemeta::core::JSON &document, + const struct TOP_LEVEL_BYTE_CHOICE_INDEX &options) -> void { + assert(!options.choices.empty()); + assert(sourcemeta::core::is_byte(options.choices.size())); + const auto iterator{std::ranges::find_if( + options.choices, + [&document](auto const &choice) -> bool { return choice == document; })}; + assert(iterator != std::cend(options.choices)); + const auto cursor{std::distance(std::cbegin(options.choices), iterator)}; + assert(sourcemeta::core::is_within( + cursor, 0, static_cast(options.choices.size()) - 1)); + // This encoding encodes the first option of the enum as "no data" + if (cursor > 0) { + this->put_byte(static_cast(cursor - 1)); + } +} + +auto BinPackEncoder::CONST_NONE( + [[maybe_unused]] const sourcemeta::core::JSON &document, + [[maybe_unused]] const struct CONST_NONE &options) -> void { + assert(document == options.value); +} + +auto BinPackEncoder::ANY_PACKED_TYPE_TAG_BYTE_PREFIX( + const sourcemeta::core::JSON &document, + [[maybe_unused]] const struct ANY_PACKED_TYPE_TAG_BYTE_PREFIX &options) + -> void { + using namespace any_packed_type_tag_byte_prefix; + if (document.is_null()) { + this->put_byte(TYPE_OTHER | (SUBTYPE_NULL << TYPE_SIZE)); + } else if (document.is_boolean()) { + const std::uint8_t subtype{document.to_boolean() ? SUBTYPE_TRUE + : SUBTYPE_FALSE}; + this->put_byte(TYPE_OTHER | + static_cast(subtype << TYPE_SIZE)); + } else if (document.is_real() && document.is_integral()) { + const auto value{document.as_integer()}; + if (value >= 0 && sourcemeta::core::is_byte(value)) { + this->put_byte(TYPE_OTHER | SUBTYPE_POSITIVE_REAL_INTEGER_BYTE + << TYPE_SIZE); + this->put_byte(static_cast(value)); + } else { + this->put_byte(TYPE_OTHER | SUBTYPE_NUMBER << TYPE_SIZE); + this->DOUBLE_VARINT_TUPLE(document, {}); + } + } else if (document.is_real()) { + this->put_byte(TYPE_OTHER | SUBTYPE_NUMBER << TYPE_SIZE); + this->DOUBLE_VARINT_TUPLE(document, {}); + } else if (document.is_integer()) { + const std::int64_t value{document.to_integer()}; + const bool is_positive{value >= 0}; + const std::uint64_t absolute{is_positive + ? static_cast(value) + : sourcemeta::core::abs(value) - 1}; + if (sourcemeta::core::is_byte(absolute)) { + const std::uint8_t type{is_positive ? TYPE_POSITIVE_INTEGER_BYTE + : TYPE_NEGATIVE_INTEGER_BYTE}; + const std::uint8_t absolute_byte{static_cast(absolute)}; + if (absolute < sourcemeta::core::uint_max<5>) { + this->put_byte( + type | static_cast((absolute_byte + 1) << TYPE_SIZE)); + } else { + this->put_byte(type); + this->put_byte(absolute_byte); + } + } else { + const std::uint8_t subtype{is_positive ? SUBTYPE_POSITIVE_INTEGER + : SUBTYPE_NEGATIVE_INTEGER}; + this->put_byte(TYPE_OTHER | + static_cast(subtype << TYPE_SIZE)); + this->put_varint(absolute); + } + } else if (document.is_decimal()) { + this->put_byte(TYPE_OTHER | + static_cast(SUBTYPE_DECIMAL << TYPE_SIZE)); + this->NUMBER_VARINT_TUPLE(document, {}); + } else if (document.is_string()) { + const sourcemeta::core::JSON::String &value{document.to_string()}; + const auto size{document.byte_size()}; + const auto shared{this->cache_.find(value, BinPackCache::Type::Standalone)}; + if (size < sourcemeta::core::uint_max<5>) { + const std::uint8_t type{shared.has_value() ? TYPE_SHARED_STRING + : TYPE_STRING}; + this->put_byte( + static_cast(type | ((size + 1) << TYPE_SIZE))); + if (shared.has_value()) { + this->put_varint(this->position() - shared.value()); + } else { + this->cache_.record(value, this->position(), + BinPackCache::Type::Standalone); + this->put_string_utf8(value, size); + } + } else if (size >= sourcemeta::core::uint_max<5> && + size < + static_cast(sourcemeta::core::uint_max<5>) * + 2 && + !shared.has_value()) { + this->put_byte(static_cast( + TYPE_LONG_STRING | + ((size - sourcemeta::core::uint_max<5>) << TYPE_SIZE))); + this->put_string_utf8(value, size); + } else if (size >= 2 << (SUBTYPE_LONG_STRING_BASE_EXPONENT_7 - 1) && + !shared.has_value()) { + const std::uint8_t exponent{sourcemeta::core::closest_smallest_exponent( + size, 2, SUBTYPE_LONG_STRING_BASE_EXPONENT_7, + SUBTYPE_LONG_STRING_BASE_EXPONENT_10)}; + this->put_byte( + static_cast(TYPE_OTHER | (exponent << TYPE_SIZE))); + this->put_varint(size - static_cast(2 << (exponent - 1))); + this->put_string_utf8(value, size); + } else { + // Exploit the fact that a shared string always starts + // with an impossible length marker (0) to avoid having + // to encode an additional tag + if (!shared.has_value()) { + this->put_byte(TYPE_STRING); + } + + // If we got this far, the string is at least a certain length + FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED( + document, + {static_cast(sourcemeta::core::uint_max<5> * 2)}); + return; + } + } else if (document.is_array()) { + const auto size{document.size()}; + if (size >= sourcemeta::core::uint_max<5>) { + this->put_byte(TYPE_ARRAY); + this->put_varint(size - sourcemeta::core::uint_max<5>); + } else { + this->put_byte( + static_cast(TYPE_ARRAY | ((size + 1) << TYPE_SIZE))); + } + + BinPackEncoding encoding{ + sourcemeta::blaze::ANY_PACKED_TYPE_TAG_BYTE_PREFIX{}}; + this->FIXED_TYPED_ARRAY( + document, + {.size = size, + .encoding = std::make_shared(std::move(encoding)), + .prefix_encodings = {}}); + } else if (document.is_object()) { + const auto size{document.size()}; + if (size >= sourcemeta::core::uint_max<5>) { + this->put_byte(TYPE_OBJECT); + this->put_varint(size - sourcemeta::core::uint_max<5>); + } else { + this->put_byte( + static_cast(TYPE_OBJECT | ((size + 1) << TYPE_SIZE))); + } + + BinPackEncoding key_encoding{ + sourcemeta::blaze::PREFIX_VARINT_LENGTH_STRING_SHARED{}}; + BinPackEncoding value_encoding{ + sourcemeta::blaze::ANY_PACKED_TYPE_TAG_BYTE_PREFIX{}}; + this->FIXED_TYPED_ARBITRARY_OBJECT( + document, + {.size = size, + .key_encoding = + std::make_shared(std::move(key_encoding)), + .encoding = + std::make_shared(std::move(value_encoding))}); + } else { + // We should never get here + unreachable(); + } +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/encoder_array.cc b/vendor/blaze/src/binpack/encoder_array.cc new file mode 100644 index 000000000..cffe87425 --- /dev/null +++ b/vendor/blaze/src/binpack/encoder_array.cc @@ -0,0 +1,64 @@ +#include + +#include + +#include // assert +#include // std::uint8_t +#include // std::move + +namespace sourcemeta::blaze { + +auto BinPackEncoder::FIXED_TYPED_ARRAY(const sourcemeta::core::JSON &document, + const struct FIXED_TYPED_ARRAY &options) + -> void { + assert(document.is_array()); + assert(document.size() == options.size); + const auto prefix_encodings{options.prefix_encodings.size()}; + assert(prefix_encodings <= document.size()); + for (std::size_t index = 0; index < options.size; index++) { + const BinPackEncoding &encoding{prefix_encodings > index + ? options.prefix_encodings[index] + : *(options.encoding)}; + this->write(document.at(index), encoding); + } +} + +auto BinPackEncoder::BOUNDED_8BITS_TYPED_ARRAY( + const sourcemeta::core::JSON &document, + const struct BOUNDED_8BITS_TYPED_ARRAY &options) -> void { + assert(options.maximum >= options.minimum); + const auto size{document.size()}; + assert(sourcemeta::core::is_within(size, options.minimum, options.maximum)); + assert(sourcemeta::core::is_byte(options.maximum - options.minimum)); + this->put_byte(static_cast(size - options.minimum)); + this->FIXED_TYPED_ARRAY(document, + {.size = size, + .encoding = options.encoding, + .prefix_encodings = options.prefix_encodings}); +} + +auto BinPackEncoder::FLOOR_TYPED_ARRAY(const sourcemeta::core::JSON &document, + const struct FLOOR_TYPED_ARRAY &options) + -> void { + const auto size{document.size()}; + assert(size >= options.minimum); + this->put_varint(size - options.minimum); + this->FIXED_TYPED_ARRAY(document, + {.size = size, + .encoding = options.encoding, + .prefix_encodings = options.prefix_encodings}); +} + +auto BinPackEncoder::ROOF_TYPED_ARRAY(const sourcemeta::core::JSON &document, + const struct ROOF_TYPED_ARRAY &options) + -> void { + const auto size{document.size()}; + assert(size <= options.maximum); + this->put_varint(options.maximum - size); + this->FIXED_TYPED_ARRAY(document, + {.size = size, + .encoding = options.encoding, + .prefix_encodings = options.prefix_encodings}); +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/encoder_common.cc b/vendor/blaze/src/binpack/encoder_common.cc new file mode 100644 index 000000000..b56d56caa --- /dev/null +++ b/vendor/blaze/src/binpack/encoder_common.cc @@ -0,0 +1,48 @@ +#include + +#include "unreachable.h" + +#include // assert +#include // std::get + +namespace sourcemeta::blaze { + +BinPackEncoder::BinPackEncoder(Stream &output) : BinPackOutputStream{output} {} + +auto BinPackEncoder::write(const sourcemeta::core::JSON &document, + const BinPackEncoding &encoding) -> void { + switch (encoding.index()) { +#define HANDLE_ENCODING(index, name) \ + case (index): \ + return this->name(document, std::get(encoding)); + HANDLE_ENCODING(0, BOUNDED_MULTIPLE_8BITS_ENUM_FIXED) + HANDLE_ENCODING(1, FLOOR_MULTIPLE_ENUM_VARINT) + HANDLE_ENCODING(2, ROOF_MULTIPLE_MIRROR_ENUM_VARINT) + HANDLE_ENCODING(3, ARBITRARY_MULTIPLE_ZIGZAG_VARINT) + HANDLE_ENCODING(4, DOUBLE_VARINT_TUPLE) + HANDLE_ENCODING(5, BYTE_CHOICE_INDEX) + HANDLE_ENCODING(6, LARGE_CHOICE_INDEX) + HANDLE_ENCODING(7, TOP_LEVEL_BYTE_CHOICE_INDEX) + HANDLE_ENCODING(8, CONST_NONE) + HANDLE_ENCODING(9, ANY_PACKED_TYPE_TAG_BYTE_PREFIX) + HANDLE_ENCODING(10, UTF8_STRING_NO_LENGTH) + HANDLE_ENCODING(11, FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED) + HANDLE_ENCODING(12, ROOF_VARINT_PREFIX_UTF8_STRING_SHARED) + HANDLE_ENCODING(13, BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED) + HANDLE_ENCODING(14, RFC3339_DATE_INTEGER_TRIPLET) + HANDLE_ENCODING(15, PREFIX_VARINT_LENGTH_STRING_SHARED) + HANDLE_ENCODING(16, FIXED_TYPED_ARRAY) + HANDLE_ENCODING(17, BOUNDED_8BITS_TYPED_ARRAY) + HANDLE_ENCODING(18, FLOOR_TYPED_ARRAY) + HANDLE_ENCODING(19, ROOF_TYPED_ARRAY) + HANDLE_ENCODING(20, FIXED_TYPED_ARBITRARY_OBJECT) + HANDLE_ENCODING(21, VARINT_TYPED_ARBITRARY_OBJECT) + HANDLE_ENCODING(22, NUMBER_VARINT_TUPLE) +#undef HANDLE_ENCODING + default: + // We should never get here. If so, it is definitely a bug + unreachable(); + } +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/encoder_integer.cc b/vendor/blaze/src/binpack/encoder_integer.cc new file mode 100644 index 000000000..6799065ad --- /dev/null +++ b/vendor/blaze/src/binpack/encoder_integer.cc @@ -0,0 +1,77 @@ +#include + +#include + +#include // assert +#include // std::uint8_t, std::int64_t, std::uint64_t + +namespace sourcemeta::blaze { + +auto BinPackEncoder::BOUNDED_MULTIPLE_8BITS_ENUM_FIXED( + const sourcemeta::core::JSON &document, + const struct BOUNDED_MULTIPLE_8BITS_ENUM_FIXED &options) -> void { + assert(document.is_integer()); + const std::int64_t value{document.to_integer()}; + assert(sourcemeta::core::is_within(value, options.minimum, options.maximum)); + assert(options.multiplier > 0); + assert(sourcemeta::core::abs(value) % options.multiplier == 0); + const std::int64_t enum_minimum{ + sourcemeta::core::divide_ceil(options.minimum, options.multiplier)}; +#ifndef NDEBUG + const std::int64_t enum_maximum{ + sourcemeta::core::divide_floor(options.maximum, options.multiplier)}; +#endif + assert(sourcemeta::core::is_byte(enum_maximum - enum_minimum)); + this->put_byte(static_cast( + (value / static_cast(options.multiplier)) - enum_minimum)); +} + +auto BinPackEncoder::FLOOR_MULTIPLE_ENUM_VARINT( + const sourcemeta::core::JSON &document, + const struct FLOOR_MULTIPLE_ENUM_VARINT &options) -> void { + assert(document.is_integer()); + const std::int64_t value{document.to_integer()}; + assert(options.minimum <= value); + assert(options.multiplier > 0); + assert(sourcemeta::core::abs(value) % options.multiplier == 0); + if (options.multiplier == 1) { + this->put_varint(static_cast(value - options.minimum)); + return; + } + + this->put_varint( + (static_cast(value) / options.multiplier) - + static_cast(sourcemeta::core::divide_ceil( + options.minimum, static_cast(options.multiplier)))); +} + +auto BinPackEncoder::ROOF_MULTIPLE_MIRROR_ENUM_VARINT( + const sourcemeta::core::JSON &document, + const struct ROOF_MULTIPLE_MIRROR_ENUM_VARINT &options) -> void { + assert(document.is_integer()); + const std::int64_t value{document.to_integer()}; + assert(value <= options.maximum); + assert(options.multiplier > 0); + assert(sourcemeta::core::abs(value) % options.multiplier == 0); + if (options.multiplier == 1) { + this->put_varint(static_cast(options.maximum - value)); + return; + } + + this->put_varint(static_cast(sourcemeta::core::divide_floor( + options.maximum, options.multiplier)) - + (static_cast(value) / options.multiplier)); +} + +auto BinPackEncoder::ARBITRARY_MULTIPLE_ZIGZAG_VARINT( + const sourcemeta::core::JSON &document, + const struct ARBITRARY_MULTIPLE_ZIGZAG_VARINT &options) -> void { + assert(document.is_integer()); + const std::int64_t value{document.to_integer()}; + assert(options.multiplier > 0); + assert(sourcemeta::core::abs(value) % options.multiplier == 0); + this->put_varint_zigzag(value / + static_cast(options.multiplier)); +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/encoder_number.cc b/vendor/blaze/src/binpack/encoder_number.cc new file mode 100644 index 000000000..99117e47d --- /dev/null +++ b/vendor/blaze/src/binpack/encoder_number.cc @@ -0,0 +1,115 @@ +#include + +#include + +#include // assert +#include // std::from_chars +#include // std::size_t +#include // std::int64_t, std::uint64_t +#include // std::numeric_limits +#include // std::string_view +#include // std::errc + +namespace { + +#ifndef NDEBUG +// The widest coefficient that still fits in a signed 64-bit integer +constexpr std::size_t MAXIMUM_COEFFICIENT_DIGITS{19}; +#endif + +// The coefficient and the base-10 exponent of an arbitrary precision decimal. +// Core keeps both of them private and only spells them out through the +// scientific form, as `[-]D[.DDDD]e(+|-)ADJUSTED`, whose exponent is adjusted +// to count from the leading digit rather than from the last one +auto decimal_coefficient(const sourcemeta::core::Decimal &value, + std::int64_t &exponent) -> std::int64_t { + const auto scientific{value.reduce().to_scientific_string()}; + const std::string_view view{scientific}; + const auto marker{view.find('e')}; + assert(marker != std::string_view::npos); + + auto mantissa{view.substr(0, marker)}; + const auto negative{mantissa.front() == '-'}; + if (negative) { + mantissa.remove_prefix(1); + } + + // Every digit past the leading one sits after a decimal mark that the + // adjusted exponent already accounts for + const auto point{mantissa.find('.')}; + const auto leading{mantissa.substr( + 0, point == std::string_view::npos ? mantissa.size() : point)}; + const auto trailing{point == std::string_view::npos + ? std::string_view{} + : mantissa.substr(point + 1)}; + + const auto digits{leading.size() + trailing.size()}; + assert(digits >= 1); + assert(digits <= MAXIMUM_COEFFICIENT_DIGITS); + + std::uint64_t magnitude{0}; + for (const auto digit : leading) { + magnitude = (magnitude * 10) + static_cast(digit - '0'); + } + for (const auto digit : trailing) { + magnitude = (magnitude * 10) + static_cast(digit - '0'); + } + + assert(magnitude <= + static_cast(std::numeric_limits::max())); + + auto adjusted_exponent{view.substr(marker + 1)}; + if (adjusted_exponent.front() == '+') { + adjusted_exponent.remove_prefix(1); + } + + std::int64_t adjusted{0}; + [[maybe_unused]] const auto conversion{std::from_chars( + adjusted_exponent.data(), + adjusted_exponent.data() + adjusted_exponent.size(), adjusted)}; + assert(conversion.ec == std::errc{}); + + exponent = adjusted - static_cast(digits - 1); + const auto coefficient{static_cast(magnitude)}; + return negative ? -coefficient : coefficient; +} + +} // namespace + +namespace sourcemeta::blaze { + +auto BinPackEncoder::DOUBLE_VARINT_TUPLE( + const sourcemeta::core::JSON &document, + [[maybe_unused]] const struct DOUBLE_VARINT_TUPLE &options) -> void { + assert(document.is_real()); + const auto value{document.to_real()}; + std::uint64_t point_position; + const std::int64_t integral{ + sourcemeta::core::real_digits(value, point_position)}; + this->put_varint_zigzag(integral); + this->put_varint(point_position); +} + +auto BinPackEncoder::NUMBER_VARINT_TUPLE( + const sourcemeta::core::JSON &document, + [[maybe_unused]] const struct NUMBER_VARINT_TUPLE &options) -> void { + assert(document.is_real() || document.is_decimal()); + const auto decimal{document.is_decimal()}; + std::int64_t coefficient{0}; + std::int64_t exponent{0}; + + if (decimal) { + coefficient = decimal_coefficient(document.to_decimal(), exponent); + } else { + std::uint64_t point_position; + coefficient = sourcemeta::core::real_digits( + document.to_real(), point_position); + exponent = -static_cast(point_position); + } + + this->put_varint_zigzag(coefficient); + this->put_varint((sourcemeta::core::zigzag_encode(exponent) << 1) | + static_cast(decimal)); +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/encoder_object.cc b/vendor/blaze/src/binpack/encoder_object.cc new file mode 100644 index 000000000..6c8a21e96 --- /dev/null +++ b/vendor/blaze/src/binpack/encoder_object.cc @@ -0,0 +1,32 @@ +#include + +#include // assert + +namespace sourcemeta::blaze { + +auto BinPackEncoder::FIXED_TYPED_ARBITRARY_OBJECT( + const sourcemeta::core::JSON &document, + const struct FIXED_TYPED_ARBITRARY_OBJECT &options) -> void { + assert(document.is_object()); + assert(document.size() == options.size); + + for (const auto &entry : document.as_object()) { + this->write(sourcemeta::core::JSON{entry.first}, *(options.key_encoding)); + this->write(entry.second, *(options.encoding)); + } +} + +auto BinPackEncoder::VARINT_TYPED_ARBITRARY_OBJECT( + const sourcemeta::core::JSON &document, + const struct VARINT_TYPED_ARBITRARY_OBJECT &options) -> void { + assert(document.is_object()); + const auto size{document.size()}; + this->put_varint(size); + + for (const auto &entry : document.as_object()) { + this->write(sourcemeta::core::JSON{entry.first}, *(options.key_encoding)); + this->write(entry.second, *(options.encoding)); + } +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/encoder_string.cc b/vendor/blaze/src/binpack/encoder_string.cc new file mode 100644 index 000000000..cbbf3af2e --- /dev/null +++ b/vendor/blaze/src/binpack/encoder_string.cc @@ -0,0 +1,156 @@ +#include + +#include // assert +#include // std::uint8_t, std::uint16_t +#include // std::stoul + +namespace sourcemeta::blaze { + +auto BinPackEncoder::UTF8_STRING_NO_LENGTH( + const sourcemeta::core::JSON &document, + const struct UTF8_STRING_NO_LENGTH &options) -> void { + assert(document.is_string()); + const sourcemeta::core::JSON::String &value{document.to_string()}; + this->put_string_utf8(value, options.size); +} + +auto BinPackEncoder::FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED( + const sourcemeta::core::JSON &document, + const struct FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED &options) -> void { + assert(document.is_string()); + const sourcemeta::core::JSON::String &value{document.to_string()}; + const auto size{value.size()}; + assert(document.byte_size() == size); + const auto shared{this->cache_.find(value, BinPackCache::Type::Standalone)}; + + // (1) Write 0x00 if shared, else do nothing + if (shared.has_value()) { + this->put_byte(0); + } + + // (2) Write length of the string + 1 (so it will never be zero) + this->put_varint(size - options.minimum + 1); + + // (3) Write relative offset if shared, else write plain string + if (shared.has_value()) { + this->put_varint(this->position() - shared.value()); + } else { + this->cache_.record(value, this->position(), + BinPackCache::Type::Standalone); + this->put_string_utf8(value, size); + } +} + +auto BinPackEncoder::ROOF_VARINT_PREFIX_UTF8_STRING_SHARED( + const sourcemeta::core::JSON &document, + const struct ROOF_VARINT_PREFIX_UTF8_STRING_SHARED &options) -> void { + assert(document.is_string()); + const sourcemeta::core::JSON::String &value{document.to_string()}; + const auto size{value.size()}; + assert(document.byte_size() == size); + assert(size <= options.maximum); + const auto shared{this->cache_.find(value, BinPackCache::Type::Standalone)}; + + // (1) Write 0x00 if shared, else do nothing + if (shared.has_value()) { + this->put_byte(0); + } + + // (2) Write length of the string + 1 (so it will never be zero) + this->put_varint(options.maximum - size + 1); + + // (3) Write relative offset if shared, else write plain string + if (shared.has_value()) { + this->put_varint(this->position() - shared.value()); + } else { + this->cache_.record(value, this->position(), + BinPackCache::Type::Standalone); + this->put_string_utf8(value, size); + } +} + +auto BinPackEncoder::BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED( + const sourcemeta::core::JSON &document, + const struct BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED &options) -> void { + assert(document.is_string()); + const sourcemeta::core::JSON::String &value{document.to_string()}; + const auto size{value.size()}; + assert(document.byte_size() == size); + assert(options.minimum <= options.maximum); + assert(sourcemeta::core::is_byte(options.maximum - options.minimum + 1)); + assert(sourcemeta::core::is_within(size, options.minimum, options.maximum)); + const auto shared{this->cache_.find(value, BinPackCache::Type::Standalone)}; + + // (1) Write 0x00 if shared, else do nothing + if (shared.has_value()) { + this->put_byte(0); + } + + // (2) Write length of the string + 1 (so it will never be zero) + this->put_byte(static_cast(size - options.minimum + 1)); + + // (3) Write relative offset if shared, else write plain string + if (shared.has_value()) { + this->put_varint(this->position() - shared.value()); + } else { + this->cache_.record(value, this->position(), + BinPackCache::Type::Standalone); + this->put_string_utf8(value, size); + } +} + +auto BinPackEncoder::RFC3339_DATE_INTEGER_TRIPLET( + const sourcemeta::core::JSON &document, + [[maybe_unused]] const struct RFC3339_DATE_INTEGER_TRIPLET &options) + -> void { + assert(document.is_string()); + const auto &value{document.to_string()}; + assert(value.size() == 10); + assert(document.size() == value.size()); + + // As according to RFC3339: Internet Protocols MUST + // generate four digit years in dates. + const std::uint16_t year{ + static_cast(std::stoul(value.substr(0, 4)))}; + const std::uint8_t month{ + static_cast(std::stoul(value.substr(5, 2)))}; + const std::uint8_t day{ + static_cast(std::stoul(value.substr(8, 2)))}; + assert(month >= 1 && month <= 12); + assert(day >= 1 && day <= 31); + + this->put_word(year); + this->put_byte(month); + this->put_byte(day); +} + +auto BinPackEncoder::PREFIX_VARINT_LENGTH_STRING_SHARED( + const sourcemeta::core::JSON &document, + [[maybe_unused]] const struct PREFIX_VARINT_LENGTH_STRING_SHARED &options) + -> void { + assert(document.is_string()); + const sourcemeta::core::JSON::String &value{document.to_string()}; + + const auto shared{ + this->cache_.find(value, BinPackCache::Type::PrefixLengthVarintPlusOne)}; + if (shared.has_value()) { + const auto new_offset{this->position()}; + this->put_byte(0); + this->put_varint(this->position() - shared.value()); + // Bump the context cache for locality purposes + this->cache_.record(value, new_offset, + BinPackCache::Type::PrefixLengthVarintPlusOne); + } else { + const auto size{value.size()}; + assert(document.byte_size() == size); + this->cache_.record(value, this->position(), + BinPackCache::Type::PrefixLengthVarintPlusOne); + this->put_varint(size + 1); + // Also record a standalone variant of it + this->cache_.record(value, this->position(), + BinPackCache::Type::Standalone); + this->put_string_utf8(value, size); + } +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack.h b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack.h new file mode 100644 index 000000000..cc145c2a5 --- /dev/null +++ b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack.h @@ -0,0 +1,175 @@ +#ifndef SOURCEMETA_BLAZE_BINPACK_H_ +#define SOURCEMETA_BLAZE_BINPACK_H_ + +#ifndef SOURCEMETA_BLAZE_BINPACK_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +#include +#include +#include +#include +#include +// NOLINTEND(misc-include-cleaner) + +#include +#include + +#include // std::string_view + +/// @defgroup binpack BinPack +/// @brief A space-efficient binary JSON serialization format based on JSON +/// Schema +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` +/// +/// JSON BinPack is a binary JSON serialization format with a strong focus on +/// space efficiency. It encodes any JSON document, and it runs in both +/// schema-driven and schema-less modes given a matching JSON Schema 2020-12 +/// definition. +/// +/// ### Highly space-efficient +/// +/// A reproducible benchmark study has proven JSON BinPack to be space-efficient +/// in comparison to 12 alternative binary serialization formats in every +/// considered case. It also typically provides a higher average size reduction +/// than general purpose compressors such as GZIP, LZMA and LZ4 configured with +/// their highest compression levels, specially for small and medium-sized +/// documents. For large documents, combining JSON BinPack with a compression +/// format may achieve greater space-efficiency still. The benchmark is live at +/// https://benchmark.sourcemeta.com. +/// +/// ### Based on JSON Schema +/// +/// JSON BinPack adopts the industry-standard schema language for JSON +/// documents, so the schemas you already have in your OpenAPI, AsyncAPI, RAML +/// or other specifications can be reused as they are. There is no additional +/// domain-specific schema language to learn, and no translation step between +/// them. For the same reason, JSON BinPack works alongside any schema evolution +/// tooling from the wider JSON Schema ecosystem. +/// +/// ### Optional schema +/// +/// JSON BinPack is a hybrid serialization format that runs in both +/// schema-driven and schema-less modes. Thanks to JSON Schema being a +/// constraint system rather than a structural definition language, a schema can +/// be as loose or as specific as the use case demands, even within the same +/// document, without reaching for a different serialization technology. +/// Free-form data and unknown fields encode without trouble as long as the +/// corresponding schema validates the data. +/// +/// ### Space efficiency over runtime efficiency +/// +/// When transmitting data over the Internet, time is the bottleneck, which +/// makes computation cheap in comparison. Leaving extremely low-powered devices +/// aside, trading more CPU cycles for better compression is a sensible choice. +/// JSON BinPack excels at improving network performance over unreliable and +/// low-bandwidth connections on mobile and IoT systems. If instead the use case +/// demands extreme runtime efficiency or zero-copy serialization, such as +/// intra-service or inter-process communication, consider a format such as +/// FlatBuffers or Cap'n Proto. +/// +/// ### Research +/// +/// JSON BinPack is the result of binary serialization research at the +/// Department of Computer Science of University of Oxford: +/// +/// - [JSON BinPack: A space-efficient schema-driven and schema-less binary +/// serialization specification based on JSON +/// Schema](https://www.jviotti.com/dissertation.pdf) +/// - [Benchmarking JSON BinPack](https://arxiv.org/abs/2211.12799) +/// - [A benchmark of JSON-compatible binary serialization +/// specifications](https://arxiv.org/abs/2201.03051) +/// - [A survey of JSON-compatible binary serialization +/// specification](https://arxiv.org/abs/2201.02089) + +namespace sourcemeta::blaze { + +/// @ingroup binpack +/// +/// Compile a JSON Schema into an encoding schema. Keep in mind this function +/// mutates the input schema. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// #include +/// #include +/// +/// auto schema{sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "integer", +/// "minimum": -100, +/// "maximum": 100 +/// })JSON")}; +/// +/// sourcemeta::blaze::binpack_compile( +/// schema, sourcemeta::core::schema_walker, +/// sourcemeta::core::schema_resolver); +/// +/// const auto encoding{sourcemeta::blaze::binpack_load(schema)}; +/// +/// std::ostringstream output; +/// sourcemeta::blaze::BinPackEncoder encoder{output}; +/// encoder.write(sourcemeta::core::JSON{5}, encoding); +/// +/// std::istringstream input{output.str()}; +/// sourcemeta::blaze::BinPackDecoder decoder{input}; +/// sourcemeta::core::prettify(decoder.read(encoding), std::cout); +/// std::cout << std::endl; +/// ``` +SOURCEMETA_BLAZE_BINPACK_EXPORT +auto binpack_compile(sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + std::string_view default_dialect = "") -> void; + +/// @ingroup binpack +/// +/// Transform a JSON Schema into its canonical form to prepare it for +/// compilation. Keep in mind this function mutates the input schema. Also, the +/// `binpack_compile` function already performs canonicalization. This function +/// is exposed mainly for debugging and testing purposes. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// #include +/// +/// auto schema{sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "string" +/// })JSON")}; +/// +/// sourcemeta::blaze::binpack_canonicalize( +/// schema, sourcemeta::core::schema_walker, +/// sourcemeta::core::schema_resolver); +/// +/// sourcemeta::core::prettify(schema, std::cout); +/// std::cout << std::endl; +/// ``` +SOURCEMETA_BLAZE_BINPACK_EXPORT +auto binpack_canonicalize(sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + std::string_view default_dialect = "") -> void; + +/// @ingroup binpack +/// Load an encoding schema into its in-memory representation +SOURCEMETA_BLAZE_BINPACK_EXPORT +auto binpack_load(const sourcemeta::core::JSON &input) -> BinPackEncoding; + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_decoder.h b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_decoder.h new file mode 100644 index 000000000..4aa5cf339 --- /dev/null +++ b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_decoder.h @@ -0,0 +1,76 @@ +#ifndef SOURCEMETA_BLAZE_BINPACK_DECODER_H_ +#define SOURCEMETA_BLAZE_BINPACK_DECODER_H_ + +#ifndef SOURCEMETA_BLAZE_BINPACK_EXPORT +#include +#endif + +#include +#include + +#include + +namespace sourcemeta::blaze { + +/// @ingroup binpack +class SOURCEMETA_BLAZE_BINPACK_EXPORT BinPackDecoder + : private BinPackInputStream { +public: + BinPackDecoder(Stream &input); + auto read(const BinPackEncoding &encoding) -> sourcemeta::core::JSON; + +// The methods that implement individual encodings as considered private +#ifndef DOXYGEN +// The encoding names are the ones the JSON BinPack specification defines, so +// they keep their casing rather than following the C++ naming convention +// NOLINTBEGIN(readability-identifier-naming) +#define DECLARE_ENCODING(name) \ + auto name(const name &) -> sourcemeta::core::JSON; + + // Integer + DECLARE_ENCODING(BOUNDED_MULTIPLE_8BITS_ENUM_FIXED) + DECLARE_ENCODING(FLOOR_MULTIPLE_ENUM_VARINT) + DECLARE_ENCODING(ROOF_MULTIPLE_MIRROR_ENUM_VARINT) + DECLARE_ENCODING(ARBITRARY_MULTIPLE_ZIGZAG_VARINT) + + // Number + DECLARE_ENCODING(DOUBLE_VARINT_TUPLE) + DECLARE_ENCODING(NUMBER_VARINT_TUPLE) + + // Any + DECLARE_ENCODING(BYTE_CHOICE_INDEX) + DECLARE_ENCODING(LARGE_CHOICE_INDEX) + DECLARE_ENCODING(TOP_LEVEL_BYTE_CHOICE_INDEX) + DECLARE_ENCODING(CONST_NONE) + DECLARE_ENCODING(ANY_PACKED_TYPE_TAG_BYTE_PREFIX) + + // String + DECLARE_ENCODING(UTF8_STRING_NO_LENGTH) + DECLARE_ENCODING(FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED) + DECLARE_ENCODING(ROOF_VARINT_PREFIX_UTF8_STRING_SHARED) + DECLARE_ENCODING(BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED) + DECLARE_ENCODING(RFC3339_DATE_INTEGER_TRIPLET) + DECLARE_ENCODING(PREFIX_VARINT_LENGTH_STRING_SHARED) + // TODO: Implement STRING_BROTLI encoding + // TODO: Implement STRING_DICTIONARY_COMPRESSOR encoding + // TODO: Implement STRING_UNBOUNDED_SCOPED_PREFIX_LENGTH encoding + // TODO: Implement URL_PROTOCOL_HOST_REST encoding + + // Array + DECLARE_ENCODING(FIXED_TYPED_ARRAY) + DECLARE_ENCODING(BOUNDED_8BITS_TYPED_ARRAY) + DECLARE_ENCODING(FLOOR_TYPED_ARRAY) + DECLARE_ENCODING(ROOF_TYPED_ARRAY) + + // Object + DECLARE_ENCODING(FIXED_TYPED_ARBITRARY_OBJECT) + DECLARE_ENCODING(VARINT_TYPED_ARBITRARY_OBJECT) + +#undef DECLARE_ENCODING +// NOLINTEND(readability-identifier-naming) +#endif +}; + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_encoder.h b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_encoder.h new file mode 100644 index 000000000..e1ca54773 --- /dev/null +++ b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_encoder.h @@ -0,0 +1,81 @@ +#ifndef SOURCEMETA_BLAZE_BINPACK_ENCODER_H_ +#define SOURCEMETA_BLAZE_BINPACK_ENCODER_H_ + +#ifndef SOURCEMETA_BLAZE_BINPACK_EXPORT +#include +#endif + +#include +#include +#include + +#include + +namespace sourcemeta::blaze { + +/// @ingroup binpack +class SOURCEMETA_BLAZE_BINPACK_EXPORT BinPackEncoder + : private BinPackOutputStream { +public: + BinPackEncoder(Stream &output); + auto write(const sourcemeta::core::JSON &document, + const BinPackEncoding &encoding) -> void; + +// The methods that implement individual encodings as considered private +#ifndef DOXYGEN +// The encoding names are the ones the JSON BinPack specification defines, so +// they keep their casing rather than following the C++ naming convention +// NOLINTBEGIN(readability-identifier-naming) +#define DECLARE_ENCODING(name) \ + auto name(const sourcemeta::core::JSON &document, const name &) -> void; + + // Integer + DECLARE_ENCODING(BOUNDED_MULTIPLE_8BITS_ENUM_FIXED) + DECLARE_ENCODING(FLOOR_MULTIPLE_ENUM_VARINT) + DECLARE_ENCODING(ROOF_MULTIPLE_MIRROR_ENUM_VARINT) + DECLARE_ENCODING(ARBITRARY_MULTIPLE_ZIGZAG_VARINT) + + // Number + DECLARE_ENCODING(DOUBLE_VARINT_TUPLE) + DECLARE_ENCODING(NUMBER_VARINT_TUPLE) + + // Any + DECLARE_ENCODING(BYTE_CHOICE_INDEX) + DECLARE_ENCODING(LARGE_CHOICE_INDEX) + DECLARE_ENCODING(TOP_LEVEL_BYTE_CHOICE_INDEX) + DECLARE_ENCODING(CONST_NONE) + DECLARE_ENCODING(ANY_PACKED_TYPE_TAG_BYTE_PREFIX) + + // String + DECLARE_ENCODING(UTF8_STRING_NO_LENGTH) + DECLARE_ENCODING(FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED) + DECLARE_ENCODING(ROOF_VARINT_PREFIX_UTF8_STRING_SHARED) + DECLARE_ENCODING(BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED) + DECLARE_ENCODING(RFC3339_DATE_INTEGER_TRIPLET) + DECLARE_ENCODING(PREFIX_VARINT_LENGTH_STRING_SHARED) + // TODO: Implement STRING_BROTLI encoding + // TODO: Implement STRING_DICTIONARY_COMPRESSOR encoding + // TODO: Implement STRING_UNBOUNDED_SCOPED_PREFIX_LENGTH encoding + // TODO: Implement URL_PROTOCOL_HOST_REST encoding + + // Array + DECLARE_ENCODING(FIXED_TYPED_ARRAY) + DECLARE_ENCODING(BOUNDED_8BITS_TYPED_ARRAY) + DECLARE_ENCODING(FLOOR_TYPED_ARRAY) + DECLARE_ENCODING(ROOF_TYPED_ARRAY) + + // Object + DECLARE_ENCODING(FIXED_TYPED_ARBITRARY_OBJECT) + DECLARE_ENCODING(VARINT_TYPED_ARBITRARY_OBJECT) + +#undef DECLARE_ENCODING +// NOLINTEND(readability-identifier-naming) +#endif + +private: + BinPackCache cache_; +}; + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_encoder_cache.h b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_encoder_cache.h new file mode 100644 index 000000000..79c3376bb --- /dev/null +++ b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_encoder_cache.h @@ -0,0 +1,51 @@ +#ifndef SOURCEMETA_BLAZE_BINPACK_ENCODER_CACHE_H_ +#define SOURCEMETA_BLAZE_BINPACK_ENCODER_CACHE_H_ +#ifndef DOXYGEN + +#ifndef SOURCEMETA_BLAZE_BINPACK_EXPORT +#include +#endif + +#include + +#include // std::reference_wrapper +#include // std::map +#include // std::optional +#include // std::pair + +namespace sourcemeta::blaze { + +class SOURCEMETA_BLAZE_BINPACK_EXPORT BinPackCache { +public: + enum class Type : std::uint8_t { Standalone, PrefixLengthVarintPlusOne }; + auto record(const sourcemeta::core::JSON::String &value, + const std::uint64_t offset, const Type type) -> void; + [[nodiscard]] auto find(const sourcemeta::core::JSON::String &value, + const Type type) const + -> std::optional; + +#ifndef DOXYGEN + // This method is considered private. We only expose it for testing purposes + auto remove_oldest() -> void; +#endif + +private: +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + std::uint64_t byte_size_{0}; + using Entry = std::pair; + std::map data_; + std::map> order_; +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif +}; + +} // namespace sourcemeta::blaze + +#endif +#endif diff --git a/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_encoding.h b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_encoding.h new file mode 100644 index 000000000..39b6cc8f4 --- /dev/null +++ b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_encoding.h @@ -0,0 +1,1121 @@ +#ifndef SOURCEMETA_BLAZE_BINPACK_ENCODING_H_ +#define SOURCEMETA_BLAZE_BINPACK_ENCODING_H_ + +#include +#include + +#include // std::int64_t, std::uint64_t +#include // std::shared_ptr +#include // std::variant +#include // std::vector + +namespace sourcemeta::blaze { + +// Forward declarations for the sole purpose of being bale to define circular +// structures +#ifndef DOXYGEN +// The encoding names are the ones the JSON BinPack specification +// defines, so they keep their casing rather than following the C++ +// naming convention +// NOLINTBEGIN(readability-identifier-naming) +struct BOUNDED_MULTIPLE_8BITS_ENUM_FIXED; +struct FLOOR_MULTIPLE_ENUM_VARINT; +struct ROOF_MULTIPLE_MIRROR_ENUM_VARINT; +struct ARBITRARY_MULTIPLE_ZIGZAG_VARINT; +struct DOUBLE_VARINT_TUPLE; +struct NUMBER_VARINT_TUPLE; +struct BYTE_CHOICE_INDEX; +struct LARGE_CHOICE_INDEX; +struct TOP_LEVEL_BYTE_CHOICE_INDEX; +struct CONST_NONE; +struct ANY_PACKED_TYPE_TAG_BYTE_PREFIX; +struct UTF8_STRING_NO_LENGTH; +struct FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED; +struct ROOF_VARINT_PREFIX_UTF8_STRING_SHARED; +struct BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED; +struct RFC3339_DATE_INTEGER_TRIPLET; +struct PREFIX_VARINT_LENGTH_STRING_SHARED; +struct FIXED_TYPED_ARRAY; +struct BOUNDED_8BITS_TYPED_ARRAY; +struct FLOOR_TYPED_ARRAY; +struct ROOF_TYPED_ARRAY; +struct FIXED_TYPED_ARBITRARY_OBJECT; +struct VARINT_TYPED_ARBITRARY_OBJECT; +// NOLINTEND(readability-identifier-naming) +#endif + +/// @ingroup binpack +/// Represents an encoding +using BinPackEncoding = std::variant< + BOUNDED_MULTIPLE_8BITS_ENUM_FIXED, FLOOR_MULTIPLE_ENUM_VARINT, + ROOF_MULTIPLE_MIRROR_ENUM_VARINT, ARBITRARY_MULTIPLE_ZIGZAG_VARINT, + DOUBLE_VARINT_TUPLE, BYTE_CHOICE_INDEX, LARGE_CHOICE_INDEX, + TOP_LEVEL_BYTE_CHOICE_INDEX, CONST_NONE, ANY_PACKED_TYPE_TAG_BYTE_PREFIX, + UTF8_STRING_NO_LENGTH, FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED, + ROOF_VARINT_PREFIX_UTF8_STRING_SHARED, + BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED, RFC3339_DATE_INTEGER_TRIPLET, + PREFIX_VARINT_LENGTH_STRING_SHARED, FIXED_TYPED_ARRAY, + BOUNDED_8BITS_TYPED_ARRAY, FLOOR_TYPED_ARRAY, ROOF_TYPED_ARRAY, + FIXED_TYPED_ARBITRARY_OBJECT, VARINT_TYPED_ARBITRARY_OBJECT, + NUMBER_VARINT_TUPLE>; + +// The encoding names are the ones the JSON BinPack specification +// defines, so they keep their casing rather than following the C++ +// naming convention +// NOLINTBEGIN(readability-identifier-naming) + +/// @ingroup binpack +/// @defgroup encoding_integer Integer Encodings +/// @{ + +// clang-format off +/// @brief The encoding consists of the integer value divided by the +/// `multiplier`, minus the ceil of `minimum` divided by the `multiplier`, +/// encoded as an 8-bit fixed-length unsigned integer. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |--------------|--------|-----------------------------| +/// | `minimum` | `int` | The inclusive minimum value | +/// | `maximum` | `int` | The inclusive maximum value | +/// | `multiplier` | `uint` | The multiplier value | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |------------------------------|---------------------------------------------------------------------| +/// | `value >= minimum` | The input value must be greater than or equal to the minimum | +/// | `value <= maximum` | The input value must be less than or equal to the maximum | +/// | `value % multiplier == 0` | The input value must be divisible by the multiplier | +/// | `floor(maximum / multiplier) - ceil(minimum / multiplier) < 2 ** 8` | The divided range must be representable in 8 bits | +/// +/// ### Examples +/// +/// Given the input value 15, where the minimum is 1, the maximum is 19, and the +/// multiplier is 5, the encoding results in the 8-bit unsigned integer 2: +/// +/// ``` +/// +------+ +/// | 0x02 | +/// +------+ +/// ``` +// clang-format on +struct BOUNDED_MULTIPLE_8BITS_ENUM_FIXED { + /// The inclusive minimum value + std::int64_t minimum; + /// The inclusive maximum value + std::int64_t maximum; + /// The multiplier value + std::uint64_t multiplier; +}; + +// clang-format off +/// @brief The encoding consists of the integer value divided by the +/// `multiplier`, minus the ceil of `minimum` divided by the `multiplier`, +/// encoded as a Base-128 64-bit Little Endian variable-length unsigned +/// integer. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |--------------|--------|-----------------------------| +/// | `minimum` | `int` | The inclusive minimum value | +/// | `multiplier` | `uint` | The multiplier value | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |---------------------------|--------------------------------------------------------------| +/// | `value >= minimum` | The input value must be greater than or equal to the minimum | +/// | `value % multiplier == 0` | The input value must be divisible by the multiplier | +/// +/// ### Examples +/// +/// Given the input value 1000, where the minimum is -2 and the multiplier is 4, +/// the encoding results in the Base-128 64-bit Little Endian variable-length +/// unsigned integer 250: +/// +/// ``` +/// +------+------+ +/// | 0xfa | 0x01 | +/// +------+------+ +/// ``` +// clang-format on +struct FLOOR_MULTIPLE_ENUM_VARINT { + /// The inclusive minimum value + std::int64_t minimum; + /// The multiplier value + std::uint64_t multiplier; +}; + +// clang-format off +/// @brief The encoding consists of the floor of `maximum` divided by the +/// `multiplier`, minus the integer value divided by the `multiplier`, encoded +/// as a Base-128 64-bit Little Endian variable-length unsigned integer. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |--------------|--------|-----------------------------| +/// | `maximum` | `int` | The inclusive maximum value | +/// | `multiplier` | `uint` | The multiplier value | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |---------------------------|-----------------------------------------------------------| +/// | `value <= maximum` | The input value must be less than or equal to the maximum | +/// | `value % multiplier == 0` | The input value must be divisible by the multiplier | +/// +/// ### Examples +/// +/// Given the input value 5, where the maximum is 16 and the multiplier is 5, the +/// encoding results in the Base-128 64-bit Little Endian variable-length unsigned +/// integer 2: +/// +/// ``` +/// +------+ +/// | 0x02 | +/// +------+ +/// ``` +// clang-format on +struct ROOF_MULTIPLE_MIRROR_ENUM_VARINT { + /// The inclusive maximum value + std::int64_t maximum; + /// The multiplier value + std::uint64_t multiplier; +}; + +// clang-format off +/// @brief The encoding consists of the the integer value divided by the +/// `multiplier` encoded as a ZigZag-encoded Base-128 64-bit Little Endian +/// variable-length unsigned integer. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |--------------|--------|----------------------| +/// | `multiplier` | `uint` | The multiplier value | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |---------------------------|-----------------------------------------------------| +/// | `value % multiplier == 0` | The input value must be divisible by the multiplier | +/// +/// ### Examples +/// +/// Given the input value 10, where the multiplier is 5, the encoding results in +/// the Base-128 64-bit Little Endian variable-length unsigned integer 4: +/// +/// ``` +/// +------+ +/// | 0x04 | +/// +------+ +/// ``` +// clang-format on +struct ARBITRARY_MULTIPLE_ZIGZAG_VARINT { + /// The multiplier value + std::uint64_t multiplier; +}; + +/// @} + +/// @ingroup binpack +/// @defgroup encoding_number Number Encodings +/// @{ + +// clang-format off +/// @brief The encoding consists of a sequence of two integers: The signed +/// integer that results from concatenating the integral part and the decimal +/// part of the number, if any, as a ZigZag-encoded Base-128 64-bit Little +/// Endian variable-length unsigned integer; and the position of the decimal +/// mark from the last digit of the number encoded as a Base-128 64-bit Little +/// Endian variable-length unsigned integer. +/// +/// ### Options +/// +/// None +/// +/// ### Conditions +/// +/// None +/// +/// ### Examples +/// +/// Given the input value 3.14, the encoding results in the variable-length integer +/// 628 (the ZigZag encoding of 314) followed by the variable-length unsigned +/// integer 2 (the number of decimal digits in the number). +/// +/// ``` +/// +------+------+------+ +/// | 0xf4 | 0x04 | 0x02 | +/// +------+------+------+ +/// ``` +/// +/// Real numbers that represent integers are encoded with a decimal mark of zero. +/// Given the input value -5.0, the encoding results in the variable-length integer +/// 9 (the ZigZag encoding of -5) followed by the variable-length unsigned integer +/// 0. +/// +/// ``` +/// +------+------+ +/// | 0x09 | 0x00 | +/// +------+------+ +/// ``` +// clang-format on +struct DOUBLE_VARINT_TUPLE {}; + +// clang-format off +/// @brief The encoding consists of a sequence of two integers. The first is +/// the signed coefficient of the number as a ZigZag-encoded Base-128 64-bit +/// Little Endian variable-length unsigned integer. The second is the base-10 +/// exponent that the coefficient is scaled by, ZigZag-encoded and shifted left +/// by one bit, whose least significant bit is set when the number is an +/// arbitrary-precision decimal rather than a double, as a Base-128 64-bit +/// Little Endian variable-length unsigned integer. +/// +/// Unlike DOUBLE_VARINT_TUPLE, this encoding covers both of the numeric +/// representations that a JSON document may hold, and the schema-driven mode +/// relies on that: a schema that permits a number cannot tell which of the two +/// an instance will carry, so the distinction travels with the data. The +/// exponent is signed so that magnitudes a decimal mark cannot reach, such as +/// 1e300, remain within the encoding. +/// +/// ### Options +/// +/// None +/// +/// ### Conditions +/// +/// The coefficient must fit in a signed 64-bit integer, which holds every +/// number of up to 19 significant digits +/// +/// ### Examples +/// +/// Given the decimal input value 2.1, the encoding results in the +/// variable-length integer 42 (the ZigZag encoding of 21) followed by the +/// variable-length unsigned integer 3 (the ZigZag encoding of the exponent -1, +/// shifted left by one bit, with the decimal bit set). +/// +/// ``` +/// +------+------+ +/// | 0x2a | 0x03 | +/// +------+------+ +/// ``` +/// +/// Given the double input value 3.14, the encoding results in the +/// variable-length integer 628 (the ZigZag encoding of 314) followed by the +/// variable-length unsigned integer 6 (the ZigZag encoding of the exponent -2, +/// shifted left by one bit, with the decimal bit clear). +/// +/// ``` +/// +------+------+------+ +/// | 0xf4 | 0x04 | 0x06 | +/// +------+------+------+ +/// ``` +// clang-format on +struct NUMBER_VARINT_TUPLE {}; + +/// @} + +/// @ingroup binpack +/// @defgroup encoding_any Any Encodings +/// @{ + +// clang-format off +/// @brief The encoding consists of an index to the enumeration choices encoded +/// as an 8-bit fixed-length unsigned integer. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |-----------|---------|--------------------------| +/// | `choices` | `any[]` | The set of choice values | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |--------------------------|--------------------------------------------------------| +/// | `len(choices) > 0` | The choices array must not be empty | +/// | `len(choices) < 2 ** 8` | The number of choices must be representable in 8 bits | +/// | `value in choices` | The input value must be included in the set of choices | +/// +/// ### Examples +/// +/// Given an enumeration `[ "foo", "bar", "baz" ]` and an input value `"bar"`, the +/// encoding results in the unsigned 8 bit integer 1: +/// +/// ``` +/// +------+ +/// | 0x01 | +/// +------+ +/// ``` +/// +/// Given an enumeration `[ "foo", "bar", "baz" ]` and an input value `"foo"`, the +/// encoding results in the unsigned 8 bit integer 0: +/// +/// ``` +/// +------+ +/// | 0x00 | +/// +------+ +/// ``` +// clang-format on +struct BYTE_CHOICE_INDEX { + /// The set of choice values + std::vector choices; +}; + +// clang-format off +/// @brief The encoding consists of an index to the enumeration choices encoded +/// as a Base-128 64-bit Little Endian variable-length unsigned integer. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |-----------|---------|--------------------------| +/// | `choices` | `any[]` | The set of choice values | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |--------------------------|--------------------------------------------------------| +/// | `len(choices) > 0` | The choices array must not be empty | +/// | `value in choices` | The input value must be included in the set of choices | +/// +/// ### Examples +/// +/// Given an enumeration with 1000 members and an input value that equals the 300th +/// enumeration value, the encoding results in the Base-128 64-bit Little Endian +/// variable-length unsigned integer 300: +/// +/// ``` +/// +------+------+ +/// | 0xac | 0x02 | +/// +------+------+ +/// ``` +// clang-format on +struct LARGE_CHOICE_INDEX { + /// The set of choice values + std::vector choices; +}; + +// clang-format off +/// @brief If the input value corresponds to the index 0 to the enumeration +/// choices, the encoding stores no data. Otherwise, the encoding consists of +/// an index to the enumeration choices minus 1 encoded as an 8-bit +/// fixed-length unsigned integer. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |-----------|---------|--------------------------| +/// | `choices` | `any[]` | The set of choice values | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |--------------------------|--------------------------------------------------------| +/// | `len(choices) > 0` | The choices array must not be empty | +/// | `len(choices) < 2 ** 8` | The number of choices must be representable in 8 bits | +/// | `value in choices` | The input value must be included in the set of choices | +/// +/// ### Examples +/// +/// Given an enumeration `[ "foo", "bar", "baz" ]` and an input value `"bar"`, the +/// encoding results in the unsigned 8 bit integer 0: +/// +/// ``` +/// +------+ +/// | 0x00 | +/// +------+ +/// ``` +/// +/// Given an enumeration `[ "foo", "bar", "baz" ]` and an input value `"foo"`, the +/// value is not encoded. +// clang-format on +struct TOP_LEVEL_BYTE_CHOICE_INDEX { + /// The set of choice values + std::vector choices; +}; + +// clang-format off +/// @brief The constant input value is not encoded. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |---------|-------|--------------------| +/// | `value` | `any` | The constant value | +/// +/// ### Conditions +/// +/// None +/// +/// ### Examples +/// +/// The input value that matches the `value` option is not encoded. +// clang-format on +struct CONST_NONE { + /// The constant value + sourcemeta::core::JSON value; +}; + +// TODO: Write brief description +struct ANY_PACKED_TYPE_TAG_BYTE_PREFIX {}; +#ifndef DOXYGEN +namespace any_packed_type_tag_byte_prefix { +constexpr auto TYPE_SIZE = 3; +constexpr std::uint8_t TYPE_SHARED_STRING = 0b00000000; +constexpr std::uint8_t TYPE_STRING = 0b00000001; +constexpr std::uint8_t TYPE_LONG_STRING = 0b00000010; +constexpr std::uint8_t TYPE_OBJECT = 0b00000011; +constexpr std::uint8_t TYPE_ARRAY = 0b00000100; +constexpr std::uint8_t TYPE_POSITIVE_INTEGER_BYTE = 0b00000101; +constexpr std::uint8_t TYPE_NEGATIVE_INTEGER_BYTE = 0b00000110; +constexpr std::uint8_t TYPE_OTHER = 0b00000111; +static_assert(TYPE_SHARED_STRING <= sourcemeta::core::uint_max); +static_assert(TYPE_STRING <= sourcemeta::core::uint_max); +static_assert(TYPE_LONG_STRING <= sourcemeta::core::uint_max); +static_assert(TYPE_OBJECT <= sourcemeta::core::uint_max); +static_assert(TYPE_ARRAY <= sourcemeta::core::uint_max); +static_assert(TYPE_POSITIVE_INTEGER_BYTE <= + sourcemeta::core::uint_max); +static_assert(TYPE_NEGATIVE_INTEGER_BYTE <= + sourcemeta::core::uint_max); +static_assert(TYPE_OTHER <= sourcemeta::core::uint_max); + +constexpr auto SUBTYPE_SIZE = 5; +constexpr std::uint8_t SUBTYPE_FALSE = 0b00000000; +constexpr std::uint8_t SUBTYPE_TRUE = 0b00000001; +constexpr std::uint8_t SUBTYPE_NULL = 0b00000010; +constexpr std::uint8_t SUBTYPE_POSITIVE_INTEGER = 0b00000011; +constexpr std::uint8_t SUBTYPE_NEGATIVE_INTEGER = 0b00000100; +constexpr std::uint8_t SUBTYPE_NUMBER = 0b00000101; +constexpr std::uint8_t SUBTYPE_POSITIVE_REAL_INTEGER_BYTE = 0b00000110; +constexpr std::uint8_t SUBTYPE_LONG_STRING_BASE_EXPONENT_7 = 0b00000111; +constexpr std::uint8_t SUBTYPE_LONG_STRING_BASE_EXPONENT_8 = 0b00001000; +constexpr std::uint8_t SUBTYPE_LONG_STRING_BASE_EXPONENT_9 = 0b00001001; +constexpr std::uint8_t SUBTYPE_LONG_STRING_BASE_EXPONENT_10 = 0b00001010; +constexpr std::uint8_t SUBTYPE_DECIMAL = 0b00001011; + +static_assert(SUBTYPE_FALSE <= sourcemeta::core::uint_max); +static_assert(SUBTYPE_TRUE <= sourcemeta::core::uint_max); +static_assert(SUBTYPE_NULL <= sourcemeta::core::uint_max); +static_assert(SUBTYPE_POSITIVE_INTEGER <= + sourcemeta::core::uint_max); +static_assert(SUBTYPE_NEGATIVE_INTEGER <= + sourcemeta::core::uint_max); +static_assert(SUBTYPE_NUMBER <= sourcemeta::core::uint_max); +static_assert(SUBTYPE_POSITIVE_REAL_INTEGER_BYTE <= + sourcemeta::core::uint_max); +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_7 <= + sourcemeta::core::uint_max); +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_8 <= + sourcemeta::core::uint_max); +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_9 <= + sourcemeta::core::uint_max); +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_10 <= + sourcemeta::core::uint_max); +static_assert(SUBTYPE_DECIMAL <= sourcemeta::core::uint_max); + +// Note that the binary values actually match the declared exponents +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_7 == 7); +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_8 == 8); +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_9 == 9); +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_10 == 10); +} // namespace any_packed_type_tag_byte_prefix +#endif + +/// @} + +/// @ingroup binpack +/// @defgroup encoding_string String Encodings +/// @{ + +// clang-format off +/// @brief The encoding consist in the UTF-8 encoding of the input string. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |--------|--------|------------------------------| +/// | `size` | `uint` | The string UTF-8 byte-length | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |----------------------|-----------------------------------------------------------| +/// | `len(value) == size` | The input string must have the declared UTF-8 byte-length | +/// +/// ### Examples +/// +/// Given the input value "foo bar" with a corresponding size of 7, the encoding +/// results in: +/// +/// ``` +/// +------+------+------+------+------+------+------+ +/// | 0x66 | 0x6f | 0x6f | 0x20 | 0x62 | 0x61 | 0x72 | +/// +------+------+------+------+------+------+------+ +/// f o o b a r +/// ``` +// clang-format on +struct UTF8_STRING_NO_LENGTH { + /// The string UTF-8 byte-length + std::uint64_t size; +}; + +// clang-format off +/// @brief The encoding consists of the byte-length of the string minus +/// `minimum` plus 1 as a Base-128 64-bit Little Endian variable-length +/// unsigned integer followed by the UTF-8 encoding of the input value. +/// +/// Optionally, if the input string has already been encoded to the buffer +/// using UTF-8, the encoding may consist of the byte constant `0x00` followed +/// by the byte-length of the string minus `minimum` plus 1 as a Base-128 +/// 64-bit Little Endian variable-length unsigned integer, followed by the +/// current offset minus the offset to the start of the UTF-8 string value in +/// the buffer encoded as a Base-128 64-bit Little Endian variable-length +/// unsigned integer. +/// +/// #### Options +/// +/// | Option | Type | Description | +/// |-----------|--------|------------------------------------------------| +/// | `minimum` | `uint` | The inclusive minimum string UTF-8 byte-length | +/// +/// #### Conditions +/// +/// | Condition | Description | +/// |-------------------------|----------------------------------------------------------------------| +/// | `len(value) >= minimum` | The input string byte-length is equal to or greater than the minimum | +/// +/// #### Examples +/// +/// Given the input string `foo` with a minimum 3 where the string has not been +/// previously encoded, the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0x01 | 0x66 | 0x6f | 0x6f | +/// +------+------+------+------+ +/// f o o +/// ``` +/// +/// Given the encoding of `foo` with a minimum of 0 followed by the encoding of +/// `foo` with a minimum of 3, the encoding may result in: +/// +/// ``` +/// 0 1 2 3 4 5 6 +/// ^ ^ ^ ^ ^ ^ ^ +/// +------+------+------+------+------+------+------+ +/// | 0x04 | 0x66 | 0x6f | 0x6f | 0x00 | 0x01 | 0x05 | +/// +------+------+------+------+------+------+------+ +/// f o o 6 - 1 +/// ``` +// clang-format on +struct FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED { + /// The inclusive minimum string UTF-8 byte-length + std::uint64_t minimum; +}; + +// clang-format off +/// @brief The encoding consists of `maximum` minus the byte-length of the +/// string plus 1 as a Base-128 64-bit Little Endian variable-length unsigned +/// integer followed by the UTF-8 encoding of the input value. +/// +/// Optionally, if the input string has already been encoded to the buffer +/// using UTF-8, the encoding may consist of the byte constant `0x00` followed +/// by `maximum` minus the byte-length of the string plus 1 as a Base-128 +/// 64-bit Little Endian variable-length unsigned integer, followed by the +/// current offset minus the offset to the start of the UTF-8 string value in +/// the buffer encoded as a Base-128 64-bit Little Endian variable-length +/// unsigned integer. +/// +/// #### Options +/// +/// | Option | Type | Description | +/// |-----------|--------|------------------------------------------------| +/// | `maximum` | `uint` | The inclusive maximum string UTF-8 byte-length | +/// +/// #### Conditions +/// +/// | Condition | Description | +/// |-------------------------|-------------------------------------------------------------------| +/// | `len(value) <= maximum` | The input string byte-length is equal to or less than the maximum | +/// +/// #### Examples +/// +/// Given the input string `foo` with a maximum 4 where the string has not been +/// previously encoded, the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0x02 | 0x66 | 0x6f | 0x6f | +/// +------+------+------+------+ +/// f o o +/// ``` +/// +/// Given the encoding of `foo` with a maximum of 3 followed by the encoding of +/// `foo` with a maximum of 5, the encoding may result in: +/// +/// ``` +/// 0 1 2 3 4 5 6 +/// ^ ^ ^ ^ ^ ^ ^ +/// +------+------+------+------+------+------+------+ +/// | 0x01 | 0x66 | 0x6f | 0x6f | 0x00 | 0x03 | 0x05 | +/// +------+------+------+------+------+------+------+ +/// f o o 6 - 1 +/// ``` +// clang-format on +struct ROOF_VARINT_PREFIX_UTF8_STRING_SHARED { + /// The inclusive maximum string UTF-8 byte-length + std::uint64_t maximum; +}; + +// clang-format off +/// @brief The encoding consists of the byte-length of the string minus +/// `minimum` plus 1 as an 8-bit fixed-length unsigned integer followed by the +/// UTF-8 encoding of the input value. +/// +/// Optionally, if the input string has already been encoded to the buffer +/// using UTF-8, the encoding may consist of the byte constant `0x00` followed +/// by the byte-length of the string minus `minimum` plus 1 as an 8-bit +/// fixed-length unsigned integer, followed by the current offset minus the +/// offset to the start of the UTF-8 string value in the buffer encoded as a +/// Base-128 64-bit Little Endian variable-length unsigned integer. +/// +/// The byte-length of the string is encoded even if `maximum` equals `minimum` +/// in order to disambiguate between shared and non-shared fixed strings. +/// +/// #### Options +/// +/// | Option | Type | Description | +/// |-----------|--------|------------------------------------------------| +/// | `minimum` | `uint` | The inclusive minimum string UTF-8 byte-length | +/// | `maximum` | `uint` | The inclusive maximum string UTF-8 byte-length | +/// +/// #### Conditions +/// +/// | Condition | Description | +/// |----------------------------------|----------------------------------------------------------------------| +/// | `len(value) >= minimum` | The input string byte-length is equal to or greater than the minimum | +/// | `len(value) <= maximum` | The input string byte-length is equal to or less than the maximum | +/// | `maximum - minimum < 2 ** 8 - 1` | The range minus 1 must be representable in 8 bits | +/// +/// #### Examples +/// +/// Given the input string `foo` with a minimum 3 and a maximum 5 where the string +/// has not been previously encoded, the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0x01 | 0x66 | 0x6f | 0x6f | +/// +------+------+------+------+ +/// f o o +/// ``` +/// +/// Given the encoding of `foo` with a minimum of 0 and a maximum of 6 followed by +/// the encoding of `foo` with a minimum of 3 and a maximum of 100, the encoding +/// may result in: +/// +/// ``` +/// 0 1 2 3 4 5 6 +/// ^ ^ ^ ^ ^ ^ ^ +/// +------+------+------+------+------+------+------+ +/// | 0x04 | 0x66 | 0x6f | 0x6f | 0x00 | 0x01 | 0x05 | +/// +------+------+------+------+------+------+------+ +/// f o o 6 - 1 +/// ``` +// clang-format on +struct BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED { + /// The inclusive minimum string UTF-8 byte-length + std::uint64_t minimum; + /// The inclusive maximum string UTF-8 byte-length + std::uint64_t maximum; +}; + +// clang-format off +/// @brief The encoding consists of an implementation of +/// [RFC3339](https://datatracker.ietf.org/doc/html/rfc3339) date expressions +/// as the sequence of 3 integers: the year as a 16-bit fixed-length Little +/// Endian unsigned integer, the month as an 8-bit fixed-length unsigned +/// integer, and the day as an 8-bit fixed-length unsigned integer. +/// +/// #### Options +/// +/// None +/// +/// #### Conditions +/// +/// | Condition | Description | +/// |----------------------|-------------------------------------------------------------| +/// | `len(value) == 10` | The input string consists of 10 characters | +/// | `value[0:4] >= 0` | The year is greater than or equal to 0 | +/// | `value[0:4] <= 9999` | The year is less than or equal to 9999 as stated by RFC3339 | +/// | `value[4] == "-"` | The year and the month are divided by a hyphen | +/// | `value[5:7] >= 1` | The month is greater than or equal to 1 | +/// | `value[5:7] <= 12` | The month is less than or equal to 12 | +/// | `value[7] == "-"` | The month and the day are divided by a hyphen | +/// | `value[8:10] >= 1` | The day is greater than or equal to 1 | +/// | `value[8:10] <= 31` | The day is less than or equal to 31 | +/// +/// #### Examples +/// +/// Given the input string `2014-10-01`, the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0xde | 0x07 | 0x0a | 0x01 | +/// +------+------+------+------+ +/// year ... month day +/// ``` +// clang-format on +struct RFC3339_DATE_INTEGER_TRIPLET {}; + +// clang-format off +/// @brief The encoding consists of the byte-length of the string plus 1 as a +/// Base-128 64-bit Little Endian variable-length unsigned integer followed by +/// the UTF-8 encoding of the input value. +/// +/// Optionally, if the input string has already been encoded to the buffer +/// using this encoding the encoding may consist of the byte constant `0x00` +/// followed by the current offset minus the offset to the start of the string +/// as a Base-128 64-bit Little Endian variable-length unsigned integer. It is +/// permissible to point to another instance of the string that is a pointer +/// itself. +/// +/// ### Options +/// +/// None +/// +/// ### Conditions +/// +/// None +/// +/// ### Examples +/// +/// Given the input string `foo` where the string has not been previously encoded, +/// the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0x04 | 0x66 | 0x6f | 0x6f | +/// +------+------+------+------+ +/// f o o +/// ``` +/// +/// Given the encoding of `foo` repeated 3 times, the encoding may result in: +/// +/// ``` +/// 0 1 2 3 4 5 6 7 +/// ^ ^ ^ ^ ^ ^ ^ ^ +/// +------+------+------+------+------+------+------+------+ +/// | 0x04 | 0x66 | 0x6f | 0x6f | 0x00 | 0x05 | 0x00 | 0x03 | +/// +------+------+------+------+------+------+------+------+ +/// f o o 5 - 0 7 - 4 +/// ``` +// clang-format on +struct PREFIX_VARINT_LENGTH_STRING_SHARED {}; + +/// @} + +/// @ingroup binpack +/// @defgroup encoding_array Array Encodings +/// @{ + +// clang-format off +/// @brief The encoding consists of the elements of the fixed array encoded in +/// order. The encoding of the element at index `i` is either +/// `prefix_encodings[i]` if set, or `encoding`. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |-------------------|--------------|----------------------| +/// | `size` | `uint` | The array length | +/// | `prefixEncodings` | `encoding[]` | Positional encodings | +/// | `encoding` | `encoding` | Element encoding | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |--------------------------------|-----------------------------------------------------------------------| +/// | `len(prefixEncodings) <= size` | The number of prefix encodings must be less than or equal to the size | +/// | `len(value) == size` | The input array must have the declared size | +/// +/// ### Examples +/// +/// Given the array `[ 1, 2, true ]` where the `prefixEncodings` corresponds to +/// BOUNDED_MULTIPLE_8BITS_ENUM_FIXED (minimum 0, maximum 10, multiplier 1) and +/// BOUNDED_MULTIPLE_8BITS_ENUM_FIXED (minimum 0, maximum 10, multiplier 1) and +/// `encoding` corresponds to BYTE_CHOICE_INDEX with choices `[ false, true ]`, +/// the encoding results in: +/// +/// ``` +/// +------+------+------+ +/// | 0x00 | 0x01 | 0x01 | +/// +------+------+------+ +/// 1 2 true +/// ``` +// clang-format on +struct FIXED_TYPED_ARRAY { + /// The array length + std::uint64_t size; + /// Element encoding + std::shared_ptr encoding; + /// Positional encodings + std::vector prefix_encodings; +}; + +// clang-format off +/// @brief The encoding consists of the length of the array minus `minimum` +/// encoded as an 8-bit fixed-length unsigned integer followed by the elements +/// of the array encoded in order. The encoding of the element at index `i` is +/// either `prefix_encodings[i]` if set, or `encoding`. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |-------------------|--------------|---------------------------------| +/// | `minimum` | `uint` | The minimum length of the array | +/// | `maximum` | `uint` | The maximum length of the array | +/// | `prefixEncodings` | `encoding[]` | Positional encodings | +/// | `encoding` | `encoding` | Element encoding | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |----------------------------------------|---------------------------------------------------------------------------------------| +/// | `len(value) >= minimum` | The length of the array must be greater than or equal to the minimum | +/// | `len(value) <= maximum` | The length of the array must be less than or equal to the maximum | +/// | `len(prefixEncodings) <= maximum` | The number of prefix encodings must be less than or equal to the maximum array length | +/// | `len(maximum) - len(minimum) < 2 ** 8` | The array length must be representable in 8 bits | +/// +/// ### Examples +/// +/// Given the array `[ true, false, 5 ]` where the minimum is 1 and the maximum is +/// 3, the `prefixEncodings` corresponds to BYTE_CHOICE_INDEX with +/// choices `[ false, true ]` and BYTE_CHOICE_INDEX with choices `[ +/// false, true ]` and `encoding` corresponds to +/// BOUNDED_MULTIPLE_8BITS_ENUM_FIXED with minimum 0 and maximum +/// 255, the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0x02 | 0x01 | 0x00 | 0x05 | +/// +------+------+------+------+ +/// size true false 5 +/// ``` +// clang-format on +struct BOUNDED_8BITS_TYPED_ARRAY { + /// The minimum length of the array + std::uint64_t minimum; + /// The maximum length of the array + std::uint64_t maximum; + /// Element encoding + std::shared_ptr encoding; + /// Positional encodings + std::vector prefix_encodings; +}; + +// clang-format off +/// @brief The encoding consists of the length of the array minus `minimum` +/// encoded as a Base-128 64-bit Little Endian variable-length unsigned integer +/// followed by the elements of the array encoded in order. The encoding of the +/// element at index `i` is either `prefix_encodings[i]` if set, or `encoding`. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |-------------------|--------------|---------------------------------| +/// | `minimum` | `uint` | The minimum length of the array | +/// | `prefixEncodings` | `encoding[]` | Positional encodings | +/// | `encoding` | `encoding` | Element encoding | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |-------------------------|----------------------------------------------------------------------| +/// | `len(value) >= minimum` | The length of the array must be greater than or equal to the minimum | +/// +/// ### Examples +/// +/// TODO: Give an example of an array with more than 8-bit of elements +/// +/// Given the array `[ true, false, 5 ]` where the minimum is 1, the +/// `prefixEncodings` corresponds to BYTE_CHOICE_INDEX with choices `[ +/// false, true ]` and BYTE_CHOICE_INDEX with choices `[ false, true ]` +/// and `encoding` corresponds to BOUNDED_MULTIPLE_8BITS_ENUM_FIXED +/// with minimum 0 and maximum 255, the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0x02 | 0x01 | 0x00 | 0x05 | +/// +------+------+------+------+ +/// size true false 5 +/// ``` +// clang-format on +struct FLOOR_TYPED_ARRAY { + /// The minimum length of the array + std::uint64_t minimum; + /// Element encoding + std::shared_ptr encoding; + /// Positional encodings + std::vector prefix_encodings; +}; + +// clang-format off +/// @brief The encoding consists of `maximum` minus the length of the array +/// encoded as a Base-128 64-bit Little Endian variable-length unsigned integer +/// followed by the elements of the array encoded in order. The encoding of the +/// element at index `i` is either `prefix_encodings[i]` if set, or `encoding`. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |-------------------|--------------|---------------------------------| +/// | `maximum` | `uint` | The maximum length of the array | +/// | `prefixEncodings` | `encoding[]` | Positional encodings | +/// | `encoding` | `encoding` | Element encoding | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |-----------------------------------|---------------------------------------------------------------------------------------| +/// | `len(prefixEncodings) <= maximum` | The number of prefix encodings must be less than or equal to the maximum array length | +/// +/// ### Examples +/// +/// Given the array `[ true, false, 5 ]` where the maximum is 3, the +/// `prefixEncodings` options corresponds to BYTE_CHOICE_INDEX with +/// choices `[ false, true ]` and BYTE_CHOICE_INDEX with choices `[ +/// false, true ]` and `encoding` corresponds to +/// BOUNDED_MULTIPLE_8BITS_ENUM_FIXED with minimum 0 and maximum +/// 255, the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0x00 | 0x01 | 0x00 | 0x05 | +/// +------+------+------+------+ +/// size true false 5 +/// ``` +// clang-format on +struct ROOF_TYPED_ARRAY { + /// The maximum length of the array + std::uint64_t maximum; + /// Element encoding + std::shared_ptr encoding; + /// Positional encodings + std::vector prefix_encodings; +}; + +/// @} + +/// @ingroup binpack +/// @defgroup encoding_object Object Encodings +/// @{ + +// clang-format off +/// @brief The encoding consists of each pair encoded as the key followed by +/// the value according to `key_encoding` and `encoding`. The order in which +/// pairs are encoded is undefined. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |---------------|------------|-----------------| +/// | `size` | `uint` | The object size | +/// | `keyEncoding` | `encoding` | Key encoding | +/// | `encoding` | `encoding` | Value encoding | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |----------------------|-----------------------------------------------------------| +/// | `len(value) == size` | The input object must have the declared amount of entries | +/// +/// ### Examples +/// +/// Given the array `{ "foo": 1, "bar": 2 }` where `keyEncoding` corresponds to +/// UTF8_STRING_NO_LENGTH (size 3) and `encoding` corresponds to +/// BOUNDED_MULTIPLE_8BITS_ENUM_FIXED (minimum 0, maximum 10, +/// multiplier 1), the encoding results in: +/// +/// ``` +/// +------+------+------+------+------+------+------+------+ +/// | 0x66 | 0x6f | 0x6f | 0x01 | 0x62 | 0x61 | 0x72 | 0x02 | +/// +------+------+------+------+------+------+------+------+ +/// f o o 1 b a r 2 +/// ``` +/// +/// Or: +/// +/// ``` +/// +------+------+------+------+------+------+------+------+ +/// | 0x62 | 0x61 | 0x72 | 0x02 | 0x66 | 0x6f | 0x6f | 0x01 | +/// +------+------+------+------+------+------+------+------+ +/// b a r 2 f o o 1 +/// ``` +// clang-format on +struct FIXED_TYPED_ARBITRARY_OBJECT { + /// The object size + std::uint64_t size; + /// Key encoding + std::shared_ptr key_encoding; + /// Value encoding + std::shared_ptr encoding; +}; + +// clang-format off +/// @brief The encoding consists of the number of key-value pairs in the input +/// object as a Base-128 64-bit Little Endian variable-length unsigned integer +/// followed by each pair encoded as the key followed by the value according to +/// `key_encoding` and `encoding`. The order in which pairs are encoded is +/// undefined. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |---------------|------------|----------------| +/// | `keyEncoding` | `encoding` | Key encoding | +/// | `encoding` | `encoding` | Value encoding | +/// +/// ### Examples +/// +/// Given the array `{ "foo": 1, "bar": 2 }` where `keyEncoding` corresponds to +/// UTF8_STRING_NO_LENGTH (size 3) and `encoding` corresponds to +/// BOUNDED_MULTIPLE_8BITS_ENUM_FIXED (minimum 0, maximum 10, +/// multiplier 1), the encoding results in: +/// +/// ``` +/// +------+------+------+------+------+------+------+------+------+ +/// | 0x02 | 0x66 | 0x6f | 0x6f | 0x01 | 0x62 | 0x61 | 0x72 | 0x02 | +/// +------+------+------+------+------+------+------+------+------+ +/// 2 f o o 1 b a r 2 +/// ``` +/// +/// Or: +/// +/// ``` +/// +------+------+------+------+------+------+------+------+------+ +/// | 0x02 | 0x62 | 0x61 | 0x72 | 0x02 | 0x66 | 0x6f | 0x6f | 0x01 | +/// +------+------+------+------+------+------+------+------+------+ +/// 2 b a r 2 f o o 1 +/// ``` +// clang-format on +struct VARINT_TYPED_ARBITRARY_OBJECT { + /// Key encoding + std::shared_ptr key_encoding; + /// Value encoding + std::shared_ptr encoding; +}; + +/// @} + +// NOLINTEND(readability-identifier-naming) + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_error.h b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_error.h new file mode 100644 index 000000000..881facf9c --- /dev/null +++ b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_error.h @@ -0,0 +1,48 @@ +#ifndef SOURCEMETA_BLAZE_BINPACK_ERROR_H_ +#define SOURCEMETA_BLAZE_BINPACK_ERROR_H_ + +#ifndef SOURCEMETA_BLAZE_BINPACK_EXPORT +#include +#endif + +#include // std::exception +#include // std::string +#include // std::string_view +#include // std::move + +namespace sourcemeta::blaze { + +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup binpack +/// An error that represents an encoding that the runtime does not implement +class SOURCEMETA_BLAZE_BINPACK_EXPORT BinPackEncodingError + : public std::exception { +public: + BinPackEncodingError(std::string name) : name_{std::move(name)} {} + + [[nodiscard]] auto what() const noexcept -> const char * override { + return "The encoding is not recognized"; + } + + /// The name of the encoding that could not be recognized + [[nodiscard]] auto name() const noexcept -> std::string_view { + return this->name_; + } + +private: + std::string name_; +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_input_stream.h b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_input_stream.h new file mode 100644 index 000000000..49877d7c7 --- /dev/null +++ b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_input_stream.h @@ -0,0 +1,35 @@ +#ifndef SOURCEMETA_BLAZE_BINPACK_INPUT_STREAM_H_ +#define SOURCEMETA_BLAZE_BINPACK_INPUT_STREAM_H_ + +#ifndef SOURCEMETA_BLAZE_BINPACK_EXPORT +#include +#endif + +#include +#include + +#include // std::uint64_t, std::int64_t +#include // std::basic_istream + +namespace sourcemeta::blaze { + +/// @ingroup binpack +class SOURCEMETA_BLAZE_BINPACK_EXPORT BinPackInputStream + : public sourcemeta::core::BinaryReader { +public: + using Stream = std::basic_istream; + BinPackInputStream(Stream &input); + + // Seek backwards given a relative offset + auto rewind(const std::uint64_t relative_offset, const std::uint64_t position) + -> std::uint64_t; + auto get_varint() -> std::uint64_t; + auto get_varint_zigzag() -> std::int64_t; + auto get_string_utf8(const std::uint64_t length) + -> sourcemeta::core::JSON::String; +}; + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_output_stream.h b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_output_stream.h new file mode 100644 index 000000000..2e7ffcdc1 --- /dev/null +++ b/vendor/blaze/src/binpack/include/sourcemeta/blaze/binpack_output_stream.h @@ -0,0 +1,32 @@ +#ifndef SOURCEMETA_BLAZE_BINPACK_OUTPUT_STREAM_H_ +#define SOURCEMETA_BLAZE_BINPACK_OUTPUT_STREAM_H_ + +#ifndef SOURCEMETA_BLAZE_BINPACK_EXPORT +#include +#endif + +#include +#include + +#include // std::uint64_t, std::int64_t +#include // std::basic_ostream + +namespace sourcemeta::blaze { + +/// @ingroup binpack +class SOURCEMETA_BLAZE_BINPACK_EXPORT BinPackOutputStream + : public sourcemeta::core::BinaryWriter { +public: + using Stream = std::basic_ostream; + BinPackOutputStream(Stream &output); + + auto put_varint(const std::uint64_t value) -> void; + auto put_varint_zigzag(const std::int64_t value) -> void; + auto put_string_utf8(const sourcemeta::core::JSON::String &string, + const std::uint64_t length) -> void; +}; + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/binpack/input_stream.cc b/vendor/blaze/src/binpack/input_stream.cc new file mode 100644 index 000000000..111d986c6 --- /dev/null +++ b/vendor/blaze/src/binpack/input_stream.cc @@ -0,0 +1,68 @@ +#include + +#include + +#include // assert +#include // std::size_t +#include // std::uint8_t, std::uint64_t, std::int64_t + +namespace sourcemeta::blaze { + +BinPackInputStream::BinPackInputStream(Stream &input) + : sourcemeta::core::BinaryReader{input} {} + +auto BinPackInputStream::rewind(const std::uint64_t relative_offset, + const std::uint64_t position) -> std::uint64_t { + assert(position >= relative_offset); + const std::uint64_t offset{position - relative_offset}; + assert(offset < position); + const std::uint64_t current{this->position()}; + this->seek(offset); + return current; +} + +auto BinPackInputStream::get_varint() -> std::uint64_t { + constexpr std::uint8_t LEAST_SIGNIFICANT_BITS{0b01111111}; + constexpr std::uint8_t MOST_SIGNIFICANT_BIT{0b10000000}; + constexpr std::uint8_t SHIFT{7}; + std::uint64_t result{0}; + std::size_t cursor{0}; + while (true) { + const std::uint8_t byte{this->get_byte()}; + const std::uint64_t value{ + static_cast(byte & LEAST_SIGNIFICANT_BITS)}; +#ifndef NDEBUG + const std::uint64_t current = result; +#endif + result += static_cast(value << SHIFT * cursor); + // Try to catch potential overflows from the above addition + assert(result >= current); + cursor += 1; + if ((byte & MOST_SIGNIFICANT_BIT) == 0) { + break; + } + } + + return result; +} + +auto BinPackInputStream::get_varint_zigzag() -> std::int64_t { + return sourcemeta::core::zigzag_decode(this->get_varint()); +} + +auto BinPackInputStream::get_string_utf8(const std::uint64_t length) + -> sourcemeta::core::JSON::String { + sourcemeta::core::JSON::String result; + result.reserve(length); + std::uint64_t counter = 0; + while (counter < length) { + result += static_cast(this->get_byte()); + counter += 1; + } + + assert(counter == length); + assert(result.size() == length); + return result; +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/loader.cc b/vendor/blaze/src/binpack/loader.cc new file mode 100644 index 000000000..c11d415d7 --- /dev/null +++ b/vendor/blaze/src/binpack/loader.cc @@ -0,0 +1,57 @@ +#include + +#include "loader_v1_any.h" +#include "loader_v1_array.h" +#include "loader_v1_integer.h" +#include "loader_v1_number.h" +#include "loader_v1_string.h" + +#include // assert + +namespace sourcemeta::blaze { + +auto binpack_load(const sourcemeta::core::JSON &input) -> BinPackEncoding { + assert(input.defines("binpackEncoding")); + assert(input.defines("binpackOptions")); + const auto encoding{input.at("binpackEncoding").to_string()}; + const auto &options{input.at("binpackOptions")}; + +#define PARSE_ENCODING(version, name) \ + if (encoding == #name) \ + return version::name(options); + + // Integers + PARSE_ENCODING(v1, BOUNDED_MULTIPLE_8BITS_ENUM_FIXED) + PARSE_ENCODING(v1, FLOOR_MULTIPLE_ENUM_VARINT) + PARSE_ENCODING(v1, ROOF_MULTIPLE_MIRROR_ENUM_VARINT) + PARSE_ENCODING(v1, ARBITRARY_MULTIPLE_ZIGZAG_VARINT) + // Numbers + PARSE_ENCODING(v1, DOUBLE_VARINT_TUPLE) + PARSE_ENCODING(v1, NUMBER_VARINT_TUPLE) + // Any + PARSE_ENCODING(v1, BYTE_CHOICE_INDEX) + PARSE_ENCODING(v1, LARGE_CHOICE_INDEX) + PARSE_ENCODING(v1, TOP_LEVEL_BYTE_CHOICE_INDEX) + PARSE_ENCODING(v1, CONST_NONE) + PARSE_ENCODING(v1, ANY_PACKED_TYPE_TAG_BYTE_PREFIX) + // Strings + PARSE_ENCODING(v1, UTF8_STRING_NO_LENGTH) + PARSE_ENCODING(v1, FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED) + PARSE_ENCODING(v1, ROOF_VARINT_PREFIX_UTF8_STRING_SHARED) + PARSE_ENCODING(v1, BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED) + PARSE_ENCODING(v1, RFC3339_DATE_INTEGER_TRIPLET) + PARSE_ENCODING(v1, PREFIX_VARINT_LENGTH_STRING_SHARED) + // Arrays + PARSE_ENCODING(v1, FIXED_TYPED_ARRAY) + PARSE_ENCODING(v1, BOUNDED_8BITS_TYPED_ARRAY) + PARSE_ENCODING(v1, FLOOR_TYPED_ARRAY) + PARSE_ENCODING(v1, ROOF_TYPED_ARRAY) + + // TODO: Handle object encodings + +#undef PARSE_ENCODING + + throw BinPackEncodingError(encoding); +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/loader_v1_any.h b/vendor/blaze/src/binpack/loader_v1_any.h new file mode 100644 index 000000000..604803fd7 --- /dev/null +++ b/vendor/blaze/src/binpack/loader_v1_any.h @@ -0,0 +1,62 @@ +#ifndef SOURCEMETA_BLAZE_BINPACK_LOADER_V1_ANY_H_ +#define SOURCEMETA_BLAZE_BINPACK_LOADER_V1_ANY_H_ + +#include + +#include + +#include // assert +#include // std::move +#include // std::vector + +namespace sourcemeta::blaze::v1 { + +// The encoding names are the ones the JSON BinPack specification defines, so +// they keep their casing rather than following the C++ naming convention +// NOLINTBEGIN(readability-identifier-naming) + +auto BYTE_CHOICE_INDEX(const sourcemeta::core::JSON &options) + -> BinPackEncoding { + assert(options.defines("choices")); + const auto &choices{options.at("choices")}; + assert(choices.is_array()); + const auto &array{choices.as_array()}; + std::vector elements{array.cbegin(), array.cend()}; + return sourcemeta::blaze::BYTE_CHOICE_INDEX{.choices = std::move(elements)}; +} + +auto LARGE_CHOICE_INDEX(const sourcemeta::core::JSON &options) + -> BinPackEncoding { + assert(options.defines("choices")); + const auto &choices{options.at("choices")}; + assert(choices.is_array()); + const auto &array{choices.as_array()}; + std::vector elements{array.cbegin(), array.cend()}; + return sourcemeta::blaze::LARGE_CHOICE_INDEX{.choices = std::move(elements)}; +} + +auto TOP_LEVEL_BYTE_CHOICE_INDEX(const sourcemeta::core::JSON &options) + -> BinPackEncoding { + assert(options.defines("choices")); + const auto &choices{options.at("choices")}; + assert(choices.is_array()); + const auto &array{choices.as_array()}; + std::vector elements{array.cbegin(), array.cend()}; + return sourcemeta::blaze::TOP_LEVEL_BYTE_CHOICE_INDEX{ + .choices = std::move(elements)}; +} + +auto CONST_NONE(const sourcemeta::core::JSON &options) -> BinPackEncoding { + assert(options.defines("value")); + return sourcemeta::blaze::CONST_NONE{.value = options.at("value")}; +} + +auto ANY_PACKED_TYPE_TAG_BYTE_PREFIX( + [[maybe_unused]] const sourcemeta::core::JSON &options) -> BinPackEncoding { + return sourcemeta::blaze::ANY_PACKED_TYPE_TAG_BYTE_PREFIX{}; +} + +// NOLINTEND(readability-identifier-naming) +} // namespace sourcemeta::blaze::v1 + +#endif diff --git a/vendor/blaze/src/binpack/loader_v1_array.h b/vendor/blaze/src/binpack/loader_v1_array.h new file mode 100644 index 000000000..7fadda59f --- /dev/null +++ b/vendor/blaze/src/binpack/loader_v1_array.h @@ -0,0 +1,137 @@ +#ifndef SOURCEMETA_BLAZE_BINPACK_LOADER_V1_ARRAY_H_ +#define SOURCEMETA_BLAZE_BINPACK_LOADER_V1_ARRAY_H_ + +#include + +#include + +#include // std::transform +#include // assert +#include // std::uint64_t +#include // std::back_inserter +#include // std::make_shared +#include // std::vector + +namespace sourcemeta::blaze::v1 { + +// The encoding names are the ones the JSON BinPack specification defines, so +// they keep their casing rather than following the C++ naming convention +// NOLINTBEGIN(readability-identifier-naming) + +auto FIXED_TYPED_ARRAY(const sourcemeta::core::JSON &options) + -> BinPackEncoding { + assert(options.defines("size")); + assert(options.defines("encoding")); + assert(options.defines("prefixEncodings")); + const auto &size{options.at("size")}; + const auto &array_encoding{options.at("encoding")}; + const auto &prefix_encodings{options.at("prefixEncodings")}; + assert(size.is_integer()); + assert(size.is_positive()); + assert(array_encoding.is_object()); + assert(prefix_encodings.is_array()); + std::vector encodings; + std::transform(prefix_encodings.as_array().cbegin(), + prefix_encodings.as_array().cend(), + std::back_inserter(encodings), + [](const auto &element) -> BinPackEncoding { + return binpack_load(element); + }); + assert(encodings.size() == prefix_encodings.size()); + return sourcemeta::blaze::FIXED_TYPED_ARRAY{ + .size = static_cast(size.to_integer()), + .encoding = + std::make_shared(binpack_load(array_encoding)), + .prefix_encodings = std::move(encodings)}; +} + +auto BOUNDED_8BITS_TYPED_ARRAY(const sourcemeta::core::JSON &options) + -> BinPackEncoding { + assert(options.defines("minimum")); + assert(options.defines("maximum")); + assert(options.defines("encoding")); + assert(options.defines("prefixEncodings")); + const auto &minimum{options.at("minimum")}; + const auto &maximum{options.at("maximum")}; + const auto &array_encoding{options.at("encoding")}; + const auto &prefix_encodings{options.at("prefixEncodings")}; + assert(minimum.is_integer()); + assert(maximum.is_integer()); + assert(minimum.is_positive()); + assert(maximum.is_positive()); + assert(array_encoding.is_object()); + assert(prefix_encodings.is_array()); + std::vector encodings; + std::transform(prefix_encodings.as_array().cbegin(), + prefix_encodings.as_array().cend(), + std::back_inserter(encodings), + [](const auto &element) -> BinPackEncoding { + return binpack_load(element); + }); + assert(encodings.size() == prefix_encodings.size()); + return sourcemeta::blaze::BOUNDED_8BITS_TYPED_ARRAY{ + .minimum = static_cast(minimum.to_integer()), + .maximum = static_cast(maximum.to_integer()), + .encoding = + std::make_shared(binpack_load(array_encoding)), + .prefix_encodings = std::move(encodings)}; +} + +auto FLOOR_TYPED_ARRAY(const sourcemeta::core::JSON &options) + -> BinPackEncoding { + assert(options.defines("minimum")); + assert(options.defines("encoding")); + assert(options.defines("prefixEncodings")); + const auto &minimum{options.at("minimum")}; + const auto &array_encoding{options.at("encoding")}; + const auto &prefix_encodings{options.at("prefixEncodings")}; + assert(minimum.is_integer()); + assert(minimum.is_positive()); + assert(array_encoding.is_object()); + assert(prefix_encodings.is_array()); + std::vector encodings; + std::transform(prefix_encodings.as_array().cbegin(), + prefix_encodings.as_array().cend(), + std::back_inserter(encodings), + [](const auto &element) -> BinPackEncoding { + return binpack_load(element); + }); + assert(encodings.size() == prefix_encodings.size()); + return sourcemeta::blaze::FLOOR_TYPED_ARRAY{ + .minimum = static_cast(minimum.to_integer()), + .encoding = + std::make_shared(binpack_load(array_encoding)), + .prefix_encodings = std::move(encodings)}; +} + +auto ROOF_TYPED_ARRAY(const sourcemeta::core::JSON &options) + -> BinPackEncoding { + assert(options.defines("maximum")); + assert(options.defines("encoding")); + assert(options.defines("prefixEncodings")); + const auto &maximum{options.at("maximum")}; + const auto &array_encoding{options.at("encoding")}; + const auto &prefix_encodings{options.at("prefixEncodings")}; + assert(maximum.is_integer()); + assert(maximum.is_positive()); + assert(array_encoding.is_object()); + assert(prefix_encodings.is_array()); + std::vector encodings; + std::transform(prefix_encodings.as_array().cbegin(), + prefix_encodings.as_array().cend(), + std::back_inserter(encodings), + [](const auto &element) -> BinPackEncoding { + return binpack_load(element); + }); + assert(encodings.size() == prefix_encodings.size()); + return sourcemeta::blaze::ROOF_TYPED_ARRAY{ + .maximum = static_cast(maximum.to_integer()), + .encoding = + std::make_shared(binpack_load(array_encoding)), + .prefix_encodings = std::move(encodings)}; +} + +// NOLINTEND(readability-identifier-naming) +} // namespace sourcemeta::blaze::v1 + +#endif diff --git a/vendor/blaze/src/binpack/loader_v1_integer.h b/vendor/blaze/src/binpack/loader_v1_integer.h new file mode 100644 index 000000000..5631b98eb --- /dev/null +++ b/vendor/blaze/src/binpack/loader_v1_integer.h @@ -0,0 +1,76 @@ +#ifndef SOURCEMETA_BLAZE_BINPACK_LOADER_V1_INTEGER_H_ +#define SOURCEMETA_BLAZE_BINPACK_LOADER_V1_INTEGER_H_ + +#include + +#include + +#include // assert +#include // std::uint64_t + +namespace sourcemeta::blaze::v1 { + +// The encoding names are the ones the JSON BinPack specification defines, so +// they keep their casing rather than following the C++ naming convention +// NOLINTBEGIN(readability-identifier-naming) + +auto BOUNDED_MULTIPLE_8BITS_ENUM_FIXED(const sourcemeta::core::JSON &options) + -> BinPackEncoding { + assert(options.defines("minimum")); + assert(options.defines("maximum")); + assert(options.defines("multiplier")); + const auto &minimum{options.at("minimum")}; + const auto &maximum{options.at("maximum")}; + const auto &multiplier{options.at("multiplier")}; + assert(minimum.is_integer()); + assert(maximum.is_integer()); + assert(multiplier.is_integer()); + assert(multiplier.is_positive()); + return sourcemeta::blaze::BOUNDED_MULTIPLE_8BITS_ENUM_FIXED{ + .minimum = minimum.to_integer(), + .maximum = maximum.to_integer(), + .multiplier = static_cast(multiplier.to_integer())}; +} + +auto FLOOR_MULTIPLE_ENUM_VARINT(const sourcemeta::core::JSON &options) + -> BinPackEncoding { + assert(options.defines("minimum")); + assert(options.defines("multiplier")); + const auto &minimum{options.at("minimum")}; + const auto &multiplier{options.at("multiplier")}; + assert(minimum.is_integer()); + assert(multiplier.is_integer()); + assert(multiplier.is_positive()); + return sourcemeta::blaze::FLOOR_MULTIPLE_ENUM_VARINT{ + .minimum = minimum.to_integer(), + .multiplier = static_cast(multiplier.to_integer())}; +} + +auto ROOF_MULTIPLE_MIRROR_ENUM_VARINT(const sourcemeta::core::JSON &options) + -> BinPackEncoding { + assert(options.defines("maximum")); + assert(options.defines("multiplier")); + const auto &maximum{options.at("maximum")}; + const auto &multiplier{options.at("multiplier")}; + assert(maximum.is_integer()); + assert(multiplier.is_integer()); + assert(multiplier.is_positive()); + return sourcemeta::blaze::ROOF_MULTIPLE_MIRROR_ENUM_VARINT{ + .maximum = maximum.to_integer(), + .multiplier = static_cast(multiplier.to_integer())}; +} + +auto ARBITRARY_MULTIPLE_ZIGZAG_VARINT(const sourcemeta::core::JSON &options) + -> BinPackEncoding { + assert(options.defines("multiplier")); + const auto &multiplier{options.at("multiplier")}; + assert(multiplier.is_integer()); + assert(multiplier.is_positive()); + return sourcemeta::blaze::ARBITRARY_MULTIPLE_ZIGZAG_VARINT{ + .multiplier = static_cast(multiplier.to_integer())}; +} + +// NOLINTEND(readability-identifier-naming) +} // namespace sourcemeta::blaze::v1 + +#endif diff --git a/vendor/blaze/src/binpack/loader_v1_number.h b/vendor/blaze/src/binpack/loader_v1_number.h new file mode 100644 index 000000000..9e5ecb400 --- /dev/null +++ b/vendor/blaze/src/binpack/loader_v1_number.h @@ -0,0 +1,27 @@ +#ifndef SOURCEMETA_BLAZE_BINPACK_LOADER_V1_NUMBER_H_ +#define SOURCEMETA_BLAZE_BINPACK_LOADER_V1_NUMBER_H_ + +#include + +#include + +namespace sourcemeta::blaze::v1 { + +// The encoding names are the ones the JSON BinPack specification defines, so +// they keep their casing rather than following the C++ naming convention +// NOLINTBEGIN(readability-identifier-naming) + +auto DOUBLE_VARINT_TUPLE([[maybe_unused]] const sourcemeta::core::JSON &options) + -> BinPackEncoding { + return sourcemeta::blaze::DOUBLE_VARINT_TUPLE{}; +} + +auto NUMBER_VARINT_TUPLE([[maybe_unused]] const sourcemeta::core::JSON &options) + -> BinPackEncoding { + return sourcemeta::blaze::NUMBER_VARINT_TUPLE{}; +} + +// NOLINTEND(readability-identifier-naming) +} // namespace sourcemeta::blaze::v1 + +#endif diff --git a/vendor/blaze/src/binpack/loader_v1_string.h b/vendor/blaze/src/binpack/loader_v1_string.h new file mode 100644 index 000000000..851bb7d0f --- /dev/null +++ b/vendor/blaze/src/binpack/loader_v1_string.h @@ -0,0 +1,75 @@ +#ifndef SOURCEMETA_BLAZE_BINPACK_LOADER_V1_STRING_H_ +#define SOURCEMETA_BLAZE_BINPACK_LOADER_V1_STRING_H_ + +#include + +#include + +#include // assert +#include // std::uint64_t + +namespace sourcemeta::blaze::v1 { + +// The encoding names are the ones the JSON BinPack specification defines, so +// they keep their casing rather than following the C++ naming convention +// NOLINTBEGIN(readability-identifier-naming) + +auto UTF8_STRING_NO_LENGTH(const sourcemeta::core::JSON &options) + -> BinPackEncoding { + assert(options.defines("size")); + const auto &size{options.at("size")}; + assert(size.is_integer()); + assert(size.is_positive()); + return sourcemeta::blaze::UTF8_STRING_NO_LENGTH{ + .size = static_cast(size.to_integer())}; +} + +auto FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED( + const sourcemeta::core::JSON &options) -> BinPackEncoding { + assert(options.defines("minimum")); + const auto &minimum{options.at("minimum")}; + assert(minimum.is_integer()); + assert(minimum.is_positive()); + return sourcemeta::blaze::FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED{ + .minimum = static_cast(minimum.to_integer())}; +} + +auto ROOF_VARINT_PREFIX_UTF8_STRING_SHARED( + const sourcemeta::core::JSON &options) -> BinPackEncoding { + assert(options.defines("maximum")); + const auto &maximum{options.at("maximum")}; + assert(maximum.is_integer()); + assert(maximum.is_positive()); + return sourcemeta::blaze::ROOF_VARINT_PREFIX_UTF8_STRING_SHARED{ + .maximum = static_cast(maximum.to_integer())}; +} + +auto BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED( + const sourcemeta::core::JSON &options) -> BinPackEncoding { + assert(options.defines("minimum")); + assert(options.defines("maximum")); + const auto &minimum{options.at("minimum")}; + const auto &maximum{options.at("maximum")}; + assert(minimum.is_integer()); + assert(maximum.is_integer()); + assert(minimum.is_positive()); + assert(maximum.is_positive()); + return sourcemeta::blaze::BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED{ + .minimum = static_cast(minimum.to_integer()), + .maximum = static_cast(maximum.to_integer())}; +} + +auto RFC3339_DATE_INTEGER_TRIPLET( + [[maybe_unused]] const sourcemeta::core::JSON &options) -> BinPackEncoding { + return sourcemeta::blaze::RFC3339_DATE_INTEGER_TRIPLET{}; +} + +auto PREFIX_VARINT_LENGTH_STRING_SHARED( + [[maybe_unused]] const sourcemeta::core::JSON &options) -> BinPackEncoding { + return sourcemeta::blaze::PREFIX_VARINT_LENGTH_STRING_SHARED{}; +} + +// NOLINTEND(readability-identifier-naming) +} // namespace sourcemeta::blaze::v1 + +#endif diff --git a/vendor/blaze/src/binpack/mapper/enum_8_bit.h b/vendor/blaze/src/binpack/mapper/enum_8_bit.h new file mode 100644 index 000000000..f5c6032e3 --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/enum_8_bit.h @@ -0,0 +1,34 @@ +// TODO: Unit test this mapping once we have container encodings +class Enum8Bit final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + Enum8Bit() : sourcemeta::blaze::SchemaTransformRule{"enum_8_bit", ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) && + schema.is_object() && schema.defines("enum") && + schema.at("enum").is_array() && !location.pointer.empty() && + schema.at("enum").size() > 1 && + sourcemeta::core::is_byte(schema.at("enum").size() - 1); + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + auto options = sourcemeta::core::JSON::make_object(); + options.assign("choices", schema.at("enum")); + make_encoding(schema, "BYTE_CHOICE_INDEX", options); + } +}; diff --git a/vendor/blaze/src/binpack/mapper/enum_8_bit_top_level.h b/vendor/blaze/src/binpack/mapper/enum_8_bit_top_level.h new file mode 100644 index 000000000..7810e8f4c --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/enum_8_bit_top_level.h @@ -0,0 +1,34 @@ +class Enum8BitTopLevel final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + Enum8BitTopLevel() + : sourcemeta::blaze::SchemaTransformRule{"enum_8_bit_top_level", ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) && + schema.is_object() && schema.defines("enum") && + schema.at("enum").is_array() && location.pointer.empty() && + schema.at("enum").size() > 1 && + sourcemeta::core::is_byte(schema.at("enum").size() - 1); + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + auto options = sourcemeta::core::JSON::make_object(); + options.assign("choices", schema.at("enum")); + make_encoding(schema, "TOP_LEVEL_BYTE_CHOICE_INDEX", options); + } +}; diff --git a/vendor/blaze/src/binpack/mapper/enum_arbitrary.h b/vendor/blaze/src/binpack/mapper/enum_arbitrary.h new file mode 100644 index 000000000..34b4b5a90 --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/enum_arbitrary.h @@ -0,0 +1,35 @@ +// TODO: Unit test this mapping once we have container encodings +class EnumArbitrary final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + EnumArbitrary() + : sourcemeta::blaze::SchemaTransformRule{"enum_arbitrary", ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) && + schema.is_object() && schema.defines("enum") && + schema.at("enum").is_array() && !location.pointer.empty() && + schema.at("enum").size() > 1 && + !sourcemeta::core::is_byte(schema.at("enum").size() - 1); + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + auto options = sourcemeta::core::JSON::make_object(); + options.assign("choices", schema.at("enum")); + make_encoding(schema, "LARGE_CHOICE_INDEX", options); + } +}; diff --git a/vendor/blaze/src/binpack/mapper/enum_singleton.h b/vendor/blaze/src/binpack/mapper/enum_singleton.h new file mode 100644 index 000000000..bc61d34d9 --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/enum_singleton.h @@ -0,0 +1,32 @@ +class EnumSingleton final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + EnumSingleton() + : sourcemeta::blaze::SchemaTransformRule{"enum_singleton", ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) && + schema.is_object() && schema.defines("enum") && + schema.at("enum").is_array() && schema.at("enum").size() == 1; + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + auto options = sourcemeta::core::JSON::make_object(); + options.assign("value", schema.at("enum").at(0)); + make_encoding(schema, "CONST_NONE", options); + } +}; diff --git a/vendor/blaze/src/binpack/mapper/integer_bounded_8_bit.h b/vendor/blaze/src/binpack/mapper/integer_bounded_8_bit.h new file mode 100644 index 000000000..c9661da6d --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/integer_bounded_8_bit.h @@ -0,0 +1,40 @@ +class IntegerBounded8Bit final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerBounded8Bit() + : sourcemeta::blaze::SchemaTransformRule{"integer_bounded_8_bit", ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + schema.defines("minimum") && schema.defines("maximum") && + sourcemeta::core::is_byte(schema.at("maximum").to_integer() - + schema.at("minimum").to_integer()) && + !schema.defines("multipleOf"); + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + auto minimum = schema.at("minimum"); + auto maximum = schema.at("maximum"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("minimum", std::move(minimum)); + options.assign("maximum", std::move(maximum)); + options.assign("multiplier", sourcemeta::core::JSON{1}); + make_encoding(schema, "BOUNDED_MULTIPLE_8BITS_ENUM_FIXED", options); + } +}; diff --git a/vendor/blaze/src/binpack/mapper/integer_bounded_greater_than_8_bit.h b/vendor/blaze/src/binpack/mapper/integer_bounded_greater_than_8_bit.h new file mode 100644 index 000000000..85f8fe652 --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/integer_bounded_greater_than_8_bit.h @@ -0,0 +1,40 @@ +class IntegerBoundedGreaterThan8Bit final + : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerBoundedGreaterThan8Bit() + : sourcemeta::blaze::SchemaTransformRule{ + "integer_bounded_greater_than_8_bit", ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + schema.defines("minimum") && schema.defines("maximum") && + !sourcemeta::core::is_byte(schema.at("maximum").to_integer() - + schema.at("minimum").to_integer()) && + !schema.defines("multipleOf"); + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + auto minimum = schema.at("minimum"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("minimum", std::move(minimum)); + options.assign("multiplier", sourcemeta::core::JSON{1}); + make_encoding(schema, "FLOOR_MULTIPLE_ENUM_VARINT", options); + } +}; diff --git a/vendor/blaze/src/binpack/mapper/integer_bounded_multiplier_8_bit.h b/vendor/blaze/src/binpack/mapper/integer_bounded_multiplier_8_bit.h new file mode 100644 index 000000000..29414f411 --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/integer_bounded_multiplier_8_bit.h @@ -0,0 +1,50 @@ +class IntegerBoundedMultiplier8Bit final + : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerBoundedMultiplier8Bit() + : sourcemeta::blaze::SchemaTransformRule{ + "integer_bounded_multiplier_8_bit", ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + if (location.dialect != "https://json-schema.org/draft/2020-12/schema" || + !vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) || + !schema.is_object() || !schema.defines("type") || + schema.at("type").to_string() != "integer" || + !schema.defines("minimum") || !schema.at("minimum").is_integer() || + !schema.defines("maximum") || !schema.at("maximum").is_integer() || + !schema.defines("multipleOf") || + !schema.at("multipleOf").is_integer()) { + return false; + } + + return sourcemeta::core::is_byte(sourcemeta::core::count_multiples( + schema.at("minimum").to_integer(), schema.at("maximum").to_integer(), + schema.at("multipleOf").to_integer())); + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + auto minimum = schema.at("minimum"); + auto maximum = schema.at("maximum"); + auto multiplier = schema.at("multipleOf"); + + auto options = sourcemeta::core::JSON::make_object(); + options.assign("minimum", std::move(minimum)); + options.assign("maximum", std::move(maximum)); + options.assign("multiplier", std::move(multiplier)); + make_encoding(schema, "BOUNDED_MULTIPLE_8BITS_ENUM_FIXED", options); + } +}; diff --git a/vendor/blaze/src/binpack/mapper/integer_bounded_multiplier_greater_than_8_bit.h b/vendor/blaze/src/binpack/mapper/integer_bounded_multiplier_greater_than_8_bit.h new file mode 100644 index 000000000..267399c41 --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/integer_bounded_multiplier_greater_than_8_bit.h @@ -0,0 +1,47 @@ +class IntegerBoundedMultiplierGreaterThan8Bit final + : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerBoundedMultiplierGreaterThan8Bit() + : sourcemeta::blaze::SchemaTransformRule{ + "integer_bounded_multiplier_greater_than_8_bit", ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + if (location.dialect != "https://json-schema.org/draft/2020-12/schema" || + !vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) || + !schema.is_object() || !schema.defines("type") || + schema.at("type").to_string() != "integer" || + !schema.defines("minimum") || !schema.at("minimum").is_integer() || + !schema.defines("maximum") || !schema.at("maximum").is_integer() || + !schema.defines("multipleOf") || + !schema.at("multipleOf").is_integer()) { + return false; + } + + return !sourcemeta::core::is_byte(sourcemeta::core::count_multiples( + schema.at("minimum").to_integer(), schema.at("maximum").to_integer(), + schema.at("multipleOf").to_integer())); + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + auto minimum = schema.at("minimum"); + auto multiplier = schema.at("multipleOf"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("minimum", std::move(minimum)); + options.assign("multiplier", std::move(multiplier)); + make_encoding(schema, "FLOOR_MULTIPLE_ENUM_VARINT", options); + } +}; diff --git a/vendor/blaze/src/binpack/mapper/integer_lower_bound.h b/vendor/blaze/src/binpack/mapper/integer_lower_bound.h new file mode 100644 index 000000000..d9a68ff30 --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/integer_lower_bound.h @@ -0,0 +1,36 @@ +class IntegerLowerBound final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerLowerBound() + : sourcemeta::blaze::SchemaTransformRule{"integer_lower_bound", ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + schema.defines("minimum") && !schema.defines("maximum") && + !schema.defines("multipleOf"); + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + auto minimum = schema.at("minimum"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("minimum", std::move(minimum)); + options.assign("multiplier", sourcemeta::core::JSON{1}); + make_encoding(schema, "FLOOR_MULTIPLE_ENUM_VARINT", options); + } +}; diff --git a/vendor/blaze/src/binpack/mapper/integer_lower_bound_multiplier.h b/vendor/blaze/src/binpack/mapper/integer_lower_bound_multiplier.h new file mode 100644 index 000000000..e0a1195c8 --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/integer_lower_bound_multiplier.h @@ -0,0 +1,39 @@ +class IntegerLowerBoundMultiplier final + : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerLowerBoundMultiplier() + : sourcemeta::blaze::SchemaTransformRule{"integer_lower_bound_multiplier", + ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + schema.defines("minimum") && !schema.defines("maximum") && + schema.defines("multipleOf") && schema.at("multipleOf").is_integer(); + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + auto minimum = schema.at("minimum"); + auto multiplier = schema.at("multipleOf"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("minimum", std::move(minimum)); + options.assign("multiplier", std::move(multiplier)); + make_encoding(schema, "FLOOR_MULTIPLE_ENUM_VARINT", options); + } +}; diff --git a/vendor/blaze/src/binpack/mapper/integer_unbound.h b/vendor/blaze/src/binpack/mapper/integer_unbound.h new file mode 100644 index 000000000..506df0bd8 --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/integer_unbound.h @@ -0,0 +1,34 @@ +class IntegerUnbound final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerUnbound() + : sourcemeta::blaze::SchemaTransformRule{"integer_unbound", ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + !schema.defines("minimum") && !schema.defines("maximum") && + !schema.defines("multipleOf"); + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + auto options = sourcemeta::core::JSON::make_object(); + options.assign("multiplier", sourcemeta::core::JSON{1}); + make_encoding(schema, "ARBITRARY_MULTIPLE_ZIGZAG_VARINT", options); + } +}; diff --git a/vendor/blaze/src/binpack/mapper/integer_unbound_multiplier.h b/vendor/blaze/src/binpack/mapper/integer_unbound_multiplier.h new file mode 100644 index 000000000..2e839fde9 --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/integer_unbound_multiplier.h @@ -0,0 +1,37 @@ +class IntegerUnboundMultiplier final + : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerUnboundMultiplier() + : sourcemeta::blaze::SchemaTransformRule{"integer_unbound_multiplier", + ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + !schema.defines("minimum") && !schema.defines("maximum") && + schema.defines("multipleOf") && schema.at("multipleOf").is_integer(); + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + auto multiplier = schema.at("multipleOf"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("multiplier", std::move(multiplier)); + make_encoding(schema, "ARBITRARY_MULTIPLE_ZIGZAG_VARINT", options); + } +}; diff --git a/vendor/blaze/src/binpack/mapper/integer_upper_bound.h b/vendor/blaze/src/binpack/mapper/integer_upper_bound.h new file mode 100644 index 000000000..c6b4009d1 --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/integer_upper_bound.h @@ -0,0 +1,36 @@ +class IntegerUpperBound final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerUpperBound() + : sourcemeta::blaze::SchemaTransformRule{"integer_upper_bound", ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + !schema.defines("minimum") && schema.defines("maximum") && + !schema.defines("multipleOf"); + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + auto maximum = schema.at("maximum"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("maximum", std::move(maximum)); + options.assign("multiplier", sourcemeta::core::JSON{1}); + make_encoding(schema, "ROOF_MULTIPLE_MIRROR_ENUM_VARINT", options); + } +}; diff --git a/vendor/blaze/src/binpack/mapper/integer_upper_bound_multiplier.h b/vendor/blaze/src/binpack/mapper/integer_upper_bound_multiplier.h new file mode 100644 index 000000000..404e2b0ac --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/integer_upper_bound_multiplier.h @@ -0,0 +1,39 @@ +class IntegerUpperBoundMultiplier final + : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerUpperBoundMultiplier() + : sourcemeta::blaze::SchemaTransformRule{"integer_upper_bound_multiplier", + ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + !schema.defines("minimum") && schema.defines("maximum") && + schema.defines("multipleOf") && schema.at("multipleOf").is_integer(); + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + auto maximum = schema.at("maximum"); + auto multiplier = schema.at("multipleOf"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("maximum", std::move(maximum)); + options.assign("multiplier", std::move(multiplier)); + make_encoding(schema, "ROOF_MULTIPLE_MIRROR_ENUM_VARINT", options); + } +}; diff --git a/vendor/blaze/src/binpack/mapper/number_arbitrary.h b/vendor/blaze/src/binpack/mapper/number_arbitrary.h new file mode 100644 index 000000000..eb76f5a60 --- /dev/null +++ b/vendor/blaze/src/binpack/mapper/number_arbitrary.h @@ -0,0 +1,31 @@ +class NumberArbitrary final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + NumberArbitrary() + : sourcemeta::blaze::SchemaTransformRule{"number_arbitrary", ""} {}; + + [[nodiscard]] auto condition( + const sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaVocabularies &vocabularies, + [[maybe_unused]] const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + [[maybe_unused]] const sourcemeta::core::SchemaWalker &walker, + [[maybe_unused]] const sourcemeta::core::SchemaResolver &resolver) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_VALIDATION) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "number"; + } + + auto transform( + sourcemeta::core::JSON &schema, + [[maybe_unused]] const sourcemeta::blaze::SchemaTransformRule::Result + &result) const -> void override { + make_encoding(schema, "NUMBER_VARINT_TUPLE", + sourcemeta::core::JSON::make_object()); + } +}; diff --git a/vendor/blaze/src/binpack/output_stream.cc b/vendor/blaze/src/binpack/output_stream.cc new file mode 100644 index 000000000..11b41d0a6 --- /dev/null +++ b/vendor/blaze/src/binpack/output_stream.cc @@ -0,0 +1,43 @@ +#include + +#include + +#include // assert +#include // std::uint8_t, std::uint64_t, std::int64_t + +namespace sourcemeta::blaze { + +BinPackOutputStream::BinPackOutputStream(Stream &output) + : sourcemeta::core::BinaryWriter{output} {} + +auto BinPackOutputStream::put_varint(const std::uint64_t value) -> void { + constexpr std::uint8_t LEAST_SIGNIFICANT_BITS{0b01111111}; + constexpr std::uint8_t MOST_SIGNIFICANT_BIT{0b10000000}; + constexpr std::uint8_t SHIFT{7}; + std::uint64_t accumulator = value; + + while (accumulator > LEAST_SIGNIFICANT_BITS) { + this->put_byte(static_cast( + (accumulator & LEAST_SIGNIFICANT_BITS) | MOST_SIGNIFICANT_BIT)); + accumulator >>= SHIFT; + } + + this->put_byte(static_cast(accumulator)); +} + +auto BinPackOutputStream::put_varint_zigzag(const std::int64_t value) -> void { + this->put_varint(sourcemeta::core::zigzag_encode(value)); +} + +auto BinPackOutputStream::put_string_utf8( + const sourcemeta::core::JSON::String &string, const std::uint64_t length) + -> void { + assert(string.size() == length); + // Do a manual for-loop based on the provided length instead of a range + // loop based on the string value to avoid accidental overflows + for (std::uint64_t index = 0; index < length; index++) { + this->put_byte(static_cast(string[index])); + } +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/binpack/unreachable.h b/vendor/blaze/src/binpack/unreachable.h new file mode 100644 index 000000000..779cb6b15 --- /dev/null +++ b/vendor/blaze/src/binpack/unreachable.h @@ -0,0 +1,17 @@ +#ifndef SOURCEMETA_BLAZE_BINPACK_UNREACHABLE_H_ +#define SOURCEMETA_BLAZE_BINPACK_UNREACHABLE_H_ + +#include // assert + +// Until we are on C++23 and can use std::unreachable +// See https://en.cppreference.com/w/cpp/utility/unreachable +[[noreturn]] inline void unreachable() { + assert(false); +#if defined(_MSC_VER) && !defined(__clang__) + __assume(false); +#else + __builtin_unreachable(); +#endif +} + +#endif diff --git a/vendor/blaze/src/canonicalizer/canonicalize.cc b/vendor/blaze/src/canonicalizer/canonicalize.cc index 55bf51853..96ba28cc2 100644 --- a/vendor/blaze/src/canonicalizer/canonicalize.cc +++ b/vendor/blaze/src/canonicalizer/canonicalize.cc @@ -246,20 +246,52 @@ auto apply(const std::vector &rules, sourcemeta::core::JSON &schema, } } -auto is_draft3(const core::SchemaBaseDialect base_dialect) -> bool { +/// The OpenAPI 3.0 Schema Object dialect, in both of its dated spellings. +/// Neither is canonical, and a tag URI carries no fragment and no scheme the +/// spelling rules fold, so what a document says is already the only spelling +/// there is +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 a location is an OpenAPI 3.0 Schema Object. One describes itself +/// through a Draft 4 meta-schema, so it frames as Draft 4 while being a closed +/// dialect that names neither `definitions` nor an identifier. Neither the +/// graph form nor the identifier elimination that leads to it has a spelling +/// there, so such a document is left exactly as it stands +auto is_openapi_3_0(const std::string_view dialect) -> bool { + return std::ranges::find(OPENAPI_3_0_DIALECTS, dialect) != + OPENAPI_3_0_DIALECTS.cend(); +} + +/// The drafts that spell the identifier `id` and let it nest anywhere, so +/// that a subschema can change the base URI the references around it resolve +/// against. Draft 6 renamed the keyword to `$id` and the rules there differ. +/// +/// Erasing an identifier is only worth doing where the graph form follows, +/// since the form is what cannot carry one. These must therefore name exactly +/// the dialects `profile_for` covers: a dialect that lost its identifiers +/// without being lifted would keep every nested document it had while no +/// longer being able to address them. That is why Draft 4 hyper-schema is +/// absent, as it is there, and why the OpenAPI 3.0 Schema Object is turned +/// down although it frames as Draft 4 +auto has_nested_id(const core::SchemaBaseDialect base_dialect, + const std::string_view dialect) -> bool { return base_dialect == core::SchemaBaseDialect::JSON_SCHEMA_DRAFT_3 || - base_dialect == core::SchemaBaseDialect::JSON_SCHEMA_DRAFT_3_HYPER; + base_dialect == core::SchemaBaseDialect::JSON_SCHEMA_DRAFT_3_HYPER || + (base_dialect == core::SchemaBaseDialect::JSON_SCHEMA_DRAFT_4 && + !is_openapi_3_0(dialect)); } -/// Remove every Draft 3 identifier, including anchors and the identifier of -/// the root. Every reference is first resolved while the frame still knows -/// the identifiers, and rewritten into a form that does not depend on any of -/// them: a JSON Pointer into this document when the destination is local, or -/// the absolute URI when it is not. Only then are the identifiers erased, so -/// no reference can lose the base it was resolved against. +/// Remove every `id`, including anchors and the identifier of the root. Every +/// reference is first resolved while the frame still knows the identifiers, +/// and rewritten into a form that does not depend on any of them: a JSON +/// Pointer into this document when the destination is local, or the absolute +/// URI when it is not. Only then are the identifiers erased, so no reference +/// can lose the base it was resolved against. /// -/// This only applies to documents that are Draft 3 throughout. A document of -/// another dialect may embed a Draft 3 resource and reference it by its +/// This only applies to documents that use `id` throughout. A document of +/// another dialect may embed such a resource and reference it by its /// identifier from outside, with a reference this pass must not rewrite, so /// such documents are left untouched auto eliminate_identifiers(sourcemeta::core::JSON &schema, @@ -285,7 +317,7 @@ auto eliminate_identifiers(sourcemeta::core::JSON &schema, if (frame.any_subschema( [](const core::SchemaFrame::Location &location) -> bool { - return !is_draft3(location.base_dialect); + return !has_nested_id(location.base_dialect, location.dialect); })) { return; } @@ -374,11 +406,83 @@ auto entry_number(const sourcemeta::core::JSON &reference) return static_cast(std::stoull(suffix)); } +/// How the graph form is spelled in a given dialect: the form is the same +/// everywhere, only the keywords and what the dialect allows differ +struct Profile { + /// The keyword a conjunction is written with + sourcemeta::core::JSON::String wrapper; + /// Keywords whose value is an object whose members are subschemas + std::set mappings; + /// Keywords whose value is a single subschema + std::set singles; + /// Keywords whose value is a subschema or an array of them + std::set branches; + /// Keywords whose arrays say nothing new by naming the same entry twice + std::set unique_branches; + /// Whether requiredness is a marker beside the subschema it applies to, as + /// in Draft 3, rather than a list of names on the object, as from Draft 4 on + bool property_marker; + /// Whether a boolean standing in for a subschema can be an entry. From + /// Draft 4 on the values of `definitions` have to be schemas, and a boolean + /// is not one, so there it stays where it is + bool boolean_nodes; +}; + +/// Draft 3 states a conjunction with `extends`, a disjunction as an array of +/// subschemas under `type`, and a negation with `disallow` +auto draft3_profile() -> const Profile & { + static const Profile PROFILE{ + .wrapper = "extends", + .mappings = {"properties", "patternProperties", "dependencies"}, + .singles = {"additionalProperties", "additionalItems"}, + .branches = {"items", "type", "extends", "disallow"}, + .unique_branches = {"type", "extends", "disallow"}, + .property_marker = true, + .boolean_nodes = true}; + return PROFILE; +} + +/// Draft 4 replaces those three with `allOf`, `anyOf`, `oneOf` and `not`. Its +/// `type` only ever names types, so unlike Draft 3 it holds no subschema, and +/// its requiredness is a list of names on the object rather than a marker +auto draft4_profile() -> const Profile & { + static const Profile PROFILE{ + .wrapper = "allOf", + .mappings = {"properties", "patternProperties", "dependencies"}, + .singles = {"additionalProperties", "additionalItems", "not"}, + .branches = {"items", "allOf", "anyOf", "oneOf"}, + // `oneOf` counts how many branches match, so naming the same entry + // twice is not the same as naming it once: two identical branches + // never match, one always can + .unique_branches = {"allOf", "anyOf"}, + .property_marker = false, + .boolean_nodes = false}; + return PROFILE; +} + +/// The profile for a dialect, when the graph form covers it at all. Draft 3 +/// hyper-schema shares the Draft 3 profile: by the time this runs the rules +/// have dropped the keywords its vocabulary adds, none of which takes part +/// in whether an instance is valid. Draft 4 hyper-schema has no such rule +/// yet, so its `links` would be lifted as though they were subschemas +auto profile_for(const core::SchemaBaseDialect base_dialect, + const std::string_view dialect) -> const Profile * { + switch (base_dialect) { + case core::SchemaBaseDialect::JSON_SCHEMA_DRAFT_3: + case core::SchemaBaseDialect::JSON_SCHEMA_DRAFT_3_HYPER: + return &draft3_profile(); + case core::SchemaBaseDialect::JSON_SCHEMA_DRAFT_4: + return is_openapi_3_0(dialect) ? nullptr : &draft4_profile(); + default: + return nullptr; + } +} + /// The reference an entry amounts to, when the entry is nothing but a -/// reference: either on its own, or behind the one-element `extends` that -/// this pass leaves in place of a lifted subschema. Such a subschema is an -/// edge of the graph rather than a node of it -auto edge_reference(const sourcemeta::core::JSON &body) +/// reference: either on its own, or behind the one-element wrapper that this +/// pass leaves in place of a lifted subschema. Such a subschema is an edge of +/// the graph rather than a node of it +auto edge_reference(const sourcemeta::core::JSON &body, const Profile &profile) -> const sourcemeta::core::JSON * { if (!body.is_object() || body.size() != 1) { return nullptr; @@ -388,11 +492,11 @@ auto edge_reference(const sourcemeta::core::JSON &body) return &body.at("$ref"); } - if (!body.defines("extends")) { + if (!body.defines(profile.wrapper)) { return nullptr; } - const auto &branches{body.at("extends")}; + const auto &branches{body.at(profile.wrapper)}; if (!branches.is_array() || branches.size() != 1) { return nullptr; } @@ -408,20 +512,25 @@ auto edge_reference(const sourcemeta::core::JSON &body) /// 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 { - const auto *reference{edge_reference(body)}; +auto is_edge(const sourcemeta::core::JSON &body, const Profile &profile) + -> bool { + const auto *reference{edge_reference(body, profile)}; return reference != nullptr && reference->is_string() && reference->to_string().starts_with('#'); } /// What a subschema says, with the keywords that carry the graph itself taken -/// out. `definitions` holds other nodes rather than part of this one, and a -/// `required` marker belongs to the parent `properties` -auto without_containers(sourcemeta::core::JSON body) -> sourcemeta::core::JSON { +/// out. `definitions` holds other nodes rather than part of this one, and +/// where requiredness is a marker it belongs to the parent `properties` +/// rather than to the subschema it sits on +auto without_containers(sourcemeta::core::JSON body, const Profile &profile) + -> sourcemeta::core::JSON { if (body.is_object()) { body.erase("$schema"); body.erase("definitions"); - body.erase("required"); + if (profile.property_marker) { + body.erase("required"); + } } return body; @@ -439,22 +548,12 @@ auto without_containers(sourcemeta::core::JSON body) -> sourcemeta::core::JSON { /// /// Members are read in key order so that the result never depends on the order /// an object happens to store them in -auto reference_slots(const sourcemeta::core::JSON &body) +auto reference_slots(const sourcemeta::core::JSON &body, const Profile &profile) -> std::vector { - static const std::set MAPPINGS{ - "properties", "patternProperties", "dependencies"}; - static const std::set SINGLES{ - "additionalProperties", "additionalItems"}; - // `items` holds either one subschema or a tuple of them, and the three - // Draft 3 keywords that take branches hold an array once the rules have run, - // though hyper-schema can leave a lone subschema behind - static const std::set BRANCHES{ - "items", "type", "extends", "disallow"}; - std::vector result; - const auto collect{[&result](const sourcemeta::core::JSON ¤t, - const core::Pointer &position, - const auto &self) -> void { + const auto collect{[&result, &profile](const sourcemeta::core::JSON ¤t, + const core::Pointer &position, + const auto &self) -> void { if (!current.is_object()) { return; } @@ -472,7 +571,7 @@ auto reference_slots(const sourcemeta::core::JSON &body) if (value.is_string()) { result.push_back(position.concat(key)); } - } else if (MAPPINGS.contains(key) && value.is_object()) { + } else if (profile.mappings.contains(key) && value.is_object()) { std::vector names; names.reserve(value.size()); for (const auto &member : value.as_object()) { @@ -483,9 +582,9 @@ auto reference_slots(const sourcemeta::core::JSON &body) for (const auto &name : names) { self(value.at(name), position.concat(key).concat(name), self); } - } else if (SINGLES.contains(key)) { + } else if (profile.singles.contains(key)) { self(value, position.concat(key), self); - } else if (BRANCHES.contains(key)) { + } else if (profile.branches.contains(key)) { if (value.is_array()) { for (std::size_t index = 0; index < value.size(); index++) { self(value.at(index), @@ -519,6 +618,7 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, return; } + const Profile *profile{nullptr}; std::vector nodes; // Every reference the document declares, before edges are followed through std::vector> links; @@ -533,24 +633,39 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, default_id, core::SchemaFrame::IdentifierMode::Fallback}; - // 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 !is_draft3(location.base_dialect); - })) { + // Every subschema has to land on the same spelling of the form, and on + // one the form covers at all. Draft 3 and its hyper-schema share a + // profile: by now the rules have dropped everything the hyper-schema + // vocabulary adds, none of which takes part in whether an instance is + // valid + bool unsupported{false}; + frame.for_each_subschema([&profile, &unsupported]( + const core::SchemaFrame::Location &location) + -> void { + const auto *current{profile_for(location.base_dialect, location.dialect)}; + if (current == nullptr || (profile != nullptr && profile != current)) { + unsupported = true; + } else { + profile = current; + } + }); + + if (unsupported || profile == nullptr) { return; } frame.for_each_subschema( - [&nodes, &schema](const core::SchemaFrame::Location &location) -> void { + [&nodes, &schema, + profile](const core::SchemaFrame::Location &location) -> void { + const auto &body{core::get(schema, location.pointer)}; + if (!profile->boolean_nodes && body.is_boolean()) { + return; + } + // The test runs on what the body would be once the container // keywords are stripped, so that a root already in graph form is // recognised as the wrapper it is - if (is_edge( - without_containers(core::get(schema, location.pointer)))) { + if (is_edge(without_containers(body, *profile), *profile)) { return; } @@ -589,9 +704,8 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, // A reference can target a subschema that is itself an edge, so follow such // a chain through to the node it ultimately reaches static const core::JSON::String REF_KEYWORD{"$ref"}; - static const core::JSON::String EXTENDS_KEYWORD{"extends"}; - const auto resolve{[&nodes, &links](const core::Pointer &start) + const auto resolve{[&nodes, &links, profile](const core::Pointer &start) -> std::optional { auto target{start}; for (std::size_t hop = 0; hop <= links.size(); hop++) { @@ -609,10 +723,10 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, continue; } - const auto wrapped{ - std::ranges::find_if(links, [&target](const auto &entry) -> bool { + const auto wrapped{std::ranges::find_if( + links, [&target, profile](const auto &entry) -> bool { return entry.first == - target.concat({EXTENDS_KEYWORD, + target.concat({profile->wrapper, static_cast(0), REF_KEYWORD}); })}; @@ -666,35 +780,40 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, return result; }}; - const auto link{[](const std::size_t index, - const sourcemeta::core::JSON &target, - const core::Pointer &position) -> sourcemeta::core::JSON { - auto reference{sourcemeta::core::JSON::make_object()}; - reference.assign("$ref", sourcemeta::core::JSON{"#/definitions/" + - std::to_string(index)}); - - // `required` is read by the parent `properties`, and Draft 3 ignores - // the siblings of a `$ref`, so the marker can neither travel into the - // entry nor sit next to the reference. It stays behind, wrapping it - const auto *marker{target.is_object() ? target.try_at("required") - : nullptr}; - // Anywhere but a property entry the marker means nothing at all, so - // a subschema under `items` or `additionalProperties` loses it - const auto under_properties{ - position.size() >= 2 && - position.at(position.size() - 2).is_property() && - position.at(position.size() - 2).to_property() == "properties"}; - if (marker == nullptr || !under_properties) { - return reference; - } + const auto link{ + [profile](const std::size_t index, const sourcemeta::core::JSON &target, + const core::Pointer &position) -> sourcemeta::core::JSON { + auto reference{sourcemeta::core::JSON::make_object()}; + reference.assign("$ref", sourcemeta::core::JSON{"#/definitions/" + + std::to_string(index)}); - auto wrapper{sourcemeta::core::JSON::make_object()}; - wrapper.assign("required", *marker); - auto branches{sourcemeta::core::JSON::make_array()}; - branches.push_back(std::move(reference)); - wrapper.assign("extends", std::move(branches)); - return wrapper; - }}; + // A list of names on the object leaves nothing behind + if (!profile->property_marker) { + return reference; + } + + // A marker is read by the parent `properties`, and these drafts ignore + // the siblings of a `$ref`, so it can neither travel into the entry nor + // sit next to the reference. It stays behind, wrapping it + const auto *marker{target.is_object() ? target.try_at("required") + : nullptr}; + // Anywhere but a property entry the marker means nothing at all, so + // a subschema under `items` or `additionalProperties` loses it + const auto under_properties{ + position.size() >= 2 && + position.at(position.size() - 2).is_property() && + position.at(position.size() - 2).to_property() == "properties"}; + if (marker == nullptr || !under_properties) { + return reference; + } + + auto wrapper{sourcemeta::core::JSON::make_object()}; + wrapper.assign("required", *marker); + auto branches{sourcemeta::core::JSON::make_array()}; + branches.push_back(std::move(reference)); + wrapper.assign(profile->wrapper, std::move(branches)); + return wrapper; + }}; const auto entry_node{resolve(core::Pointer{})}; if (!entry_node.has_value()) { @@ -735,7 +854,9 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, if (body.is_object()) { body.erase("$schema"); - body.erase("required"); + if (profile->property_marker) { + body.erase("required"); + } // Every entry of a `definitions` is a node of its own, so the container // is not part of any body. Leaving it in would make a later pass lift // the same entries again @@ -759,7 +880,7 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, // and the entry itself goes away std::map collapsed; for (const auto &entry : definitions.as_object()) { - const auto *reference{edge_reference(entry.second)}; + const auto *reference{edge_reference(entry.second, *profile)}; if (reference == nullptr) { continue; } @@ -792,9 +913,9 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, } } - const auto rewrite{[&destination, - &renamed](sourcemeta::core::JSON &body) -> void { - for (const auto &slot : reference_slots(body)) { + const auto rewrite{[&destination, &renamed, + profile](sourcemeta::core::JSON &body) -> void { + for (const auto &slot : reference_slots(body, *profile)) { const auto entry{entry_number(core::get(body, slot))}; if (!entry.has_value()) { continue; @@ -836,9 +957,9 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, // not depend on the order an object happens to store its members in. // Reading them by sorted key gives the same answer however the object was // built - const auto targets{[](const sourcemeta::core::JSON &body, - std::vector &result) -> void { - for (const auto &slot : reference_slots(body)) { + const auto targets{[profile](const sourcemeta::core::JSON &body, + std::vector &result) -> void { + for (const auto &slot : reference_slots(body, *profile)) { const auto entry{entry_number(core::get(body, slot))}; if (entry.has_value()) { result.push_back(entry.value()); @@ -849,17 +970,17 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, // Only the references this pass produced are set aside. A reference that // leaves the document names no entry, so it stays part of what the body // says: two entries that point at different documents are not the same - const auto skeleton{ - [](const sourcemeta::core::JSON &body) -> sourcemeta::core::JSON { - auto result{body}; - for (const auto &slot : reference_slots(result)) { - if (entry_number(core::get(result, slot)).has_value()) { - core::set(result, slot, sourcemeta::core::JSON{""}); - } - } + const auto skeleton{[profile](const sourcemeta::core::JSON &body) + -> sourcemeta::core::JSON { + auto result{body}; + for (const auto &slot : reference_slots(result, *profile)) { + if (entry_number(core::get(result, slot)).has_value()) { + core::set(result, slot, sourcemeta::core::JSON{""}); + } + } - return result; - }}; + return result; + }}; std::vector> outgoing{bodies.size()}; std::vector classes(bodies.size(), 0); @@ -933,33 +1054,34 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, position.emplace(walked.at(index), index); } - const auto relabel{[&classes, &position](const sourcemeta::core::JSON &body) - -> sourcemeta::core::JSON { - auto result{body}; - for (const auto &slot : reference_slots(result)) { - const auto entry{entry_number(core::get(result, slot))}; - if (!entry.has_value()) { - continue; - } + const auto relabel{ + [&classes, &position, profile]( + const sourcemeta::core::JSON &body) -> sourcemeta::core::JSON { + auto result{body}; + for (const auto &slot : reference_slots(result, *profile)) { + const auto entry{entry_number(core::get(result, slot))}; + if (!entry.has_value()) { + continue; + } - core::set(result, slot, - sourcemeta::core::JSON{ - "#/definitions/" + - std::to_string(position.at(classes.at(entry.value())))}); - } + core::set(result, slot, + sourcemeta::core::JSON{"#/definitions/" + + std::to_string(position.at( + classes.at(entry.value())))}); + } - return result; - }}; + return result; + }}; // Merging entries can leave a disjunction or a conjunction listing the same - // entry twice, which says nothing new. The rules already drop duplicates in - // these three keywords, but they run before any of this - const auto unique{[](sourcemeta::core::JSON &body) -> void { + // entry twice, which says nothing new. The rules already drop duplicates + // in these keywords, but they run before any of this + const auto unique{[profile](sourcemeta::core::JSON &body) -> void { if (!body.is_object()) { return; } - for (const auto *keyword : {"type", "extends", "disallow"}) { + for (const auto &keyword : profile->unique_branches) { auto *entries{body.try_at(keyword)}; if (entries == nullptr || !entries->is_array()) { continue; @@ -1015,7 +1137,7 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, reference.assign("$ref", sourcemeta::core::JSON{"#/definitions/" + std::to_string(entry_point)}); wrapper.push_back(std::move(reference)); - result.assign("extends", std::move(wrapper)); + result.assign(profile->wrapper, std::move(wrapper)); result.assign("definitions", std::move(definitions)); schema.into(std::move(result)); } diff --git a/vendor/blaze/src/codegen/codegen_default_compiler.h b/vendor/blaze/src/codegen/codegen_default_compiler.h index 4559bc13b..44204523e 100644 --- a/vendor/blaze/src/codegen/codegen_default_compiler.h +++ b/vendor/blaze/src/codegen/codegen_default_compiler.h @@ -13,12 +13,14 @@ // We do not check vocabularies here because the canonicaliser ensures // we never get an official keyword when its vocabulary is not present +// Every shape already allows `$defs`. `definitions` is the same container +// under the spelling Drafts 4 to 7 use, so allow it everywhere too // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) #define ONLY_WHITELIST_KEYWORDS(schema, subschema, pointer, ...) \ { \ static const std::unordered_set allowed{__VA_ARGS__}; \ for (const auto &entry : (subschema).as_object()) { \ - if (!allowed.contains(entry.first)) { \ + if (entry.first != "definitions" && !allowed.contains(entry.first)) { \ throw sourcemeta::blaze::CodegenUnsupportedKeywordError( \ (schema), (pointer), entry.first, \ "Unsupported keyword in subschema"); \ diff --git a/vendor/blaze/src/compiler/compile.cc b/vendor/blaze/src/compiler/compile.cc index ca9aa0dee..32dbdded6 100644 --- a/vendor/blaze/src/compiler/compile.cc +++ b/vendor/blaze/src/compiler/compile.cc @@ -1034,15 +1034,30 @@ auto compile(const Context &context, const SchemaContext &schema_context, .recompose_without_fragment() .value_or("") : schema_context.base.recompose_without_fragment().value_or("")}; + const auto new_vocabularies{ + entry.has_value() + ? context.frame.vocabularies(entry.value().get(), context.resolver) + : schema_context.vocabularies}; + + // The root check on dialects without boolean schemas cannot see a boolean + // reference target, as a target compiles with the pointer of wherever it + // sits rather than as the root of a resource. Only a reference lands here + // with a URI: a boolean a keyword admits in place of a schema comes in + // through the suffixes instead, so it stays out of this one's way + if (uri.has_value() && new_schema.is_boolean() && + new_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known::OPENAPI_3_0_BASE)) + [[unlikely]] { + throw sourcemeta::blaze::CompilerError( + new_base, sourcemeta::core::to_pointer(target), + "This dialect does not support boolean schemas"); + } return compile_subschema( context, {.relative_pointer = new_relative_pointer, .schema = new_schema, - .vocabularies = entry.has_value() - ? context.frame.vocabularies(entry.value().get(), - context.resolver) - : schema_context.vocabularies, + .vocabularies = new_vocabularies, .base = new_base, .is_property_name = schema_context.is_property_name}, {.keyword = dynamic_context.keyword, diff --git a/vendor/blaze/src/compiler/default_compiler_draft3.h b/vendor/blaze/src/compiler/default_compiler_draft3.h index 2e9a2411a..d5726f5fd 100644 --- a/vendor/blaze/src/compiler/default_compiler_draft3.h +++ b/vendor/blaze/src/compiler/default_compiler_draft3.h @@ -117,72 +117,24 @@ 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 +// A subschema that `narrows_type_away` drops is never compiled, so neither +// the refusal at the end of the dispatch table nor the keyword shape checks +// ever reach its members. The OpenAPI 3.0 Schema Object is the only dialect +// here whose documents those checks judge as a whole, reachable or not, so +// compile the subschema the way a reachable one compiles and discard what it +// yields: the checks are the point rather than the instructions +static inline auto +compile_pruned_subschema(const sourcemeta::blaze::Context &context, + const sourcemeta::blaze::SchemaContext &schema_context, + const sourcemeta::core::WeakPointer &suffix) -> void { 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); - } - } - } - } + sourcemeta::blaze::compile(context, schema_context, + sourcemeta::blaze::relative_dynamic_context(), + suffix, suffix); } static constexpr auto EXPECTED_UNIQUE_VALUES{ @@ -845,7 +797,9 @@ 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); + compile_pruned_subschema( + context, schema_context, + sourcemeta::blaze::make_weak_pointer(entry.first)); } return {}; @@ -1504,9 +1458,8 @@ 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); + compile_pruned_subschema(context, schema_context, + sourcemeta::core::EMPTY_WEAK_POINTER); return {}; } @@ -1859,9 +1812,8 @@ 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); + compile_pruned_subschema(context, schema_context, + sourcemeta::core::EMPTY_WEAK_POINTER); return {}; } @@ -2471,33 +2423,6 @@ 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, @@ -2511,12 +2436,12 @@ auto compiler_draft3_validation_type(const Context &context, // 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 + // `draft3_set_type_bits` 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())) { + !draft3_set_type_bits(value.to_string(), recognised)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), @@ -2910,7 +2835,7 @@ auto compiler_draft3_validation_type(const Context &context, continue; } - if (!assign_type_bit(types, element.to_string()) && !is_draft3) { + if (!draft3_set_type_bits(element.to_string(), types) && !is_draft3) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), @@ -3240,6 +3165,35 @@ auto is_openapi_numeric_format(const SchemaContext &schema_context, name == "double"; } +// Every OpenAPI version types `int32`, `int64`, `float`, and `double` over +// numbers, so the assertion each name compiles to is the same wherever it +// appears. The caller answers for `name` being one of the four +auto make_openapi_numeric_format_assertion( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const sourcemeta::core::JSON::String &name) -> Instruction { + if (name == "int32") { + return make( + sourcemeta::blaze::InstructionIndex::AssertionNumberIntegerBounded, + context, schema_context, dynamic_context, + ValueIntegerBounds{std::numeric_limits::min(), + std::numeric_limits::max()}); + } + + if (name == "int64") { + return make( + sourcemeta::blaze::InstructionIndex::AssertionNumberIntegerBounded, + context, schema_context, dynamic_context, + ValueIntegerBounds{std::numeric_limits::min(), + std::numeric_limits::max()}); + } + + return make(sourcemeta::blaze::InstructionIndex::AssertionNumberType, context, + schema_context, dynamic_context, + name == "float" ? ValueNumberType::Float + : ValueNumberType::Double); +} + auto compiler_draft3_validation_format(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, @@ -3277,26 +3231,8 @@ auto compiler_draft3_validation_format(const Context &context, // 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)); - } + Instructions instructions{make_openapi_numeric_format_assertion( + context, schema_context, dynamic_context, name)}; // Unlike the string formats, the annotation cannot be grouped by the // instance type, as `ControlGroupWhenType` does not take a number @@ -3424,24 +3360,12 @@ auto compiler_draft3_validation_format(const Context &context, schema_context.vocabularies.contains(Known::JSON_SCHEMA_DRAFT_7_HYPER)}; if (schema_context.vocabularies.contains(Known::OPENAPI_3_0_BASE)) { - // OpenAPI 3.0 defines these two for numbers, so they cannot resolve to a + // OpenAPI 3.0 defines these four 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 == "int32" || name == "int64" || name == "float" || + name == "double") { + return {make_openapi_numeric_format_assertion(context, schema_context, + dynamic_context, name)}; } if (name == "date-time") { diff --git a/vendor/blaze/src/compiler/default_compiler_draft4.h b/vendor/blaze/src/compiler/default_compiler_draft4.h index 879169a12..1e8034e16 100644 --- a/vendor/blaze/src/compiler/default_compiler_draft4.h +++ b/vendor/blaze/src/compiler/default_compiler_draft4.h @@ -44,13 +44,14 @@ 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), - in_place_applicators_may_be_empty(schema_context.vocabularies))) { + const auto allows_empty{ + in_place_applicators_may_be_empty(schema_context.vocabularies)}; + if (!is_schema_array(schema_context.schema.at(dynamic_context.keyword), + booleans_are_schemas(schema_context.vocabularies), + allows_empty)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), - EXPECTED_SCHEMA_ARRAY); + allows_empty ? EXPECTED_SCHEMA_ARRAY_ANY_SIZE : EXPECTED_SCHEMA_ARRAY); } Instructions children; @@ -94,13 +95,14 @@ 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), - in_place_applicators_may_be_empty(schema_context.vocabularies))) { + const auto allows_empty{ + in_place_applicators_may_be_empty(schema_context.vocabularies)}; + if (!is_schema_array(schema_context.schema.at(dynamic_context.keyword), + booleans_are_schemas(schema_context.vocabularies), + allows_empty)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), - EXPECTED_SCHEMA_ARRAY); + allows_empty ? EXPECTED_SCHEMA_ARRAY_ANY_SIZE : EXPECTED_SCHEMA_ARRAY); } // A disjunction over no branches can never be satisfied. Only a dialect that @@ -180,13 +182,14 @@ 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), - in_place_applicators_may_be_empty(schema_context.vocabularies))) { + const auto allows_empty{ + in_place_applicators_may_be_empty(schema_context.vocabularies)}; + if (!is_schema_array(schema_context.schema.at(dynamic_context.keyword), + booleans_are_schemas(schema_context.vocabularies), + allows_empty)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), - EXPECTED_SCHEMA_ARRAY); + allows_empty ? EXPECTED_SCHEMA_ARRAY_ANY_SIZE : EXPECTED_SCHEMA_ARRAY); } // A disjunction over no branches can never be satisfied. Only a dialect that diff --git a/vendor/blaze/src/test/test_parser.cc b/vendor/blaze/src/test/test_parser.cc index ccd127d2d..a3be1d281 100644 --- a/vendor/blaze/src/test/test_parser.cc +++ b/vendor/blaze/src/test/test_parser.cc @@ -24,6 +24,53 @@ inline auto frame_root(const sourcemeta::core::SchemaFrame &frame) return uri.value().get(); } +// A YAML file may hold more than one document, while a test case names a +// single instance rather than a stream of them. The combined reader hands back +// the first document and drops the rest without a word, and has no way of +// reporting that it did, so how it picks a reader is mirrored here in order to +// ask the YAML one what remained +inline auto read_single_document(const std::filesystem::path &path, + bool &more_documents) + -> sourcemeta::core::JSON { + const auto extension{path.extension()}; + if (extension == ".yaml" || extension == ".yml") { + return sourcemeta::core::read_yaml(path, more_documents); + } + + if (extension == ".json") { + return sourcemeta::core::read_json(path); + } + + try { + return sourcemeta::core::read_json(path); + } catch (const sourcemeta::core::JSONParseError &) { + return sourcemeta::core::read_yaml(path, more_documents); + } +} + +inline auto +read_single_document(const std::filesystem::path &path, bool &more_documents, + sourcemeta::core::JSON &output, + const sourcemeta::core::JSON::ParseCallback &callback) + -> void { + const auto extension{path.extension()}; + if (extension == ".yaml" || extension == ".yml") { + sourcemeta::core::read_yaml(path, more_documents, output, callback); + return; + } + + if (extension == ".json") { + sourcemeta::core::read_json(path, output, callback); + return; + } + + try { + sourcemeta::core::read_json(path, output, callback); + } catch (const sourcemeta::core::JSONParseError &) { + sourcemeta::core::read_yaml(path, more_documents, output, callback); + } +} + inline auto test_error_if( bool condition, const sourcemeta::core::PointerPositionTracker &tracker, const sourcemeta::core::Pointer &pointer, const char *message) -> void { @@ -106,7 +153,11 @@ auto TestCase::parse( } else if (test_case_json.defines("rdfPath")) { const std::filesystem::path rdf_path{sourcemeta::core::weakly_canonical( base_path / test_case_json.at("rdfPath").to_string())}; - rdf = sourcemeta::core::read_yaml_or_json(rdf_path); + bool more_documents{false}; + rdf = read_single_document(rdf_path, more_documents); + test_error_if(more_documents, tracker, location.concat("rdfPath"), + "The file referenced by the test case `rdfPath` property " + "must hold a single document"); test_error_if(!rdf.value().is_array(), tracker, location.concat("rdfPath"), "The document referenced by the test case `rdfPath` " "property must be an array"); @@ -125,7 +176,11 @@ auto TestCase::parse( const std::filesystem::path data_path{sourcemeta::core::weakly_canonical( base_path / test_case_json.at("dataPath").to_string())}; sourcemeta::core::JSON data{nullptr}; - sourcemeta::core::read_yaml_or_json(data_path, data, std::ref(data_tracker)); + bool more_documents{false}; + read_single_document(data_path, more_documents, data, std::ref(data_tracker)); + test_error_if(more_documents, tracker, location.concat("dataPath"), + "The file referenced by the test case `dataPath` property " + "must hold a single document"); return TestCase{.description = std::move(description), .valid = test_case_json.at("valid").to_boolean(), .data = std::move(data), diff --git a/vendor/core/src/core/jsonschema/bundle.cc b/vendor/core/src/core/jsonschema/bundle.cc index 7a55a8780..5fe3b546c 100644 --- a/vendor/core/src/core/jsonschema/bundle.cc +++ b/vendor/core/src/core/jsonschema/bundle.cc @@ -665,6 +665,14 @@ auto embed_references( depth == 0 ? default_base : std::string_view{}, remaining}; charge(remaining, frame); + // Framing holds the base it resolves against to its canonical spelling, so + // every reference destination it reports carries that spelling rather than + // the one the caller wrote. Settling it once here is what lets a reference + // be compared against it without parsing the base over again for each + const JSON::String canonical_base{depth == 0 && !default_base.empty() + ? URI::canonicalize(default_base) + : JSON::String{}}; + std::vector> deferred; std::vector> ref_rewrites; @@ -685,6 +693,16 @@ auto embed_references( "Could not resolve schema reference"); } + // And so is one whose base is the very document being bundled. A wrapper + // format puts the schemas it holds below a root that is no schema + // resource of its own, so nothing framed answers to the document's base + // and the test above passes it over, even though the fragment is all that + // leads nowhere + if (!canonical_base.empty() && reference.base == canonical_base) { + throw SchemaReferenceError(reference.destination, to_pointer(pointer), + "Could not resolve schema reference"); + } + if (reference.base.empty()) { throw SchemaReferenceError(reference.destination, to_pointer(pointer), "Could not resolve schema reference"); diff --git a/vendor/core/src/core/jsonschema/frame.cc b/vendor/core/src/core/jsonschema/frame.cc index 8958f28e1..b021aec08 100644 --- a/vendor/core/src/core/jsonschema/frame.cc +++ b/vendor/core/src/core/jsonschema/frame.cc @@ -402,6 +402,43 @@ auto canonicalize_pointer_fragment(sourcemeta::core::URI &uri) -> void { } } +// A static location and a dynamic one of the same spelling are two places, so +// a URI that names either is looked for under both +template +auto locate(const Locations &locations, + const sourcemeta::core::JSON::String &uri) + -> std::optional< + std::reference_wrapper> { + const auto static_result{ + locations.find({sourcemeta::core::SchemaReferenceType::Static, uri})}; + if (static_result != locations.cend()) { + return static_result->second; + } + + const auto dynamic_result{ + locations.find({sourcemeta::core::SchemaReferenceType::Dynamic, uri})}; + if (dynamic_result != locations.cend()) { + return dynamic_result->second; + } + + return std::nullopt; +} + +// The spelling that every location is framed under and that every reference +// destination is held to, which is what makes a URI that comes from anywhere +// else comparable to either of them +auto canonicalize_reference(const sourcemeta::core::JSON::String &uri) + -> std::optional { + try { + sourcemeta::core::URI result{uri}; + result.canonicalize(); + canonicalize_pointer_fragment(result); + return result.recompose(); + } catch (const sourcemeta::core::URIParseError &) { + return std::nullopt; + } +} + auto set_base_and_fragment(sourcemeta::core::SchemaFrame::Reference &entry) -> void { const std::string_view destination_view{entry.destination}; @@ -2080,19 +2117,24 @@ auto SchemaFrame::traverse( auto SchemaFrame::traverse(const std::string_view uri) const -> std::optional> { const sourcemeta::core::JSON::String uri_string{uri}; - const auto static_result{ - this->locations_.find({SchemaReferenceType::Static, uri_string})}; - if (static_result != this->locations_.cend()) { - return static_result->second; - } - - const auto dynamic_result{ - this->locations_.find({SchemaReferenceType::Dynamic, uri_string})}; - if (dynamic_result != this->locations_.cend()) { - return dynamic_result->second; + const auto direct{locate(this->locations_, uri_string)}; + if (direct.has_value()) { + return direct; + } + + // Framing writes every location down under the canonical spelling of its + // URI, and holds the destination of every reference to that same spelling, + // so one that reaches here spelled any other way names what a reference + // spelled identically would reach. RFC 3986 Section 6.2.2.2 leaves a + // percent-encoded reserved character where it stands, and RFC 6901 Section + // 6 only asks for the characters a fragment disallows to be encoded, so one + // pointer has more than one spelling to begin with + const auto canonical{canonicalize_reference(uri_string)}; + if (!canonical.has_value() || canonical.value() == uri_string) { + return std::nullopt; } - return std::nullopt; + return locate(this->locations_, canonical.value()); } auto SchemaFrame::traverse(const sourcemeta::core::WeakPointer &pointer) const diff --git a/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_capabilities.h b/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_capabilities.h index 9c317bc8b..2e79054a0 100644 --- a/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_capabilities.h +++ b/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_capabilities.h @@ -168,9 +168,9 @@ struct MCPServerCapabilities { /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#clientcapabilities -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_parse_client_capabilities(const MCPProtocolVersion version, - const sourcemeta::core::JSON &capabilities) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_parse_client_capabilities(const MCPProtocolVersion version, + const sourcemeta::core::JSON &capabilities) -> MCPClientCapabilities; /// @ingroup mcp @@ -199,9 +199,9 @@ auto mcp_parse_client_capabilities(const MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#clientcapabilities -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_serialize_client_capabilities( - const MCPProtocolVersion version, const MCPClientCapabilities &capabilities) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_serialize_client_capabilities(const MCPProtocolVersion version, + const MCPClientCapabilities &capabilities) -> sourcemeta::core::JSON; /// @ingroup mcp @@ -226,9 +226,9 @@ auto mcp_serialize_client_capabilities( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#servercapabilities -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_parse_server_capabilities(MCPProtocolVersion version, - const JSON &capabilities) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_parse_server_capabilities(MCPProtocolVersion version, + const JSON &capabilities) -> MCPServerCapabilities; /// @ingroup mcp @@ -252,9 +252,9 @@ auto mcp_parse_server_capabilities(MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#servercapabilities -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_serialize_server_capabilities( - MCPProtocolVersion version, const MCPServerCapabilities &capabilities) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_serialize_server_capabilities(MCPProtocolVersion version, + const MCPServerCapabilities &capabilities) -> JSON; } // namespace sourcemeta::core diff --git a/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_error.h b/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_error.h index f5333f52f..1878f878c 100644 --- a/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_error.h +++ b/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_error.h @@ -109,11 +109,11 @@ enum class MCPErrorContext : std::uint8_t { /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/index#responses -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_error(MCPProtocolVersion version, const JSON *identifier, - std::int64_t code, JSON::StringView message, - std::optional data = std::nullopt, - MCPErrorContext context = MCPErrorContext::Request) -> JSON; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_error(MCPProtocolVersion version, const JSON *identifier, + std::int64_t code, JSON::StringView message, + std::optional data = std::nullopt, + MCPErrorContext context = MCPErrorContext::Request) -> JSON; /// @ingroup mcp /// The legacy MCP error code returned when a requested resource cannot be @@ -196,8 +196,9 @@ constexpr std::int64_t MCP_CODE_UNSUPPORTED_PROTOCOL_VERSION{-32022}; /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#server-validation /// https://modelcontextprotocol.io/specification/2026-07-28/basic/index#_meta -constexpr auto mcp_error_code_to_http_status(const MCPProtocolVersion version, - const std::int64_t code) noexcept +[[nodiscard]] constexpr auto +mcp_error_code_to_http_status(const MCPProtocolVersion version, + const std::int64_t code) noexcept -> std::optional { if (version != MCPProtocolVersion::V_2026_07_28) { return std::nullopt; @@ -247,8 +248,8 @@ constexpr auto mcp_error_code_to_http_status(const MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codes -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_error_unsupported_protocol_version( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_error_unsupported_protocol_version( const MCPProtocolVersion version, const sourcemeta::core::JSON *identifier, const JSON::StringView requested, const std::span supported) @@ -282,8 +283,8 @@ auto mcp_make_error_unsupported_protocol_version( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codes -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_error_missing_required_capability( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_error_missing_required_capability( const MCPProtocolVersion version, const sourcemeta::core::JSON *identifier, sourcemeta::core::JSON required_capabilities, const JSON::StringView message) -> sourcemeta::core::JSON; @@ -310,11 +311,9 @@ auto mcp_make_error_missing_required_capability( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#schema-extension -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_error_header_mismatch(const MCPProtocolVersion version, - const sourcemeta::core::JSON *identifier, - const JSON::StringView message) - -> sourcemeta::core::JSON; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_error_header_mismatch( + const MCPProtocolVersion version, const sourcemeta::core::JSON *identifier, + const JSON::StringView message) -> sourcemeta::core::JSON; /// @ingroup mcp /// Build a JSON-RPC error response for header mismatch (-32020) with detailed @@ -346,13 +345,10 @@ auto mcp_make_error_header_mismatch(const MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#schema-extension -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_error_header_mismatch(const MCPProtocolVersion version, - const sourcemeta::core::JSON *identifier, - const JSON::StringView header_name, - const JSON::StringView header_value, - const JSON::StringView body_value) - -> sourcemeta::core::JSON; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_error_header_mismatch( + const MCPProtocolVersion version, const sourcemeta::core::JSON *identifier, + const JSON::StringView header_name, const JSON::StringView header_value, + const JSON::StringView body_value) -> sourcemeta::core::JSON; /// @ingroup mcp /// Build a JSON-RPC error response for header mismatch (-32020) reporting an @@ -379,11 +375,9 @@ auto mcp_make_error_header_mismatch(const MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#schema-extension -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_error_header_mismatch(const MCPProtocolVersion version, - const sourcemeta::core::JSON *identifier, - const JSON::StringView header_name, - const JSON::StringView header_value) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_error_header_mismatch( + const MCPProtocolVersion version, const sourcemeta::core::JSON *identifier, + const JSON::StringView header_name, const JSON::StringView header_value) -> sourcemeta::core::JSON; /// @ingroup mcp @@ -414,9 +408,9 @@ auto mcp_make_error_header_mismatch(const MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/resources#error-handling -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_error_resource_not_found(const MCPProtocolVersion version, - const sourcemeta::core::JSON *identifier) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_error_resource_not_found(const MCPProtocolVersion version, + const sourcemeta::core::JSON *identifier) -> sourcemeta::core::JSON; } // namespace sourcemeta::core diff --git a/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_headers.h b/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_headers.h index a10fdb6db..54993d054 100644 --- a/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_headers.h +++ b/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_headers.h @@ -141,8 +141,8 @@ struct MCPRequestMeta { /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/index#_meta -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_validate_request_meta(const sourcemeta::core::JSON &envelope) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_validate_request_meta(const sourcemeta::core::JSON &envelope) -> std::pair>; /// @ingroup mcp @@ -166,8 +166,8 @@ auto mcp_validate_request_meta(const sourcemeta::core::JSON &envelope) /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/index#_meta -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_validate_request_parameters(const JSON ¶meters) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_validate_request_parameters(const JSON ¶meters) -> std::pair>; /// @ingroup mcp @@ -216,8 +216,7 @@ auto mcp_validate_request_parameters(const JSON ¶meters) /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codes -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_error_request_meta( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_error_request_meta( const MCPProtocolVersion version, const sourcemeta::core::JSON *identifier, const MCPRequestMetaStatus status, const JSON::StringView requested, const std::span supported) @@ -242,8 +241,8 @@ auto mcp_make_error_request_meta( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#standard-request-headers -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_request_name_from_body(const sourcemeta::core::JSON &envelope) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_request_name_from_body(const sourcemeta::core::JSON &envelope) -> std::optional; /// @ingroup mcp @@ -285,8 +284,7 @@ auto mcp_request_name_from_body(const sourcemeta::core::JSON &envelope) /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#server-validation -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_validate_request_headers( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_validate_request_headers( const MCPProtocolVersion version, const std::optional &protocol_version_header, const std::optional &method_header, @@ -316,8 +314,8 @@ auto mcp_validate_request_headers( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#value-encoding -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_encode_header_value(JSON::StringView value) -> std::string; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_encode_header_value(JSON::StringView value) -> std::string; /// @ingroup mcp /// Validate a borrowed HTTP header collection. Names are case insensitive. @@ -363,8 +361,7 @@ auto mcp_encode_header_value(JSON::StringView value) -> std::string; /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#server-validation -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_validate_request_headers( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_validate_request_headers( MCPProtocolVersion version, std::span> headers, const JSON &envelope, std::span supported_versions) @@ -442,9 +439,9 @@ struct MCPHeaderParameter { /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#schema-extension -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_parameter_headers(std::span parameters, - const JSON &arguments) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_parameter_headers(std::span parameters, + const JSON &arguments) -> std::vector>; /// @ingroup mcp @@ -478,8 +475,7 @@ auto mcp_make_parameter_headers(std::span parameters, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#server-validation -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_validate_parameter_headers( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_validate_parameter_headers( MCPProtocolVersion version, const JSON &identifier, std::span parameters, const JSON &arguments, std::span> headers) diff --git a/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_logging.h b/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_logging.h index 707dc2b63..9c7b5e696 100644 --- a/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_logging.h +++ b/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_logging.h @@ -55,8 +55,8 @@ enum class MCPLogLevel : std::uint8_t { /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging -constexpr auto mcp_log_level_string(const MCPLogLevel level) noexcept - -> JSON::StringView { +[[nodiscard]] constexpr auto +mcp_log_level_string(const MCPLogLevel level) noexcept -> JSON::StringView { switch (level) { case MCPLogLevel::Debug: return "debug"; @@ -99,7 +99,8 @@ constexpr auto mcp_log_level_string(const MCPLogLevel level) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging -constexpr auto mcp_resolve_log_level(const JSON::StringView value) noexcept +[[nodiscard]] constexpr auto +mcp_resolve_log_level(const JSON::StringView value) noexcept -> std::optional { constexpr std::array LEVELS{ {MCPLogLevel::Debug, MCPLogLevel::Info, MCPLogLevel::Notice, @@ -135,9 +136,9 @@ constexpr auto mcp_resolve_log_level(const JSON::StringView value) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging -constexpr auto mcp_log_level_enabled(const MCPLogLevel level, - const MCPLogLevel threshold) noexcept - -> bool { +[[nodiscard]] constexpr auto +mcp_log_level_enabled(const MCPLogLevel level, + const MCPLogLevel threshold) noexcept -> bool { return level >= threshold; } diff --git a/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_protocol.h b/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_protocol.h index def5acdd9..0b4260cea 100644 --- a/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_protocol.h +++ b/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_protocol.h @@ -384,7 +384,7 @@ constexpr JSON::StringView MCP_METHOD_TASKS_RESULT{"tasks/result"}; /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning -constexpr auto +[[nodiscard]] constexpr auto mcp_protocol_version_string(const MCPProtocolVersion version) noexcept -> JSON::StringView { switch (version) { @@ -421,7 +421,7 @@ mcp_protocol_version_string(const MCPProtocolVersion version) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning -constexpr auto +[[nodiscard]] constexpr auto mcp_resolve_protocol_version(const JSON::StringView value) noexcept -> std::optional { if (value == MCP_PROTOCOL_VERSION_2026_07_28) { @@ -458,7 +458,7 @@ mcp_resolve_protocol_version(const JSON::StringView value) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning -constexpr auto +[[nodiscard]] constexpr auto mcp_protocol_version_at_least(const MCPProtocolVersion current, const MCPProtocolVersion minimum) noexcept -> bool { @@ -480,7 +480,7 @@ mcp_protocol_version_at_least(const MCPProtocolVersion current, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning -constexpr auto +[[nodiscard]] constexpr auto mcp_protocol_version_is_valid(const JSON::StringView value) noexcept -> bool { return mcp_resolve_protocol_version(value).has_value(); } @@ -502,7 +502,7 @@ mcp_protocol_version_is_valid(const JSON::StringView value) noexcept -> bool { /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/lifecycle -constexpr auto +[[nodiscard]] constexpr auto mcp_uses_initialization_handshake(const MCPProtocolVersion version) noexcept -> bool { return version != MCPProtocolVersion::V_2026_07_28; @@ -524,8 +524,8 @@ mcp_uses_initialization_handshake(const MCPProtocolVersion version) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/lifecycle -constexpr auto mcp_supports_ping(const MCPProtocolVersion version) noexcept - -> bool { +[[nodiscard]] constexpr auto +mcp_supports_ping(const MCPProtocolVersion version) noexcept -> bool { return version != MCPProtocolVersion::V_2026_07_28; } @@ -546,7 +546,7 @@ constexpr auto mcp_supports_ping(const MCPProtocolVersion version) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/index#_meta -constexpr auto +[[nodiscard]] constexpr auto mcp_requires_request_meta(const MCPProtocolVersion version) noexcept -> bool { return version == MCPProtocolVersion::V_2026_07_28; } @@ -568,7 +568,7 @@ mcp_requires_request_meta(const MCPProtocolVersion version) noexcept -> bool { /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema -constexpr auto +[[nodiscard]] constexpr auto mcp_requires_result_type(const MCPProtocolVersion version) noexcept -> bool { return version == MCPProtocolVersion::V_2026_07_28; } @@ -590,7 +590,7 @@ mcp_requires_result_type(const MCPProtocolVersion version) noexcept -> bool { /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cacheable-results -constexpr auto +[[nodiscard]] constexpr auto mcp_requires_cacheable_metadata(const MCPProtocolVersion version) noexcept -> bool { return version == MCPProtocolVersion::V_2026_07_28; @@ -613,7 +613,7 @@ mcp_requires_cacheable_metadata(const MCPProtocolVersion version) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/discover -constexpr auto +[[nodiscard]] constexpr auto mcp_supports_server_discover(const MCPProtocolVersion version) noexcept -> bool { return version == MCPProtocolVersion::V_2026_07_28; @@ -636,7 +636,7 @@ mcp_supports_server_discover(const MCPProtocolVersion version) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions -constexpr auto +[[nodiscard]] constexpr auto mcp_supports_subscriptions_listen(const MCPProtocolVersion version) noexcept -> bool { return version == MCPProtocolVersion::V_2026_07_28; @@ -659,8 +659,8 @@ mcp_supports_subscriptions_listen(const MCPProtocolVersion version) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr -constexpr auto mcp_supports_mrtr(const MCPProtocolVersion version) noexcept - -> bool { +[[nodiscard]] constexpr auto +mcp_supports_mrtr(const MCPProtocolVersion version) noexcept -> bool { return version == MCPProtocolVersion::V_2026_07_28; } @@ -681,7 +681,7 @@ constexpr auto mcp_supports_mrtr(const MCPProtocolVersion version) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http -constexpr auto +[[nodiscard]] constexpr auto mcp_supports_protocol_sessions(const MCPProtocolVersion version) noexcept -> bool { return version != MCPProtocolVersion::V_2026_07_28; @@ -704,7 +704,7 @@ mcp_supports_protocol_sessions(const MCPProtocolVersion version) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/tools -constexpr auto +[[nodiscard]] constexpr auto mcp_supports_output_schema(const MCPProtocolVersion version) noexcept -> bool { return mcp_protocol_version_at_least(version, MCPProtocolVersion::V_2025_06_18); @@ -726,7 +726,7 @@ mcp_supports_output_schema(const MCPProtocolVersion version) noexcept -> bool { /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/tools -constexpr auto +[[nodiscard]] constexpr auto mcp_supports_structured_content(const MCPProtocolVersion version) noexcept -> bool { return mcp_protocol_version_at_least(version, @@ -749,7 +749,7 @@ mcp_supports_structured_content(const MCPProtocolVersion version) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema -constexpr auto +[[nodiscard]] constexpr auto mcp_supports_resource_link_content(const MCPProtocolVersion version) noexcept -> bool { return mcp_protocol_version_at_least(version, @@ -772,7 +772,7 @@ mcp_supports_resource_link_content(const MCPProtocolVersion version) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema -constexpr auto +[[nodiscard]] constexpr auto mcp_supports_implementation_title(const MCPProtocolVersion version) noexcept -> bool { return mcp_protocol_version_at_least(version, @@ -796,7 +796,7 @@ mcp_supports_implementation_title(const MCPProtocolVersion version) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema -constexpr auto mcp_supports_implementation_description( +[[nodiscard]] constexpr auto mcp_supports_implementation_description( const MCPProtocolVersion version) noexcept -> bool { return mcp_protocol_version_at_least(version, MCPProtocolVersion::V_2025_11_25); @@ -819,7 +819,7 @@ constexpr auto mcp_supports_implementation_description( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema -constexpr auto mcp_supports_implementation_website_url( +[[nodiscard]] constexpr auto mcp_supports_implementation_website_url( const MCPProtocolVersion version) noexcept -> bool { return mcp_protocol_version_at_least(version, MCPProtocolVersion::V_2025_11_25); @@ -841,7 +841,7 @@ constexpr auto mcp_supports_implementation_website_url( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http -constexpr auto +[[nodiscard]] constexpr auto mcp_supports_jsonrpc_batching(const MCPProtocolVersion version) noexcept -> bool { return version == MCPProtocolVersion::V_2025_03_26; @@ -863,9 +863,9 @@ mcp_supports_jsonrpc_batching(const MCPProtocolVersion version) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema -constexpr auto mcp_is_request_method(const MCPProtocolVersion version, - const JSON::StringView method) noexcept - -> bool { +[[nodiscard]] constexpr auto +mcp_is_request_method(const MCPProtocolVersion version, + const JSON::StringView method) noexcept -> bool { switch (version) { case MCPProtocolVersion::V_2025_03_26: case MCPProtocolVersion::V_2025_06_18: @@ -931,7 +931,7 @@ constexpr auto mcp_is_request_method(const MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema -constexpr auto +[[nodiscard]] constexpr auto mcp_is_server_request_method(const MCPProtocolVersion version, const JSON::StringView method) noexcept -> bool { switch (version) { @@ -978,7 +978,7 @@ mcp_is_server_request_method(const MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema -constexpr auto +[[nodiscard]] constexpr auto mcp_is_notification_method(const MCPProtocolVersion version, const JSON::StringView method) noexcept -> bool { switch (version) { @@ -1040,7 +1040,7 @@ mcp_is_notification_method(const MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr -constexpr auto +[[nodiscard]] constexpr auto mcp_is_input_request_method(const MCPProtocolVersion version, const JSON::StringView method) noexcept -> bool { switch (version) { @@ -1078,9 +1078,9 @@ mcp_is_input_request_method(const MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema -constexpr auto mcp_supports_method(const MCPProtocolVersion version, - const JSON::StringView method) noexcept - -> bool { +[[nodiscard]] constexpr auto +mcp_supports_method(const MCPProtocolVersion version, + const JSON::StringView method) noexcept -> bool { switch (version) { case MCPProtocolVersion::V_2025_03_26: return method == MCP_METHOD_COMPLETION_COMPLETE || @@ -1202,8 +1202,8 @@ constexpr auto mcp_supports_method(const MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr -constexpr auto mcp_is_mrtr_method(const JSON::StringView method) noexcept - -> bool { +[[nodiscard]] constexpr auto +mcp_is_mrtr_method(const JSON::StringView method) noexcept -> bool { return method == MCP_METHOD_TOOLS_CALL || method == MCP_METHOD_RESOURCES_READ || method == MCP_METHOD_PROMPTS_GET; @@ -1223,8 +1223,8 @@ constexpr auto mcp_is_mrtr_method(const JSON::StringView method) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cacheable-results -constexpr auto mcp_is_cacheable_method(const JSON::StringView method) noexcept - -> bool { +[[nodiscard]] constexpr auto +mcp_is_cacheable_method(const JSON::StringView method) noexcept -> bool { return method == MCP_METHOD_SERVER_DISCOVER || method == MCP_METHOD_TOOLS_LIST || method == MCP_METHOD_PROMPTS_LIST || method == MCP_METHOD_RESOURCES_LIST || @@ -1250,7 +1250,7 @@ constexpr auto mcp_is_cacheable_method(const JSON::StringView method) noexcept /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema -constexpr auto +[[nodiscard]] constexpr auto mcp_is_client_notification_method(MCPProtocolVersion version, JSON::StringView method) noexcept -> bool { switch (version) { @@ -1294,7 +1294,7 @@ mcp_is_client_notification_method(MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema -constexpr auto +[[nodiscard]] constexpr auto mcp_is_server_notification_method(MCPProtocolVersion version, JSON::StringView method) noexcept -> bool { switch (version) { diff --git a/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_results.h b/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_results.h index 114ceb639..1bd364735 100644 --- a/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_results.h +++ b/vendor/core/src/core/mcp/include/sourcemeta/core/mcp_results.h @@ -45,8 +45,8 @@ enum class MCPCacheScope : std::uint8_t { /// assert(sourcemeta::core::mcp_cache_scope_string( /// sourcemeta::core::MCPCacheScope::Private) == "private"); /// ``` -constexpr auto mcp_cache_scope_string(const MCPCacheScope scope) noexcept - -> JSON::StringView { +[[nodiscard]] constexpr auto +mcp_cache_scope_string(const MCPCacheScope scope) noexcept -> JSON::StringView { switch (scope) { case MCPCacheScope::Public: return "public"; @@ -73,7 +73,8 @@ constexpr auto mcp_cache_scope_string(const MCPCacheScope scope) noexcept /// sourcemeta::core::MCPCacheScope::Private); /// assert(!sourcemeta::core::mcp_resolve_cache_scope("shared").has_value()); /// ``` -constexpr auto mcp_resolve_cache_scope(const JSON::StringView scope) noexcept +[[nodiscard]] constexpr auto +mcp_resolve_cache_scope(const JSON::StringView scope) noexcept -> std::optional { if (scope == "public") { return MCPCacheScope::Public; @@ -183,9 +184,9 @@ struct MCPDescriptorPresentation { /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#annotations -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_serialize_resource_annotations( - MCPProtocolVersion version, const MCPResourceAnnotations &annotations) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_serialize_resource_annotations(MCPProtocolVersion version, + const MCPResourceAnnotations &annotations) -> JSON; /// @ingroup mcp @@ -215,11 +216,10 @@ auto mcp_serialize_resource_annotations( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#imagecontent -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_image_block(MCPProtocolVersion version, JSON::StringView data, - JSON::StringView mime_type, - const MCPResourceAnnotations &annotations = {}) - -> JSON; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_image_block(MCPProtocolVersion version, JSON::StringView data, + JSON::StringView mime_type, + const MCPResourceAnnotations &annotations = {}) -> JSON; /// @ingroup mcp /// Build a base64 audio block, with the same contracts as the image builder. /// For example: @@ -240,11 +240,10 @@ auto mcp_make_image_block(MCPProtocolVersion version, JSON::StringView data, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#audiocontent -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_audio_block(MCPProtocolVersion version, JSON::StringView data, - JSON::StringView mime_type, - const MCPResourceAnnotations &annotations = {}) - -> JSON; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_audio_block(MCPProtocolVersion version, JSON::StringView data, + JSON::StringView mime_type, + const MCPResourceAnnotations &annotations = {}) -> JSON; /// @ingroup mcp /// Embed a checked text/blob resource-content entry in a content block. For /// example: @@ -269,9 +268,9 @@ auto mcp_make_audio_block(MCPProtocolVersion version, JSON::StringView data, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#embeddedresource -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_embedded_resource(MCPProtocolVersion version, JSON resource, - const MCPResourceAnnotations &annotations = {}) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_embedded_resource(MCPProtocolVersion version, JSON resource, + const MCPResourceAnnotations &annotations = {}) -> JSON; /// @ingroup mcp /// Build a blob resource-content entry. The caller supplies base64-encoded @@ -292,10 +291,9 @@ auto mcp_make_embedded_resource(MCPProtocolVersion version, JSON resource, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#blobresourcecontents -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_resource_blob_content(JSON::StringView uri, - JSON::StringView mime_type, - JSON::StringView blob) -> JSON; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_resource_blob_content(JSON::StringView uri, JSON::StringView mime_type, + JSON::StringView blob) -> JSON; /// @ingroup mcp /// Build an MCP `text` content block carrying the given text payload. For /// example: @@ -313,8 +311,8 @@ auto mcp_make_resource_blob_content(JSON::StringView uri, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#textcontent -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_text_block(const JSON::StringView text) -> sourcemeta::core::JSON; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_text_block(const JSON::StringView text) -> sourcemeta::core::JSON; /// @ingroup mcp /// Build an MCP content block referencing a resource by URI. This block type @@ -339,13 +337,10 @@ auto mcp_make_text_block(const JSON::StringView text) -> sourcemeta::core::JSON; /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#resourcelink -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_resource_link(const MCPProtocolVersion version, - const JSON::StringView uri, - const JSON::StringView name, - const JSON::StringView mime_type = {}, - const JSON::StringView description = {}) - -> sourcemeta::core::JSON; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_resource_link( + const MCPProtocolVersion version, const JSON::StringView uri, + const JSON::StringView name, const JSON::StringView mime_type = {}, + const JSON::StringView description = {}) -> sourcemeta::core::JSON; /// @ingroup mcp /// Decorate a successful MCP result object with version-appropriate envelope @@ -373,8 +368,7 @@ auto mcp_make_resource_link(const MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#result -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_decorate_result( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_decorate_result( const MCPProtocolVersion version, sourcemeta::core::JSON result, const std::optional &server_info = std::nullopt) -> sourcemeta::core::JSON; @@ -403,11 +397,9 @@ auto mcp_decorate_result( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cacheable-results -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_decorate_cacheable_result(const MCPProtocolVersion version, - sourcemeta::core::JSON result, - const MCPCachePolicy &cache_policy, - const JSON::StringView method) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_decorate_cacheable_result( + const MCPProtocolVersion version, sourcemeta::core::JSON result, + const MCPCachePolicy &cache_policy, const JSON::StringView method) -> sourcemeta::core::JSON; /// @ingroup mcp @@ -439,8 +431,7 @@ auto mcp_decorate_cacheable_result(const MCPProtocolVersion version, /// https://modelcontextprotocol.io/specification/2026-07-28/schema#emptyresult /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#jsonrpcresultresponse -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_empty_response( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_empty_response( const MCPProtocolVersion version, const sourcemeta::core::JSON &identifier, const std::optional &server_info = std::nullopt) -> sourcemeta::core::JSON; @@ -474,8 +465,7 @@ auto mcp_make_empty_response( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/tools -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_tool_success( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_tool_success( const MCPProtocolVersion version, const sourcemeta::core::JSON &identifier, sourcemeta::core::JSON result, const std::optional &server_info = std::nullopt) @@ -511,8 +501,7 @@ auto mcp_make_tool_success( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/tools -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_tool_success( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_tool_success( const MCPProtocolVersion version, const sourcemeta::core::JSON &identifier, sourcemeta::core::JSON structured, sourcemeta::core::JSON content_blocks, const std::optional &server_info = std::nullopt) @@ -543,8 +532,7 @@ auto mcp_make_tool_success( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/tools#error-handling -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_tool_error( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_tool_error( const MCPProtocolVersion version, const sourcemeta::core::JSON &identifier, const JSON::StringView message, const std::optional &server_info = std::nullopt) @@ -571,15 +559,14 @@ auto mcp_make_tool_error( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#resource -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_resource(const MCPProtocolVersion version, - const JSON::StringView uri, const JSON::StringView name, - const JSON::StringView mime_type, - const JSON::StringView description = {}, - const std::optional size = std::nullopt, - const std::optional priority = std::nullopt, - const MCPDescriptorPresentation &presentation = {}, - const MCPResourceAnnotations &annotations = {}) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_resource(const MCPProtocolVersion version, const JSON::StringView uri, + const JSON::StringView name, const JSON::StringView mime_type, + const JSON::StringView description = {}, + const std::optional size = std::nullopt, + const std::optional priority = std::nullopt, + const MCPDescriptorPresentation &presentation = {}, + const MCPResourceAnnotations &annotations = {}) -> sourcemeta::core::JSON; /// @ingroup mcp @@ -601,11 +588,9 @@ auto mcp_make_resource(const MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#textresourcecontents -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_resource_text_content(const JSON::StringView uri, - const JSON::StringView mime_type, - const JSON::StringView text) - -> sourcemeta::core::JSON; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_resource_text_content( + const JSON::StringView uri, const JSON::StringView mime_type, + const JSON::StringView text) -> sourcemeta::core::JSON; /// @ingroup mcp /// Wrap a pre-built array of content entries into the MCP `resources/read` @@ -635,11 +620,9 @@ auto mcp_make_resource_text_content(const JSON::StringView uri, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/resources -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_resources_read_result(const MCPProtocolVersion version, - sourcemeta::core::JSON contents, - const MCPCachePolicy &cache_policy) - -> sourcemeta::core::JSON; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_resources_read_result( + const MCPProtocolVersion version, sourcemeta::core::JSON contents, + const MCPCachePolicy &cache_policy) -> sourcemeta::core::JSON; /// @ingroup mcp /// Build a single entry for an MCP `resources/templates/list` response. For @@ -663,8 +646,7 @@ auto mcp_make_resources_read_result(const MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#resourcetemplate -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_resource_template( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_resource_template( const MCPProtocolVersion version, const JSON::StringView uri_template, const JSON::StringView name, const JSON::StringView description, const JSON::StringView mime_type, @@ -702,8 +684,7 @@ auto mcp_make_resource_template( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#tool -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_tool_descriptor( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_tool_descriptor( const MCPProtocolVersion version, const JSON::StringView name, const JSON::StringView description, sourcemeta::core::JSON input_schema, std::optional output_schema = std::nullopt, @@ -735,8 +716,7 @@ auto mcp_make_tool_descriptor( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cacheable-results -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_tools_list_result( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_tools_list_result( const MCPProtocolVersion version, sourcemeta::core::JSON tools, const std::optional next_cursor, const MCPCachePolicy &cache_policy) -> sourcemeta::core::JSON; @@ -771,8 +751,7 @@ auto mcp_make_tools_list_result( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cacheable-results -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_resources_list_result( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_resources_list_result( const MCPProtocolVersion version, sourcemeta::core::JSON resources, const std::optional next_cursor, const MCPCachePolicy &cache_policy) -> sourcemeta::core::JSON; @@ -801,8 +780,8 @@ auto mcp_make_resources_list_result( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cacheable-results -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_resource_templates_list_result( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_resource_templates_list_result( const MCPProtocolVersion version, sourcemeta::core::JSON resource_templates, const std::optional next_cursor, const MCPCachePolicy &cache_policy) -> sourcemeta::core::JSON; @@ -833,8 +812,7 @@ auto mcp_make_resource_templates_list_result( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cacheable-results -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_prompts_list_result( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_prompts_list_result( const MCPProtocolVersion version, sourcemeta::core::JSON prompts, const std::optional next_cursor, const MCPCachePolicy &cache_policy) -> sourcemeta::core::JSON; @@ -869,9 +847,9 @@ auto mcp_make_prompts_list_result( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/prompts -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_prompts_get_result(MCPProtocolVersion version, - JSON::StringView description, JSON messages) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_prompts_get_result(MCPProtocolVersion version, + JSON::StringView description, JSON messages) -> JSON; /// @ingroup mcp @@ -893,10 +871,10 @@ auto mcp_make_prompts_get_result(MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#completeresult -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_completion_result(MCPProtocolVersion version, JSON values, - std::optional total, - std::optional has_more) -> JSON; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_completion_result(MCPProtocolVersion version, JSON values, + std::optional total, + std::optional has_more) -> JSON; /// @ingroup mcp /// Serialize a complete MCP response for one of the six cacheable operations. @@ -944,6 +922,25 @@ auto mcp_write_result( std::optional cache_policy, std::optional server_info = std::nullopt) -> void; +/// @ingroup mcp +/// Serialize a complete MCP response for one of the six cacheable operations +/// using move semantics, decorating the provided result in-place. +/// A cache policy is mandatory for 2026-07-28. Pass `std::nullopt` for legacy +/// responses. A precomputed discovery result is held to the same rules as the +/// builder that produces one, so its advertised revisions must be non-empty, +/// must include the revision being served, and must each be one this library +/// models. +/// This writes JSON only. Stdio callers must append a newline themselves. +/// +/// @see +/// https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cacheable-results +SOURCEMETA_CORE_MCP_EXPORT +auto mcp_write_result( + std::ostream &stream, MCPProtocolVersion version, JSON::StringView method, + const JSON &identifier, JSON &&result, + std::optional cache_policy, + std::optional server_info = std::nullopt) -> void; + /// @ingroup mcp /// Build the JSON-RPC envelope returned in response to an MCP `initialize` /// request. The revision is the one the client asked for when the server @@ -1005,8 +1002,7 @@ auto mcp_write_result( /// /// @see /// https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_initialize_response( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_initialize_response( const sourcemeta::core::JSON &request, const MCPServerCapabilities &capabilities, const MCPImplementation &server, const std::span supported_versions, @@ -1045,8 +1041,7 @@ auto mcp_make_initialize_response( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/schema#discoverresult -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_server_discover_response( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_server_discover_response( const MCPProtocolVersion version, const sourcemeta::core::JSON &identifier, const MCPServerCapabilities &capabilities, const MCPImplementation &server, const std::span supported_versions, @@ -1098,8 +1093,7 @@ auto mcp_make_server_discover_response( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#supported-requests -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_input_required_response( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_make_input_required_response( const MCPProtocolVersion version, const JSON::StringView method, const sourcemeta::core::JSON &identifier, std::optional input_requests, @@ -1136,9 +1130,9 @@ auto mcp_make_input_required_response( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_request_input_responses(MCPProtocolVersion version, - const sourcemeta::core::JSON &envelope) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_request_input_responses(MCPProtocolVersion version, + const sourcemeta::core::JSON &envelope) -> const sourcemeta::core::JSON *; /// @ingroup mcp @@ -1162,9 +1156,9 @@ auto mcp_request_input_responses(MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_request_state(MCPProtocolVersion version, - const sourcemeta::core::JSON &envelope) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_request_state(MCPProtocolVersion version, + const sourcemeta::core::JSON &envelope) -> std::optional; /// @ingroup mcp @@ -1215,9 +1209,9 @@ struct MCPSubscriptionFilter { /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_parse_subscription_filter(MCPProtocolVersion version, - const JSON ¶meters) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_parse_subscription_filter(MCPProtocolVersion version, + const JSON ¶meters) -> std::optional; /// @ingroup mcp @@ -1242,9 +1236,9 @@ auto mcp_parse_subscription_filter(MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_intersect_subscription_filters(const MCPSubscriptionFilter &requested, - const MCPSubscriptionFilter &supported) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_intersect_subscription_filters(const MCPSubscriptionFilter &requested, + const MCPSubscriptionFilter &supported) -> MCPSubscriptionFilter; /// @ingroup mcp @@ -1269,8 +1263,8 @@ auto mcp_intersect_subscription_filters(const MCPSubscriptionFilter &requested, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/index#result-responses -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_result_type(const JSON &result) -> std::optional; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_result_type(const JSON &result) -> std::optional; /// @ingroup mcp /// Recognize base and explicitly negotiated extension result types. The @@ -1299,8 +1293,7 @@ auto mcp_result_type(const JSON &result) -> std::optional; /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/index#result-responses -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_resolve_result_type( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto mcp_resolve_result_type( MCPProtocolVersion version, const JSON &result, std::span supported_extension_types = {}) -> std::optional; @@ -1344,10 +1337,9 @@ auto mcp_resolve_result_type( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#client-requirements-basic-workflow -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_validate_continuation(MCPProtocolVersion version, - const JSON ¶meters, - const JSON &input_requests) -> bool; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_validate_continuation(MCPProtocolVersion version, const JSON ¶meters, + const JSON &input_requests) -> bool; /// @ingroup mcp /// Check continuation fields and the state token, where the expected state @@ -1382,11 +1374,10 @@ auto mcp_validate_continuation(MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_validate_continuation(MCPProtocolVersion version, - const JSON ¶meters, - const JSON &input_requests, - std::optional expected_state) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_validate_continuation(MCPProtocolVersion version, const JSON ¶meters, + const JSON &input_requests, + std::optional expected_state) -> bool; /// @ingroup mcp @@ -1419,13 +1410,12 @@ auto mcp_validate_continuation(MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_notification(MCPProtocolVersion version, bool from_server, - JSON::StringView method, JSON parameters, - std::optional subscription_id, - const MCPSubscriptionFilter *acknowledged = nullptr, - const MCPRequestMeta *request_context = nullptr) - -> JSON; +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_notification(MCPProtocolVersion version, bool from_server, + JSON::StringView method, JSON parameters, + std::optional subscription_id, + const MCPSubscriptionFilter *acknowledged = nullptr, + const MCPRequestMeta *request_context = nullptr) -> JSON; /// @ingroup mcp /// Build an acknowledgement notification for `subscriptions/listen` with a @@ -1454,8 +1444,8 @@ auto mcp_make_notification(MCPProtocolVersion version, bool from_server, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_subscription_acknowledged_notification( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_subscription_acknowledged_notification( const MCPProtocolVersion version, const sourcemeta::core::JSON &subscription_id, const MCPSubscriptionFilter &requested, @@ -1486,8 +1476,8 @@ auto mcp_make_subscription_acknowledged_notification( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_subscription_acknowledged_notification( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_subscription_acknowledged_notification( const MCPProtocolVersion version, const JSON::StringView subscription_id, const MCPSubscriptionFilter &requested, const MCPSubscriptionFilter &supported) -> sourcemeta::core::JSON; @@ -1508,8 +1498,8 @@ auto mcp_make_subscription_acknowledged_notification( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_make_subscription_close_response( +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_make_subscription_close_response( const MCPProtocolVersion version, const sourcemeta::core::JSON &identifier, const std::optional &server_info = std::nullopt) -> sourcemeta::core::JSON; @@ -1537,9 +1527,9 @@ auto mcp_make_subscription_close_response( /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_request_subscription_id(MCPProtocolVersion version, - const sourcemeta::core::JSON &envelope) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_request_subscription_id(MCPProtocolVersion version, + const sourcemeta::core::JSON &envelope) -> const sourcemeta::core::JSON *; /// @ingroup mcp @@ -1564,8 +1554,8 @@ auto mcp_request_subscription_id(MCPProtocolVersion version, /// /// @see /// https://modelcontextprotocol.io/specification/2026-07-28/server/tools -SOURCEMETA_CORE_MCP_EXPORT -auto mcp_tool_call_arguments(const sourcemeta::core::JSON &envelope) +[[nodiscard]] SOURCEMETA_CORE_MCP_EXPORT auto +mcp_tool_call_arguments(const sourcemeta::core::JSON &envelope) -> const sourcemeta::core::JSON *; } // namespace sourcemeta::core diff --git a/vendor/core/src/core/mcp/notifications.cc b/vendor/core/src/core/mcp/notifications.cc index a4ae2bac2..111a4001f 100644 --- a/vendor/core/src/core/mcp/notifications.cc +++ b/vendor/core/src/core/mcp/notifications.cc @@ -206,20 +206,20 @@ auto mcp_make_notification(const MCPProtocolVersion version, } else if (method == MCP_METHOD_NOTIFICATIONS_MESSAGE) { const auto *level{ parameters.try_at("level", sourcemeta::core::MCP_HASH_LEVEL)}; + const auto resolved_level{(level != nullptr && level->is_string()) + ? mcp_resolve_log_level(level->to_string()) + : std::nullopt}; internal::require( - (level != nullptr) && level->is_string() && - internal::valid_log_level(level->to_string()) && + resolved_level.has_value() && parameters.defines("data", sourcemeta::core::MCP_HASH_DATA) && optional_string(parameters, "logger", sourcemeta::core::MCP_HASH_LOGGER), "Invalid logging notification"); if (version == MCPProtocolVersion::V_2026_07_28 && from_server) { const auto threshold{mcp_resolve_log_level(*request_context->log_level)}; - internal::require( - threshold.has_value() && - mcp_log_level_enabled(*mcp_resolve_log_level(level->to_string()), - *threshold), - "Logging notification is below the request threshold"); + internal::require(threshold.has_value() && + mcp_log_level_enabled(*resolved_level, *threshold), + "Logging notification is below the request threshold"); } } else if (method == MCP_METHOD_NOTIFICATIONS_CANCELLED) { const auto *identifier{ diff --git a/vendor/core/src/core/mcp/parameter_headers.cc b/vendor/core/src/core/mcp/parameter_headers.cc index a67114e4b..877989dfa 100644 --- a/vendor/core/src/core/mcp/parameter_headers.cc +++ b/vendor/core/src/core/mcp/parameter_headers.cc @@ -64,8 +64,15 @@ auto require_parameters(const std::span parameters) auto safe_parameter(const JSON &value, const MCPHeaderParameter ¶meter) -> bool { if (parameter.type == JSON::Type::Integer) { - return value.is_integral() && value >= JSON{INT64_C(-9007199254740991)} && - value <= JSON{INT64_C(9007199254740991)}; + if (value.is_integer()) { + const auto integer{value.to_integer()}; + return integer >= INT64_C(-9007199254740991) && + integer <= INT64_C(9007199254740991); + } + static const JSON MIN_SAFE_INTEGER{INT64_C(-9007199254740991)}; + static const JSON MAX_SAFE_INTEGER{INT64_C(9007199254740991)}; + return value.is_integral() && value >= MIN_SAFE_INTEGER && + value <= MAX_SAFE_INTEGER; } return value.type() == parameter.type; } diff --git a/vendor/core/src/core/mcp/results.cc b/vendor/core/src/core/mcp/results.cc index d5f521d30..eee63d2b7 100644 --- a/vendor/core/src/core/mcp/results.cc +++ b/vendor/core/src/core/mcp/results.cc @@ -207,14 +207,8 @@ namespace { // is reachable from the interim and subscription results too, whose own result // type rules out the complete-result decorator // https://modelcontextprotocol.io/specification/2026-07-28/basic/index#responses -auto attach_server_info(const MCPProtocolVersion version, - sourcemeta::core::JSON &result, - const std::optional &server_info) - -> void { - if (!server_info.has_value() || !mcp_requires_result_type(version)) { - return; - } - auto info{internal::make_implementation(version, *server_info)}; +auto attach_server_info(sourcemeta::core::JSON &result, + sourcemeta::core::JSON &&info) -> void { if (auto *meta{result.try_at("_meta", MCP_HASH_META)}; meta != nullptr && meta->is_object()) { // Caller-supplied metadata may already identify the server @@ -227,6 +221,17 @@ auto attach_server_info(const MCPProtocolVersion version, } } +auto attach_server_info(const MCPProtocolVersion version, + sourcemeta::core::JSON &result, + const std::optional &server_info) + -> void { + if (!server_info.has_value() || !mcp_requires_result_type(version)) { + return; + } + attach_server_info(result, + internal::make_implementation(version, *server_info)); +} + auto decorate_result( const MCPProtocolVersion version, sourcemeta::core::JSON &result, const std::optional &server_info = std::nullopt) @@ -256,12 +261,20 @@ auto decorate_result( "or extension results"); } + auto info{ + server_info.has_value() + ? std::optional{internal::make_implementation( + version, *server_info)} + : std::nullopt}; + if (result.try_at("resultType", MCP_HASH_RESULT_TYPE) == nullptr) { result.assign_assume_new("resultType", sourcemeta::core::JSON{"complete"}, MCP_HASH_RESULT_TYPE); } - attach_server_info(version, result, server_info); + if (info) { + attach_server_info(result, std::move(*info)); + } } } // namespace @@ -1066,11 +1079,11 @@ auto mcp_make_completion_result(const MCPProtocolVersion version, JSON values, return result; } -auto mcp_write_result(std::ostream &stream, const MCPProtocolVersion version, - const JSON::StringView method, const JSON &identifier, - const JSON &result, - const std::optional cache_policy, - const std::optional server_info) +namespace { +auto validate_write_result(const MCPProtocolVersion version, + const JSON::StringView method, + const JSON &identifier, const JSON &result, + const std::optional &cache_policy) -> void { internal::require(internal::valid_id(identifier), "Invalid MCP identifier"); internal::require(mcp_is_request_method(version, method) && @@ -1102,6 +1115,10 @@ auto mcp_write_result(std::ostream &stream, const MCPProtocolVersion version, internal::require(cache_policy.has_value(), "Cacheable results require a policy"); } + if (cache_policy) { + internal::require(cache_policy->ttl_ms >= 0, + "A cache time to live cannot be negative"); + } const JSON *entries = nullptr; if (method == MCP_METHOD_TOOLS_LIST) { entries = result.try_at("tools", sourcemeta::core::MCP_HASH_TOOLS); @@ -1164,11 +1181,46 @@ auto mcp_write_result(std::ostream &stream, const MCPProtocolVersion version, cursor) { internal::require(cursor->is_string(), "Invalid result cursor"); } - auto payload{result}; - decorate_result(version, payload, server_info); +} + +auto write_result_impl(std::ostream &stream, const MCPProtocolVersion version, + const JSON::StringView method, const JSON &identifier, + JSON &&result, + const std::optional &cache_policy, + const std::optional &server_info) + -> void { + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + const auto envelope_identifier{identifier}; + const JSON::String operation{method}; + decorate_result(version, result, server_info); if (cache_policy) { - decorate_cacheable_result(version, payload, *cache_policy, method); + decorate_cacheable_result(version, result, *cache_policy, operation); } - stringify(jsonrpc_make_success(identifier, std::move(payload)), stream); + stringify(jsonrpc_make_success(envelope_identifier, std::move(result)), + stream); +} +} // namespace + +auto mcp_write_result(std::ostream &stream, const MCPProtocolVersion version, + const JSON::StringView method, const JSON &identifier, + JSON &&result, + const std::optional cache_policy, + const std::optional server_info) + -> void { + validate_write_result(version, method, identifier, result, cache_policy); + write_result_impl(stream, version, method, identifier, std::move(result), + cache_policy, server_info); +} + +auto mcp_write_result(std::ostream &stream, const MCPProtocolVersion version, + const JSON::StringView method, const JSON &identifier, + const JSON &result, + const std::optional cache_policy, + const std::optional server_info) + -> void { + validate_write_result(version, method, identifier, result, cache_policy); + auto payload{result}; + write_result_impl(stream, version, method, identifier, std::move(payload), + cache_policy, server_info); } } // namespace sourcemeta::core diff --git a/vendor/core/src/core/mcp/validation.h b/vendor/core/src/core/mcp/validation.h index 53accb05a..1bba501eb 100644 --- a/vendor/core/src/core/mcp/validation.h +++ b/vendor/core/src/core/mcp/validation.h @@ -165,12 +165,6 @@ inline auto icon_size(const JSON::StringView value) -> bool { }); } -inline auto strings(const JSON &value) -> bool { - return value.is_array() && - std::all_of(value.as_array().begin(), value.as_array().end(), - [](const auto &item) { return item.is_string(); }); -} - inline auto annotations(const JSON &value, const bool received = false) -> bool { const auto *item{ @@ -194,11 +188,12 @@ inline auto annotations(const JSON &value, const bool received = false) if (const auto *audience{ field(*item, "audience", sourcemeta::core::MCP_HASH_AUDIENCE)}; audience != nullptr) { - if (!strings(*audience)) { + if (!audience->is_array_of_strings()) { return false; } for (const auto &role : audience->as_array()) { - if (role.to_string() != "user" && role.to_string() != "assistant") { + const auto &role_name{role.to_string()}; + if (role_name != "user" && role_name != "assistant") { return false; } } @@ -228,7 +223,7 @@ inline auto icons(const JSON &value, const bool received = false) -> bool { if (const auto *sizes{ field(item, "sizes", sourcemeta::core::MCP_HASH_SIZES)}; sizes != nullptr) { - if (!strings(*sizes)) { + if (!sizes->is_array_of_strings()) { return false; } @@ -240,10 +235,14 @@ inline auto icons(const JSON &value, const bool received = false) -> bool { } if (const auto *theme{ field(item, "theme", sourcemeta::core::MCP_HASH_THEME)}; - (theme != nullptr) && - (!theme->is_string() || - (theme->to_string() != "light" && theme->to_string() != "dark"))) { - return false; + theme != nullptr) { + if (!theme->is_string()) { + return false; + } + const auto &theme_name{theme->to_string()}; + if (theme_name != "light" && theme_name != "dark") { + return false; + } } } return true; @@ -314,7 +313,7 @@ inline auto metadata_key(const JSON::StringView key, const bool prefix_required) const auto slash{key.find('/')}; JSON::StringView name{key}; if (slash == JSON::StringView::npos) { - if (prefix_required) { + if (prefix_required || key.empty()) { return false; } } else { @@ -361,7 +360,7 @@ inline auto sampling_block(const JSON &value, const bool received = false) value)) { return false; } - const auto name{type->to_string()}; + const auto &name{type->to_string()}; if (name == "text" || name == "image" || name == "audio") { return sourcemeta::core::internal::valid_content( MCPProtocolVersion::V_2026_07_28, value, received); @@ -402,7 +401,7 @@ inline auto sampling_message_block(const JSON &block, const bool tools, if (!sampling_block(block)) { return false; } - const auto type{ + const auto &type{ block.at("type", sourcemeta::core::MCP_HASH_TYPE).to_string()}; if (type == "tool_result") { return tools && answering && role == "user" && @@ -484,7 +483,7 @@ inline auto sampling_options(const JSON ¶ms) -> bool { } if (const auto *stop{field(params, "stopSequences", sourcemeta::core::MCP_HASH_STOP_SEQUENCES)}; - (stop != nullptr) && !strings(*stop)) { + (stop != nullptr) && !stop->is_array_of_strings()) { return false; } if (const auto *choice{ @@ -914,7 +913,7 @@ inline auto valid_resource(const MCPProtocolVersion version, const JSON &value, validation::field(value, "size", sourcemeta::core::MCP_HASH_SIZE)}; return validation::base(value, received) && validation::annotations(value, received) && - ((size == nullptr) || (size->is_integral() && *size >= JSON{0})); + ((size == nullptr) || (size->is_integral() && size->is_positive())); } inline auto valid_content(const MCPProtocolVersion version, const JSON &value, @@ -928,7 +927,7 @@ inline auto valid_content(const MCPProtocolVersion version, const JSON &value, if ((type == nullptr) || !type->is_string()) { return false; } - const auto name{type->to_string()}; + const auto &name{type->to_string()}; if (name == "text") { return validation::string_field(value, "text", sourcemeta::core::MCP_HASH_TEXT, true); @@ -1002,7 +1001,7 @@ inline auto valid_input_requests(const JSON &value, !valid_meta(MCPProtocolVersion::V_2026_07_28, *params))) { return false; } - const auto name{method->to_string()}; + const auto &name{method->to_string()}; if (name == MCP_METHOD_ROOTS_LIST) { if (!capabilities.roots) { return false; @@ -1147,7 +1146,7 @@ inline auto valid_input_response(const JSON::StringView method, } for (const auto &entry : content->as_object()) { if (!entry.second.is_string() && !entry.second.is_number() && - !entry.second.is_boolean() && !validation::strings(entry.second)) { + !entry.second.is_boolean() && !entry.second.is_array_of_strings()) { return false; } } diff --git a/vendor/core/src/core/openapi/bundle.cc b/vendor/core/src/core/openapi/bundle.cc index c7614e627..eae16ace4 100644 --- a/vendor/core/src/core/openapi/bundle.cc +++ b/vendor/core/src/core/openapi/bundle.cc @@ -55,20 +55,36 @@ struct OpenAPIPending { }; // What the description reaches for and does not hold. A reference that names -// the document being read and lands nowhere is left alone, as OpenAPI -// Specification 3.1.1, Section 4.8.23 holds a `$ref` to the form of a URI and -// says nothing about it having to resolve, so there is nothing to fetch here -// and nothing this specification lets us report +// the document being read and lands nowhere is settled here rather than +// collected, as there is no document left to fetch that could supply the +// place it named. Nothing asks for that. OpenAPI Specification 3.1.1, Section +// 4.8.23 holds a `$ref` to the form of a URI and says nothing about it having +// to resolve, so turning one of these down is a choice this makes rather than +// a rule it follows. +// +// What makes the choice is that nothing is left to wait for. The one restraint +// this specification writes down is Section 4.3.1's "requirement to parse +// complete documents before deeming a Schema Object reference to be +// unresolvable", and a Schema Object reference never reaches here, so holding +// back would be waiting on a document that no reference named. Saying nothing +// instead would hand back a result that the very same reading calls +// incomplete, while a reference into another document that lands nowhere is +// already turned down once that document has been read auto pending(const sourcemeta::core::OpenAPIWalk &walk) -> std::vector { std::vector result; for (const auto &entry : walk.references) { - if (walk.locations.contains(entry.second.destination) || - sourcemeta::core::openapi_within_document(entry.second.destination, - walk.base)) { + if (walk.locations.contains(entry.second.destination)) { continue; } + if (sourcemeta::core::openapi_within_document(entry.second.destination, + walk.base)) { + throw sourcemeta::core::OpenAPIReferenceError{ + walk.base, entry.second.origin, entry.second.destination, + "This reference must name a place within the document it points at"}; + } + result.push_back({.origin = entry.second.origin, .destination = entry.second.destination, .expected = entry.second.expected, @@ -83,12 +99,18 @@ auto pending(const sourcemeta::core::OpenAPIWalk &walk) // references above only because a single one of those Objects may name // several schemes, which is more than one entry keyed by where it sits for (const auto &entry : walk.security_references) { - if (walk.locations.contains(entry.second.destination) || - sourcemeta::core::openapi_within_document(entry.second.destination, - walk.base)) { + if (walk.locations.contains(entry.second.destination)) { continue; } + if (sourcemeta::core::openapi_within_document(entry.second.destination, + walk.base)) { + throw sourcemeta::core::OpenAPIReferenceError{ + walk.base, entry.second.origin, entry.second.destination, + "This security requirement must name a Security Scheme Object " + "within the document it points at"}; + } + result.push_back({.origin = entry.second.origin, .destination = entry.second.destination, .expected = entry.second.expected, diff --git a/vendor/core/src/core/openapi/include/sourcemeta/core/openapi.h b/vendor/core/src/core/openapi/include/sourcemeta/core/openapi.h index c68e137c2..b976d99ff 100644 --- a/vendor/core/src/core/openapi/include/sourcemeta/core/openapi.h +++ b/vendor/core/src/core/openapi/include/sourcemeta/core/openapi.h @@ -996,9 +996,10 @@ using OpenAPIResolverResult = OwnedOrReference; /// @ingroup openapi /// How bundling reaches the other documents that an OpenAPI Description is -/// split across. Every document handed back must itself be an OpenAPI -/// Description, and a URI that names nothing is reported by handing back no -/// value. For example: +/// split across. A URI that names nothing is reported by handing back no +/// value, and whatever else a URI names is handed back as it stands, for +/// bundling to decide whether it is a document a description may span. For +/// example: /// /// ```cpp /// #include diff --git a/vendor/core/src/core/yaml/include/sourcemeta/core/yaml.h b/vendor/core/src/core/yaml/include/sourcemeta/core/yaml.h index 8274b590d..39a75960c 100644 --- a/vendor/core/src/core/yaml/include/sourcemeta/core/yaml.h +++ b/vendor/core/src/core/yaml/include/sourcemeta/core/yaml.h @@ -70,6 +70,26 @@ auto parse_yaml(std::basic_istream &stream) SOURCEMETA_CORE_YAML_EXPORT auto parse_yaml(const JSON::String &input) -> JSON; +/// @ingroup yaml +/// +/// Create a JSON document from a YAML string, reporting whether the string +/// held more documents after the one that was read. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// #include +/// +/// bool more_documents{false}; +/// const sourcemeta::core::JSON document = +/// sourcemeta::core::parse_yaml("hello: world\n---\nsecond: document", +/// more_documents); +/// assert(more_documents); +/// ``` +SOURCEMETA_CORE_YAML_EXPORT +auto parse_yaml(const JSON::String &input, bool &more_documents) -> JSON; + /// @ingroup yaml /// /// Read a JSON document from a file location that represents a YAML file. The @@ -92,6 +112,28 @@ auto parse_yaml(const JSON::String &input) -> JSON; SOURCEMETA_CORE_YAML_EXPORT auto read_yaml(const std::filesystem::path &path) -> JSON; +/// @ingroup yaml +/// +/// Read a JSON document from a file location that represents a YAML file, +/// reporting whether the file held more documents after the one that was +/// read. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// #include +/// #include +/// +/// bool more_documents{false}; +/// const std::filesystem::path path{"test.yaml"}; +/// const sourcemeta::core::JSON document = +/// sourcemeta::core::read_yaml(path, more_documents); +/// assert(!more_documents); +/// ``` +SOURCEMETA_CORE_YAML_EXPORT +auto read_yaml(const std::filesystem::path &path, bool &more_documents) -> JSON; + /// @ingroup yaml /// /// Parse a YAML document from a C++ standard input stream into an existing @@ -113,6 +155,18 @@ SOURCEMETA_CORE_YAML_EXPORT auto parse_yaml(const JSON::String &input, JSON &output, const JSON::ParseCallback &callback) -> void; +/// @ingroup yaml +/// +/// Parse a YAML string into an existing JSON value, invoking the given +/// callback during parsing and reporting whether the string held more +/// documents after the one that was read. The result is constructed directly +/// into the given reference rather than returned by value to ensure that +/// references passed through the parse callback remain valid after parsing +/// completes. +SOURCEMETA_CORE_YAML_EXPORT +auto parse_yaml(const JSON::String &input, bool &more_documents, JSON &output, + const JSON::ParseCallback &callback) -> void; + /// @ingroup yaml /// /// Read a YAML file into an existing JSON value, invoking the given callback @@ -123,6 +177,17 @@ SOURCEMETA_CORE_YAML_EXPORT auto read_yaml(const std::filesystem::path &path, JSON &output, const JSON::ParseCallback &callback) -> void; +/// @ingroup yaml +/// +/// Read a YAML file into an existing JSON value, invoking the given callback +/// during parsing and reporting whether the file held more documents after +/// the one that was read. The result is constructed directly into the given +/// reference rather than returned by value to ensure that references passed +/// through the parse callback remain valid after parsing completes. +SOURCEMETA_CORE_YAML_EXPORT +auto read_yaml(const std::filesystem::path &path, bool &more_documents, + JSON &output, const JSON::ParseCallback &callback) -> void; + /// @ingroup yaml /// /// Read a JSON document from a file location that represents a YAML file or a diff --git a/vendor/core/src/core/yaml/parser.h b/vendor/core/src/core/yaml/parser.h index 3b60fd2c3..73a04bf61 100644 --- a/vendor/core/src/core/yaml/parser.h +++ b/vendor/core/src/core/yaml/parser.h @@ -237,7 +237,12 @@ class Parser { } } - auto validate_end_of_stream() -> void { + // Whether anything stood after the document that was read, which is what a + // caller holding the whole input has no other way of learning. YAML 1.2.2 + // Section 9.2 makes a stream "zero or more documents", so one that carries + // more than the single document handed back is well formed rather than + // malformed, and saying so is all there is to report + auto validate_end_of_stream() -> bool { // Whatever stands after the document that was read is checked rather than // handed over, so nothing of it is announced to whoever is listening for // the one document the caller gets @@ -258,7 +263,7 @@ class Parser { bool saw_document_end{this->document_ended_}; this->skip_document_ends(token, saw_document_end); if (token.type == TokenType::StreamEnd) { - return; + return false; } while (token.type != TokenType::StreamEnd) { // Section 9.2: a document that is not terminated by a document end @@ -271,7 +276,7 @@ class Parser { opened_document = true; token = this->next_token(); if (token.type == TokenType::StreamEnd) { - return; + return true; } // Section 8.2.3 admits an empty node, so an explicit document may // carry nothing of its own and be closed by its end marker directly @@ -310,6 +315,8 @@ class Parser { token = this->next_token(); this->skip_document_ends(token, saw_document_end); } + + return true; } private: diff --git a/vendor/core/src/core/yaml/yaml.cc b/vendor/core/src/core/yaml/yaml.cc index 6aed27ccb..d99620307 100644 --- a/vendor/core/src/core/yaml/yaml.cc +++ b/vendor/core/src/core/yaml/yaml.cc @@ -52,6 +52,11 @@ auto parse_yaml(std::basic_istream &stream) } auto parse_yaml(const JSON::String &input) -> JSON { + bool more_documents{false}; + return parse_yaml(input, more_documents); +} + +auto parse_yaml(const JSON::String &input, bool &more_documents) -> JSON { yaml::Lexer lexer{input}; yaml::Parser parser{&lexer, nullptr}; auto result{parser.parse()}; @@ -60,22 +65,22 @@ auto parse_yaml(const JSON::String &input) -> JSON { // so whatever stands after the first is content nothing could reach. The // stream overloads are the ones that read a document at a time, and those // leave the rest where the next call will find it - parser.validate_end_of_stream(); + more_documents = parser.validate_end_of_stream(); return result; } auto read_yaml(const std::filesystem::path &path) -> JSON { + bool more_documents{false}; + return read_yaml(path, more_documents); +} + +auto read_yaml(const std::filesystem::path &path, bool &more_documents) + -> JSON { const auto input{read_file_to_string(path)}; try { - yaml::Lexer lexer{input}; - yaml::Parser parser{&lexer, nullptr}; - auto result{parser.parse()}; - - parser.validate_end_of_stream(); - - return result; + return parse_yaml(input, more_documents); } catch (const YAMLParseError &error) { // For producing better error messages throw YAMLFileParseError(path, error); @@ -101,23 +106,31 @@ auto parse_yaml(std::basic_istream &stream, auto parse_yaml(const JSON::String &input, JSON &output, const JSON::ParseCallback &callback) -> void { + bool more_documents{false}; + parse_yaml(input, more_documents, output, callback); +} + +auto parse_yaml(const JSON::String &input, bool &more_documents, JSON &output, + const JSON::ParseCallback &callback) -> void { yaml::Lexer lexer{input}; yaml::Parser parser{&lexer, &callback}; output = parser.parse(); - parser.validate_end_of_stream(); + more_documents = parser.validate_end_of_stream(); } auto read_yaml(const std::filesystem::path &path, JSON &output, const JSON::ParseCallback &callback) -> void { + bool more_documents{false}; + read_yaml(path, more_documents, output, callback); +} + +auto read_yaml(const std::filesystem::path &path, bool &more_documents, + JSON &output, const JSON::ParseCallback &callback) -> void { const auto input{read_file_to_string(path)}; try { - yaml::Lexer lexer{input}; - yaml::Parser parser{&lexer, &callback}; - output = parser.parse(); - - parser.validate_end_of_stream(); + parse_yaml(input, more_documents, output, callback); } catch (const YAMLParseError &error) { // For producing better error messages throw YAMLFileParseError(path, error);