diff --git a/DEPENDENCIES b/DEPENDENCIES index c5a3fb83c..b47b5fe24 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,5 +1,5 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core 783d6646d159d0862755aca042dda0c426e918b2 -jsonbinpack https://github.com/sourcemeta/jsonbinpack 9c6be797ea4f2a586deff6f871506e7f729f3e8e -blaze https://github.com/sourcemeta/blaze 049a75b2a45c1178e6d484e349c449fe4b36ae4d +core https://github.com/sourcemeta/core d0bf624114232b8397bddad9a0c0e192c7315deb +jsonbinpack https://github.com/sourcemeta/jsonbinpack 147050385dce7086a28ce93e18a4c3baa9719136 +blaze https://github.com/sourcemeta/blaze 1b09fa0898cc986cc501322db2680d3fd6f093bf ctrf https://github.com/ctrf-io/ctrf 93ea827d951390190171d37443bff169cf47c808 diff --git a/docs/bundle.markdown b/docs/bundle.markdown index 006fef98d..832170f3a 100644 --- a/docs/bundle.markdown +++ b/docs/bundle.markdown @@ -56,11 +56,11 @@ two spaces. Use `--indentation/-n` to set the width in either case. OpenAPI Descriptions -------------------- -If the input is an OpenAPI spec v3.1 or v3.2, the `bundle` command treats it as -an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather than -as a schema, and bundles it as one: every other document the description spans -is embedded into the Components Object, and every reference that reached one is -rewritten to name where it landed. Any other revision, such as v3.0, is +If the input is an OpenAPI spec v3.0, v3.1 or v3.2, the `bundle` command treats +it as an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather +than as a schema, and bundles it as one: every other document the description +spans is embedded into the Components Object, and every reference that reached +one is rewritten to name where it landed. An unsupported `openapi` revision is rejected rather than read as a schema. The result is ordered exactly as [`jsonschema fmt`](./format.markdown) would order it. diff --git a/docs/compile.markdown b/docs/compile.markdown index 08a289018..9ba6c8c4e 100644 --- a/docs/compile.markdown +++ b/docs/compile.markdown @@ -88,11 +88,11 @@ jsonschema compile path/to/my/schema.json --entrypoint '/$defs/MyType' > templat ### Compile a Schema Object of an OpenAPI description -If the input is an OpenAPI spec v3.1 or v3.2, the `compile` command treats it as -an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather than -as a schema. Any other revision, such as v3.0, is rejected rather than read as a -schema. A description declares many schemas and no root one, so you must say -which to compile with `--entrypoint/-p`, given as a JSON Pointer into the +If the input is an OpenAPI spec v3.0, v3.1 or v3.2, the `compile` command treats +it as an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather +than as a schema. An unsupported `openapi` revision is rejected rather than read +as a schema. A description declares many schemas and no root one, so you must +say which to compile with `--entrypoint/-p`, given as a JSON Pointer into the description or as the identifier a Schema Object declares. ```sh diff --git a/docs/format.markdown b/docs/format.markdown index 8da5f26bc..a0f36a05f 100644 --- a/docs/format.markdown +++ b/docs/format.markdown @@ -26,11 +26,12 @@ check their adherence on a continuous integration environment. OpenAPI Descriptions -------------------- -If the input is an OpenAPI spec v3.1 or v3.2, the `fmt` command treats it as an -[OpenAPI](https://spec.openapis.org/oas/latest.html) description rather than as -a schema. The fields of the description are ordered following an OpenAPI aware -convention, and every Schema Object it holds is formatted as a schema would be. -Any other revision, such as v3.0, is rejected rather than read as a schema. +If the input is an OpenAPI spec v3.0, v3.1 or v3.2, the `fmt` command treats it +as an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather +than as a schema. The fields of the description are ordered following an OpenAPI +aware convention, and every Schema Object it holds is formatted as a schema +would be. An unsupported `openapi` revision is rejected rather than read as a +schema. `--keep-ordering/-k` applies to a description as it does to a schema, leaving every key where it was written. diff --git a/docs/inspect.markdown b/docs/inspect.markdown index 308864bcc..e34a9600b 100644 --- a/docs/inspect.markdown +++ b/docs/inspect.markdown @@ -16,11 +16,11 @@ evaluation step. This is often useful for debugging purposes. OpenAPI Descriptions -------------------- -If the input is an OpenAPI spec v3.1 or v3.2, the `inspect` command treats it as -an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather than -as a schema, and inspects it in a similar fashion, reporting on the Objects it -holds, the operations it exposes, and the Schema Objects within it. Any other -revision, such as v3.0, is rejected rather than read as a schema. +If the input is an OpenAPI spec v3.0, v3.1 or v3.2, the `inspect` command treats +it as an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather +than as a schema, and inspects it in a similar fashion, reporting on the Objects +it holds, the operations it exposes, and the Schema Objects within it. An +unsupported `openapi` revision is rejected rather than read as a schema. Examples -------- diff --git a/docs/lint.markdown b/docs/lint.markdown index 6f95ca6eb..d2d5d9b96 100644 --- a/docs/lint.markdown +++ b/docs/lint.markdown @@ -162,13 +162,13 @@ object entry with `topLevel` set to `true` marks the rule as top-level only. OpenAPI Descriptions -------------------- -If the input is an OpenAPI spec v3.1 or v3.2 (v3.0 and older are NOT fully -compliant with the JSON Schema specification), the `lint` command treats it as -an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather than -as a schema. Every Schema Object in the description (for example, the ones +If the input is an OpenAPI spec v3.0, v3.1 or v3.2, the `lint` command treats +it as an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather +than as a schema. Every Schema Object in the description (for example, the ones under `components/schemas` or the `schema` of a parameter, request body, or response) is linted, and every warning reports its location from the root of -the description. +the description. An unsupported `openapi` revision is rejected rather than read +as a schema. With `--format/-m`, the whole description is formatted, not only the Schema Objects the rules ran against. diff --git a/docs/metaschema.markdown b/docs/metaschema.markdown index c72bbf67f..1e3611750 100644 --- a/docs/metaschema.markdown +++ b/docs/metaschema.markdown @@ -47,11 +47,11 @@ code 2. OpenAPI Descriptions -------------------- -If the input is an OpenAPI spec v3.1 or v3.2, the `metaschema` command treats it -as an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather -than as a schema, and checks every Schema Object it holds against the -meta-schema of the dialect that schema is written in. Any other revision, such -as v3.0, is rejected rather than read as a schema. +If the input is an OpenAPI spec v3.0, v3.1 or v3.2, the `metaschema` command +treats it as an [OpenAPI](https://spec.openapis.org/oas/latest.html) description +rather than as a schema, and checks every Schema Object it holds against the +meta-schema of the dialect that schema is written in. An unsupported `openapi` +revision is rejected rather than read as a schema. A Schema Object that declares its own `$schema` is held to that dialect rather than to the one the description has in force, so a description mixing dialects diff --git a/docs/rdf.markdown b/docs/rdf.markdown index 1379f8bdf..c4acfa6c9 100644 --- a/docs/rdf.markdown +++ b/docs/rdf.markdown @@ -317,9 +317,22 @@ The resulting document would look like this: OpenAPI Descriptions -------------------- -If the input is an OpenAPI spec v3.1 or v3.2, the `rdf` command treats it as an -[OpenAPI](https://spec.openapis.org/oas/latest.html) description rather than as a -schema. Any other revision, such as v3.0, is rejected rather than read as a schema. +If the input is an OpenAPI spec v3.0, v3.1 or v3.2, the `rdf` command treats it +as an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather +than as a schema. An unsupported `openapi` revision is rejected rather than read +as a schema. + +A v3.0 description is read as a description, but its Schema Objects cannot be +used here. The `x-jsonld-*` keywords rely on annotation collection, which JSON +Schema only introduced in 2019-09, and the v3.0 Schema Object dialect predates +it. Use [`jsonschema upgrade`](./upgrade.markdown) to move such a Schema Object +onto a newer dialect first. That command reads schemas rather than +descriptions, so point it at the Schema Object itself: + +```sh +jsonschema upgrade path/to/my/schema.json \ + --default-dialect tag:spec.openapis.org,2024-10-18:oas/3.0/dialect +``` A description declares many schemas and no root one, so you must say which to use with `--entrypoint/-p`, given as a JSON Pointer into the description or as diff --git a/docs/test.markdown b/docs/test.markdown index ba5f4dad2..189a63e82 100644 --- a/docs/test.markdown +++ b/docs/test.markdown @@ -134,7 +134,7 @@ OpenAPI Descriptions A `target` names the schema under test wherever it lives, so the document at its base does not have to be a schema itself. If it is an -[OpenAPI](https://spec.openapis.org/oas/latest.html) spec v3.1 or v3.2 +[OpenAPI](https://spec.openapis.org/oas/latest.html) spec v3.0, v3.1 or v3.2 description, the `test` command reads it as one and runs the test cases against the Schema Object that the fragment names: diff --git a/docs/upgrade.markdown b/docs/upgrade.markdown index 325908647..b0c2ad33c 100644 --- a/docs/upgrade.markdown +++ b/docs/upgrade.markdown @@ -26,9 +26,20 @@ output. The dialects it walks through are the official JSON Schema ones up to 2020-12, and then the OpenAPI Schema Object dialects, each of which is 2020-12 plus a vocabulary of its own. Those sit past the default, so a schema lands on one of -them only when it is asked for. Note this upgrades a *schema* onto an OpenAPI -dialect, and is a separate matter from reading an OpenAPI description, which -this command does not do. +them only when it is asked for. The v3.0 Schema Object is not among them: it is +a dialect of its own rather than 2020-12 with something added, so there is no +ladder leading onto it. Note this upgrades a *schema* onto an OpenAPI dialect, +and is a separate matter from reading an OpenAPI description, which this +command does not do. + +A v3.0 Schema Object is a dialect this command upgrades *from*, however. Since +v3.0 has no `$schema` keyword to declare itself with, name the dialect with +`--default-dialect/-d`: + +```sh +jsonschema upgrade path/to/my/schema.json \ + --default-dialect tag:spec.openapis.org,2024-10-18:oas/3.0/dialect +``` The result is printed in the format the input was written in: a YAML schema upgrades into YAML and a JSON schema into JSON. A YAML result keeps the width diff --git a/docs/validate.markdown b/docs/validate.markdown index d789340b0..8a90e37db 100644 --- a/docs/validate.markdown +++ b/docs/validate.markdown @@ -102,10 +102,10 @@ non-sense results. OpenAPI Descriptions -------------------- -If the input is an OpenAPI spec v3.1 or v3.2, the `validate` command treats it as -an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather than -as a schema, and validates the instance against one of the Schema Objects it -holds. Any other revision, such as v3.0, is rejected rather than read as a +If the input is an OpenAPI spec v3.0, v3.1 or v3.2, the `validate` command treats +it as an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather +than as a schema, and validates the instance against one of the Schema Objects +it holds. An unsupported `openapi` revision is rejected rather than read as a schema. A description declares many schemas and no root one, so you must say which to diff --git a/src/command_bundle.cc b/src/command_bundle.cc index 0fe1cc0a7..3042787f9 100644 --- a/src/command_bundle.cc +++ b/src/command_bundle.cc @@ -188,6 +188,18 @@ auto sourcemeta::jsonschema::bundle(const sourcemeta::core::Options &options) throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>(schema_display_path, error); + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + const auto position{parsed_schema.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_display_path, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>(schema_display_path, + error); } catch (const sourcemeta::core::SchemaReferenceError &error) { const auto position{parsed_schema.positions.get(error.location())}; if (position.has_value()) { diff --git a/src/command_codegen.cc b/src/command_codegen.cc index 6bfb09a9c..a48ffeeb0 100644 --- a/src/command_codegen.cc +++ b/src/command_codegen.cc @@ -68,6 +68,12 @@ auto sourcemeta::jsonschema::codegen(const sourcemeta::core::Options &options) const std::filesystem::path schema_path{options.positional().front()}; auto parsed_schema{read_file(schema_path)}; + if (parsed_schema.multidocument) { + throw MultiDocumentInputError{ + "This command does not support input with multiple documents", + schema_path}; + } + // An OpenAPI description is not a schema, so it is turned down rather than // read as one, which would fail over the dialect it does not declare if (is_openapi_document(parsed_schema.document)) { @@ -121,6 +127,17 @@ auto sourcemeta::jsonschema::codegen(const sourcemeta::core::Options &options) throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>(schema_path, error); + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + const auto position{parsed_schema.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_path, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>(schema_path, error); } catch (const sourcemeta::core::SchemaResolutionError &error) { throw sourcemeta::core::FileError( schema_path, error); diff --git a/src/command_compile.cc b/src/command_compile.cc index 272dafb4b..4ca189ea5 100644 --- a/src/command_compile.cc +++ b/src/command_compile.cc @@ -41,6 +41,12 @@ auto sourcemeta::jsonschema::compile(const sourcemeta::core::Options &options) const auto is_openapi{is_openapi_document(parsed_schema.document)}; const std::filesystem::path display_path{schema_path}; + if (parsed_schema.multidocument) { + throw MultiDocumentInputError{ + "This command does not support input with multiple documents", + display_path}; + } + reject_unsupported_openapi(parsed_schema.document, display_path); if (!is_openapi && !parsed_schema.document.is_object() && @@ -169,6 +175,17 @@ auto sourcemeta::jsonschema::compile(const sourcemeta::core::Options &options) throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>(schema_path, error); + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + const auto position{parsed_schema.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_path, error); + } + + 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()) { diff --git a/src/command_fmt.cc b/src/command_fmt.cc index 362476198..e7738dd33 100644 --- a/src/command_fmt.cc +++ b/src/command_fmt.cc @@ -257,6 +257,17 @@ auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>(display_path, error); + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + const auto position{parsed.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + display_path, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>(display_path, error); } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { throw sourcemeta::core::FileError< @@ -371,6 +382,18 @@ auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>(entry.resolution_base, error); + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + const auto position{entry.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + entry.resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>(entry.resolution_base, + error); } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { throw sourcemeta::core::FileError< diff --git a/src/command_inspect.cc b/src/command_inspect.cc index 9df690473..1861cabd9 100644 --- a/src/command_inspect.cc +++ b/src/command_inspect.cc @@ -2,7 +2,6 @@ #include #include #include -#include #include // std::toupper #include // std::cout @@ -481,17 +480,13 @@ auto sourcemeta::jsonschema::inspect(const sourcemeta::core::Options &options) sourcemeta::core::PointerPositionTracker positions; auto property_storage = std::make_shared>(); + bool multidocument{false}; const sourcemeta::core::JSON schema{[&]() { - if (schema_from_stdin) { - auto parsed{read_from_stdin()}; - positions = std::move(parsed.positions); - property_storage = std::move(parsed.property_storage); - return std::move(parsed.document); - } - sourcemeta::core::JSON document{sourcemeta::core::JSON{nullptr}}; - auto callback = make_position_callback(positions, property_storage); - sourcemeta::core::read_yaml_or_json(schema_path, document, callback); - return document; + auto parsed{schema_from_stdin ? read_from_stdin() : read_file(schema_path)}; + positions = std::move(parsed.positions); + property_storage = std::move(parsed.property_storage); + multidocument = parsed.multidocument; + return std::move(parsed.document); }()}; // An OpenAPI description is not a schema, so what it goes by wherever we @@ -502,6 +497,12 @@ auto sourcemeta::jsonschema::inspect(const sourcemeta::core::Options &options) schema_from_stdin ? (is_openapi ? openapi_stdin_path() : stdin_path()) : schema_path}; + if (multidocument) { + throw MultiDocumentInputError{ + "This command does not support input with multiple documents", + schema_resolution_base}; + } + reject_unsupported_openapi(schema, schema_resolution_base, &positions); if (!is_openapi && !schema.is_object() && !schema.is_boolean()) { @@ -552,6 +553,18 @@ auto sourcemeta::jsonschema::inspect(const sourcemeta::core::Options &options) throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>(schema_resolution_base, error); + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + const auto position{positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>(schema_resolution_base, + error); } catch (const sourcemeta::core::SchemaReferenceError &error) { const auto position{positions.get(error.location())}; if (position.has_value()) { diff --git a/src/command_lint.cc b/src/command_lint.cc index 489d938ad..a298d04de 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -241,7 +241,14 @@ static auto load_rule(sourcemeta::blaze::SchemaTransformer &bundle, const std::optional &tweaks, const sourcemeta::blaze::SchemaRule::Scope scope) -> void { - auto rule_schema{sourcemeta::core::read_yaml_or_json(rule_path)}; + auto parsed_rule{sourcemeta::jsonschema::read_file(rule_path)}; + if (parsed_rule.multidocument) { + throw sourcemeta::jsonschema::MultiDocumentInputError{ + "A lint rule cannot be read from input with multiple documents", + rule_path}; + } + + auto rule_schema{std::move(parsed_rule.document)}; if (!rule_schema.defines("description")) { rule_schema.assign("description", sourcemeta::core::JSON{""}); @@ -290,6 +297,9 @@ static auto load_rule(sourcemeta::blaze::SchemaTransformer &bundle, } catch (const sourcemeta::core::SchemaAnchorCollisionError &error) { throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>(rule_path, error); + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>(rule_path, error); } catch (const sourcemeta::core::SchemaReferenceError &error) { throw sourcemeta::core::FileError( rule_path, error.identifier(), error.location(), error.what()); @@ -693,6 +703,24 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>( entry.resolution_base, error); + } catch ( + const sourcemeta::core::SchemaDialectImpossibleError &error) { + if (printed_progress) { + std::cerr << "\n"; + } + + const auto position{entry.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), + std::get<1>(position.value()), entry.resolution_base, + error); + } + + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>( + entry.resolution_base, error); } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { if (printed_progress) { std::cerr << "\n"; @@ -734,6 +762,13 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) throw sourcemeta::core::FileError< sourcemeta::core::SchemaReferenceObjectResourceError>( entry.resolution_base, error.identifier()); + } catch (const sourcemeta::core::SchemaError &error) { + if (printed_progress) { + std::cerr << "\n"; + } + + throw sourcemeta::core::FileError( + entry.resolution_base, error.what()); } catch (...) { if (printed_progress) { std::cerr << "\n"; @@ -900,6 +935,20 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>( entry.resolution_base, error); + } catch ( + const sourcemeta::core::SchemaDialectImpossibleError &error) { + const auto position{entry.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), + std::get<1>(position.value()), entry.resolution_base, + error); + } + + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>( + entry.resolution_base, error); } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { throw sourcemeta::core::FileError< sourcemeta::core::SchemaUnknownBaseDialectError>( @@ -921,6 +970,9 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) throw sourcemeta::core::FileError< sourcemeta::core::SchemaReferenceObjectResourceError>( entry.resolution_base, error.identifier()); + } catch (const sourcemeta::core::SchemaError &error) { + throw sourcemeta::core::FileError( + entry.resolution_base, error.what()); } }); diff --git a/src/command_metaschema.cc b/src/command_metaschema.cc index 029a4bf7e..7dc9d6eea 100644 --- a/src/command_metaschema.cc +++ b/src/command_metaschema.cc @@ -358,6 +358,18 @@ auto sourcemeta::jsonschema::metaschema( throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>(entry.resolution_base, error); + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + const auto position{entry.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + entry.resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>(entry.resolution_base, + error); } if (summary.failed > failures_before && !continue_on_error) { diff --git a/src/command_rdf.cc b/src/command_rdf.cc index effb27360..0b84f1284 100644 --- a/src/command_rdf.cc +++ b/src/command_rdf.cc @@ -314,6 +314,12 @@ auto sourcemeta::jsonschema::rdf(const sourcemeta::core::Options &options) schema_from_stdin ? (is_openapi ? openapi_stdin_path() : stdin_path()) : schema_resolution_base}; + if (parsed_schema.multidocument) { + throw MultiDocumentInputError{ + "This command does not support input with multiple documents", + display_path}; + } + reject_unsupported_openapi(parsed_schema.document, display_path); if (!is_openapi && !parsed_schema.document.is_object() && @@ -372,6 +378,11 @@ auto sourcemeta::jsonschema::rdf(const sourcemeta::core::Options &options) if (compact) { const std::filesystem::path context_path{options.at("compact").front()}; auto parsed_context{read_file(context_path)}; + if (parsed_context.multidocument) { + throw MultiDocumentInputError{ + "A context cannot be read from input with multiple documents", + context_path}; + } // Compacting an empty document exercises context processing on its own, // so context errors are attributed to the context file while errors on diff --git a/src/command_test.cc b/src/command_test.cc index 43a7028ef..4257d86ef 100644 --- a/src/command_test.cc +++ b/src/command_test.cc @@ -392,11 +392,6 @@ auto parse_test_suite(const sourcemeta::jsonschema::InputJSON &entry, throw sourcemeta::core::FileError{ entry.resolution_base, error}; } catch (const sourcemeta::core::OpenAPIReferenceError &error) { - // TODO: A reference of the description itself that names a plain schema - // reports "This reference must name a place within the document it points - // at", which never says that what it had to point at is another - // description. The message belongs to Core, so sharpening it is upstream's - // to do throw sourcemeta::core::FileError{ entry.resolution_base, error}; } catch (const sourcemeta::core::OpenAPIError &error) { @@ -452,6 +447,12 @@ auto parse_test_suite(const sourcemeta::jsonschema::InputJSON &entry, throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>{entry.resolution_base, error}; + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + // No position, as what compiles here is the schema the document targets + // while the positions on hand describe the test document itself + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>{entry.resolution_base, + error}; } catch (const sourcemeta::core::SchemaReferenceError &error) { // No position, as what compiles here is the schema the document targets // while the positions on hand describe the test document itself diff --git a/src/command_validate.cc b/src/command_validate.cc index dfb4a8bae..a31ef64dd 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -400,6 +400,12 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) schema_from_stdin ? (is_openapi ? openapi_stdin_path() : stdin_path()) : schema_resolution_base}; + if (parsed_schema.multidocument) { + throw MultiDocumentInputError{ + "This command does not support input with multiple documents", + display_path}; + } + reject_unsupported_openapi(parsed_schema.document, display_path); if (!is_openapi && !parsed_schema.document.is_object() && diff --git a/src/error.h b/src/error.h index caa23af18..9d9f061d9 100644 --- a/src/error.h +++ b/src/error.h @@ -1081,6 +1081,18 @@ inline auto print_exception(const bool is_json, const Exception &exception) } } + if constexpr (requires(const Exception ¤t) { + { + current.dialect() + } -> std::convertible_to; + }) { + if (is_json) { + error_json.assign("dialect", sourcemeta::core::JSON{exception.dialect()}); + } else { + std::cerr << " at dialect " << exception.dialect() << "\n"; + } + } + if constexpr (requires(const Exception ¤t) { { current.resolve_path() @@ -2087,6 +2099,16 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_SCHEMA_INPUT_ERROR; + } catch (const PositionError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; } catch ( const PositionError< sourcemeta::core::FileError> &error) { diff --git a/src/input.h b/src/input.h index bc4516762..f99917b98 100644 --- a/src/input.h +++ b/src/input.h @@ -25,7 +25,7 @@ #include // std::optional #include // std::set #include // std::ostringstream, std::istringstream -#include // std::string +#include // std::string, std::char_traits #include // std::string_view #include // std::unordered_set #include // std::vector @@ -201,6 +201,24 @@ inline auto at_end_of_stream(const std::string &input, std::istream &stream) .empty(); } +// The same question of a stream whose input was never kept around, which is +// how a file answers it without being held in memory a second time. Reading +// stops at the first character that counts rather than walking to the end +inline auto at_end_of_stream(std::istream &stream) -> bool { + if (stream.tellg() < 0) { + return false; + } + + for (auto character{stream.get()}; character != std::char_traits::eof(); + character = stream.get()) { + if (!is_json_whitespace(static_cast(character))) { + return false; + } + } + + return true; +} + // Parse every YAML document the input holds, keeping line numbers running // across the documents that follow the first inline auto read_yaml_documents(const std::string &input, @@ -260,31 +278,55 @@ struct ParsedYAML { bool multidocument{false}; }; +// A document read off a stream, keeping what it takes to write the input back +// out unless the caller says it does not need it inline auto -read_yaml_file(const std::filesystem::path &path, - sourcemeta::core::JSON &output, - const sourcemeta::core::JSON::ParseCallback &callback, - const InputFormatting formatting) -> ParsedYAML { - if (formatting == InputFormatting::Discard) { - sourcemeta::core::read_yaml(path, output, callback); - return {}; - } - - // A file that holds more than one document cannot be written back from a - // single set of round-trip metadata, but it is still valid YAML, so the - // caller gets to report that rather than a parse error - const auto input{sourcemeta::core::read_file_to_string(path)}; - std::istringstream stream{input}; - sourcemeta::core::YAMLRoundTrip roundtrip; - +parse_yaml_stream(const std::filesystem::path &path, std::istream &stream, + sourcemeta::core::JSON &output, + const sourcemeta::core::JSON::ParseCallback &callback, + const InputFormatting formatting) + -> std::optional { try { + if (formatting == InputFormatting::Discard) { + sourcemeta::core::parse_yaml(stream, output, callback); + return std::nullopt; + } + + sourcemeta::core::YAMLRoundTrip roundtrip; sourcemeta::core::parse_yaml(stream, roundtrip, output, callback); + return roundtrip; } catch (const sourcemeta::core::YAMLParseError &error) { throw sourcemeta::core::YAMLFileParseError{path, error}; } +} + +inline auto +read_yaml_file(const std::filesystem::path &path, + sourcemeta::core::JSON &output, + const sourcemeta::core::JSON::ParseCallback &callback, + const InputFormatting formatting) -> ParsedYAML { + // A file that holds more than one document is still valid YAML, so the + // caller gets to report that rather than a parse error. Reading a document + // at a time is what leaves the rest of the stream to say so with, as the + // readers that take the whole input stop at the first document without a + // word about what follows it + // + // Only a regular file can be read back from where the first document ended. + // Anything else, a named pipe above all, gives up its position the moment it + // is read, so the input is kept around to ask what follows with + if (!std::filesystem::is_regular_file(path)) { + const auto input{sourcemeta::core::read_file_to_string(path)}; + std::istringstream stream{input}; + auto roundtrip{ + parse_yaml_stream(path, stream, output, callback, formatting)}; + return {.roundtrip = std::move(roundtrip), + .multidocument = !at_end_of_stream(input, stream)}; + } + auto stream{sourcemeta::core::read_file(path)}; + auto roundtrip{parse_yaml_stream(path, stream, output, callback, formatting)}; return {.roundtrip = std::move(roundtrip), - .multidocument = !at_end_of_stream(input, stream)}; + .multidocument = !at_end_of_stream(stream)}; } inline auto diff --git a/src/resolver.h b/src/resolver.h index 14425a268..ea331f84c 100644 --- a/src/resolver.h +++ b/src/resolver.h @@ -738,9 +738,11 @@ class CustomResolver { return this->fetched_.emplace(target, std::move(document)).first->second; } - // The other half of the split. An OpenAPI Description is what this hands - // back and the only thing it ever does, so a schema that a fetch turns up - // goes to the resolver above rather than being reported from here + // The other half of the split. What the URI names is handed back whether or + // not it holds a description, as reading one that does not is how bundling + // tells a reference that it landed on the wrong kind of thing, and it cannot + // say so of a document it never sees. Nothing at all is handed back only + // where the URI names nothing auto openapi(std::string_view identifier) -> sourcemeta::core::OpenAPIResolverResult { // What a configuration remaps an identifier to holds for a description just @@ -768,8 +770,16 @@ class CustomResolver { return match->second; } - if (this->fetched_.contains(target)) { - return std::nullopt; + // A document that holds no description is still a document this URI names, + // so wherever the schema half already keeps one, that is what answers here + const auto imported{this->schemas_.find(target)}; + if (imported != this->schemas_.cend()) { + return imported->second; + } + + const auto cached{this->fetched_.find(target)}; + if (cached != this->fetched_.cend()) { + return cached->second; } auto fetched{fetch_schema(this->options_, target, this->remote_)}; @@ -779,8 +789,7 @@ class CustomResolver { auto document{std::move(fetched).to_owned()}; if (!is_openapi_document(document)) { - this->fetched_.emplace(target, std::move(document)); - return std::nullopt; + return this->fetched_.emplace(target, std::move(document)).first->second; } const auto &stored{ @@ -1105,6 +1114,18 @@ class CustomResolver { throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>(entry.resolution_base, error); + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + const auto position{entry.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + entry.resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>(entry.resolution_base, + error); } catch (const sourcemeta::core::SchemaReferenceError &error) { const auto position{entry.positions.get(error.location())}; if (position.has_value()) { @@ -1213,7 +1234,9 @@ resolver(const sourcemeta::core::Options &options, const bool remote, } // The OpenAPI half, which answers from the same instance as the schema half -// above and holds to the same separation: a schema is never reported from here +// above. What a URI names is handed back whether or not it holds a description, +// as reading one that does not is how bundling tells a reference that it landed +// on the wrong kind of thing inline auto openapi_resolver( const sourcemeta::core::Options &options, const bool remote, const std::string_view default_dialect, diff --git a/src/utils.h b/src/utils.h index c6a87741c..c29d17a4a 100644 --- a/src/utils.h +++ b/src/utils.h @@ -365,6 +365,17 @@ auto with_openapi_diagnostics( throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>(display_path, error); + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + const auto position{positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + display_path, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>(display_path, error); } catch (const sourcemeta::core::SchemaReferenceError &error) { const auto position{positions.get(error.location())}; if (position.has_value()) { @@ -593,6 +604,17 @@ bundle_for_evaluation(const sourcemeta::core::JSON &schema, throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>(resolution_base, error); + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + const auto position{positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>(resolution_base, error); } catch (const sourcemeta::core::SchemaReferenceError &error) { const auto position{positions.get(error.location())}; if (position.has_value()) { @@ -660,6 +682,17 @@ frame_for_evaluation(const sourcemeta::core::JSON &bundled, throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>(resolution_base, error); + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + const auto position{positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>(resolution_base, error); } catch (const sourcemeta::core::SchemaReferenceError &error) { const auto position{positions.get(error.location())}; if (position.has_value()) { @@ -760,6 +793,17 @@ inline auto compile_for_evaluation( throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>(resolution_base, error); + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + const auto position{positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>(resolution_base, error); } catch (const sourcemeta::core::SchemaReferenceError &error) { const auto position{positions.get(error.location())}; if (position.has_value()) { @@ -869,6 +913,18 @@ upgrade_schema(sourcemeta::core::JSON &schema, throw sourcemeta::core::FileError< sourcemeta::core::SchemaAnchorCollisionError>(schema_display_path, error); + } catch (const sourcemeta::core::SchemaDialectImpossibleError &error) { + const auto position{positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_display_path, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::core::SchemaDialectImpossibleError>(schema_display_path, + error); } catch (const sourcemeta::core::SchemaReferenceError &error) { const auto position{positions.get(error.location())}; if (position.has_value()) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 583ee81a7..70ce6c7bf 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -154,6 +154,7 @@ add_jsonschema_test(format/fail_check_many_color_always_verbose) add_jsonschema_test(format/fail_check_json_color_always) add_jsonschema_test(format/pass_check_json_color_always_verbose) add_jsonschema_test(format/pass_openapi) +add_jsonschema_test(format/pass_openapi_3_0) add_jsonschema_test(format/pass_openapi_check) add_jsonschema_test(format/fail_openapi_check) add_jsonschema_test(format/fail_openapi_check_json) @@ -205,6 +206,7 @@ add_jsonschema_test(validate/pass_directory_fast_json) add_jsonschema_test(validate/fail_directory_fast_json) add_jsonschema_test(validate/fail_schema_enoent) add_jsonschema_test(validate/fail_schema_invalid_json) +add_jsonschema_test(validate/fail_schema_yaml_multidocument) add_jsonschema_test(validate/fail_schema_non_schema) add_jsonschema_test(validate/fail_schema_unknown_dialect) add_jsonschema_test(validate/pass_2020_12_format_assertion) @@ -509,6 +511,7 @@ add_jsonschema_test(validate/fail_json_color_always) # Metaschema add_jsonschema_test(metaschema/pass_trace) add_jsonschema_test(metaschema/pass_openapi) +add_jsonschema_test(metaschema/pass_openapi_3_0) add_jsonschema_test(metaschema/fail_openapi) add_jsonschema_test(metaschema/pass_openapi_schema_dialect) add_jsonschema_test(metaschema/fail_openapi_description_dialect) @@ -707,6 +710,8 @@ add_jsonschema_test(test/fail_openapi_target_schema_id) add_jsonschema_test(test/pass_openapi_target_schema_ref_file) add_jsonschema_test(test/pass_openapi_target_cross_description) add_jsonschema_test(test/pass_openapi_target_anchor) +add_jsonschema_test(test/pass_openapi_target_3_0) +add_jsonschema_test(test/pass_openapi_target_3_0_exclusive_minimum) add_jsonschema_test(test/fail_openapi_target_description_root) add_jsonschema_test(test/fail_openapi_target_not_a_schema) add_jsonschema_test(test/fail_openapi_target_place_before_bundling) @@ -817,6 +822,7 @@ add_jsonschema_test(bundle/fail_openapi_version_newer) add_jsonschema_test(bundle/fail_openapi_version_malformed) add_jsonschema_test(bundle/pass_openapi_version_not_a_string) add_jsonschema_test(bundle/pass_openapi_schema_reference) +add_jsonschema_test(bundle/pass_openapi_3_0) add_jsonschema_test(bundle/pass_openapi_self) add_jsonschema_test(bundle/pass_openapi_yaml) add_jsonschema_test(bundle/fail_openapi_without_id) @@ -862,6 +868,7 @@ add_jsonschema_test(bundle/pass_resolve_installed_dependency_by_uri) add_jsonschema_test(bundle/fail_invalid_id_type) add_jsonschema_test(bundle/fail_invalid_schema_uri) add_jsonschema_test(bundle/fail_anchor_collision) +add_jsonschema_test(bundle/fail_impossible_dialect) add_jsonschema_test(bundle/pass_config_ignore) add_jsonschema_test(bundle/pass_ref_in_bundled_resolves_against_id) add_jsonschema_test(bundle/pass_resolve_deduplicate_embedded) @@ -882,6 +889,7 @@ add_jsonschema_test(inspect/pass_draft3_https) add_jsonschema_test(inspect/pass_draft7_https) add_jsonschema_test(inspect/pass_2020_12_http) add_jsonschema_test(inspect/pass_openapi) +add_jsonschema_test(inspect/pass_openapi_3_0) add_jsonschema_test(inspect/pass_openapi_reference) add_jsonschema_test(inspect/pass_openapi_operations) add_jsonschema_test(inspect/pass_openapi_discriminator) @@ -911,6 +919,7 @@ add_jsonschema_test(inspect/pass_json_output) add_jsonschema_test(inspect/fail_no_schema) add_jsonschema_test(inspect/fail_schema_invalid_json) add_jsonschema_test(inspect/fail_schema_invalid_yaml) +add_jsonschema_test(inspect/fail_yaml_multidocument) add_jsonschema_test(inspect/fail_unknown_metaschema) add_jsonschema_test(inspect/fail_default_dialect_config_extension_mismatch) add_jsonschema_test(inspect/fail_relative_file_metaschema_ref) @@ -921,6 +930,7 @@ add_jsonschema_test(inspect/pass_custom_extension_yaml) add_jsonschema_test(inspect/fail_invalid_id_type) add_jsonschema_test(inspect/fail_invalid_schema_uri) add_jsonschema_test(inspect/fail_anchor_collision) +add_jsonschema_test(inspect/fail_impossible_dialect) add_jsonschema_test(inspect/pass_stdin) add_jsonschema_test(inspect/pass_stdin_json) add_jsonschema_test(inspect/pass_stdin_no_id) @@ -933,6 +943,7 @@ add_jsonschema_test(compile/pass) add_jsonschema_test(compile/pass_openapi_entrypoint_pointer) add_jsonschema_test(compile/pass_openapi_entrypoint_pointer_fragment) add_jsonschema_test(compile/pass_openapi_entrypoint_uri) +add_jsonschema_test(compile/pass_openapi_3_0_entrypoint_uri) add_jsonschema_test(compile/pass_openapi_entrypoint_selects_schema) add_jsonschema_test(compile/fail_openapi_entrypoint_missing) add_jsonschema_test(compile/fail_openapi_entrypoint_missing_json) @@ -957,6 +968,7 @@ add_jsonschema_test(compile/pass_default_dialect_config_relative) add_jsonschema_test(compile/pass_yaml) add_jsonschema_test(compile/fail_no_schema) add_jsonschema_test(compile/fail_schema_invalid_json) +add_jsonschema_test(compile/fail_yaml_multidocument) add_jsonschema_test(compile/pass_format_assertion) add_jsonschema_test(compile/pass_format_assertion_flag) add_jsonschema_test(compile/fail_unknown_metaschema) @@ -1023,6 +1035,7 @@ add_jsonschema_test(codegen/fail_missing_target) 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_unknown_metaschema) add_jsonschema_test(codegen/fail_default_dialect_config_extension_mismatch) add_jsonschema_test(codegen/fail_unsupported_vocabulary) @@ -1040,6 +1053,8 @@ add_jsonschema_test(lint/pass_lint_fix) add_jsonschema_test(lint/pass_lint_no_fix) add_jsonschema_test(lint/pass_lint_embedded_resource) add_jsonschema_test(lint/fail_lint_openapi) +add_jsonschema_test(lint/fail_lint_openapi_3_0) +add_jsonschema_test(lint/fail_lint_openapi_3_0_nullable) add_jsonschema_test(lint/fail_lint_openapi_paths) add_jsonschema_test(lint/fail_lint_openapi_invalid) add_jsonschema_test(lint/fail_lint_openapi_yaml) @@ -1047,6 +1062,7 @@ add_jsonschema_test(lint/fail_lint_openapi_stdin) add_jsonschema_test(lint/pass_lint_openapi_no_schemas) add_jsonschema_test(lint/fail_lint_openapi_top_level_rule) add_jsonschema_test(lint/pass_lint_openapi_fix) +add_jsonschema_test(lint/pass_lint_openapi_3_0_fix) add_jsonschema_test(lint/pass_lint_openapi_fix_yaml) add_jsonschema_test(lint/pass_lint_openapi_fix_paths) add_jsonschema_test(lint/pass_lint_openapi_fix_format) @@ -1199,6 +1215,9 @@ add_jsonschema_test(lint/fail_lint_rule_duplicate_custom) add_jsonschema_test(lint/fail_lint_rule_invalid_json) add_jsonschema_test(lint/fail_lint_rule_unresolvable) add_jsonschema_test(lint/fail_lint_rule_invalid_yaml) +add_jsonschema_test(lint/fail_lint_impossible_dialect) +add_jsonschema_test(lint/fail_lint_no_core_vocabulary) +add_jsonschema_test(lint/fail_lint_rule_multidocument) add_jsonschema_test(lint/fail_lint_rule_property_names) add_jsonschema_test(lint/fail_lint_rule_property_names_json) add_jsonschema_test(lint/pass_lint_top_level_rule_no_violation) @@ -1414,6 +1433,7 @@ add_jsonschema_test(upgrade/fail_unknown_target) add_jsonschema_test(upgrade/pass_draft4_to_openapi_3_1) add_jsonschema_test(upgrade/pass_draft4_to_openapi_3_2) add_jsonschema_test(upgrade/pass_openapi_3_1_to_openapi_3_2) +add_jsonschema_test(upgrade/pass_openapi_3_0_source) 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) @@ -1532,6 +1552,7 @@ add_jsonschema_test(rdf/pass_yaml_schema) add_jsonschema_test(rdf/pass_yml_multidocument) add_jsonschema_test(rdf/fail_compact_context_enoent) add_jsonschema_test(rdf/fail_compact_context_invalid_json) +add_jsonschema_test(rdf/fail_compact_context_yaml_multidocument) add_jsonschema_test(rdf/fail_compact_jsonld_error) add_jsonschema_test(rdf/fail_compact_missing_value) add_jsonschema_test(rdf/fail_draft7) @@ -1569,6 +1590,7 @@ add_jsonschema_test(rdf/fail_resolution_value_on_object) add_jsonschema_test(rdf/fail_resolve_invalid_json) add_jsonschema_test(rdf/fail_schema_enoent) add_jsonschema_test(rdf/fail_schema_invalid_json) +add_jsonschema_test(rdf/fail_schema_yaml_multidocument) add_jsonschema_test(rdf/fail_schema_not_a_schema) add_jsonschema_test(rdf/fail_schema_unknown_dialect) add_jsonschema_test(rdf/fail_stdin_both) diff --git a/test/bundle/fail_impossible_dialect.clitest b/test/bundle/fail_impossible_dialect.clitest new file mode 100755 index 000000000..b5ad5ca9f --- /dev/null +++ b/test/bundle/fail_impossible_dialect.clitest @@ -0,0 +1,42 @@ +WRITE schema.json UNTIL EOF +{ + "$schema": "tag:spec.openapis.org,2024-10-18:oas/3.0/dialect", + "type": "string" +} +EOF + +// Schema input error +RUN bundle 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: The dialect that this schema declares does not define the keyword that declares it +2> at keyword $schema +2> at dialect tag:spec.openapis.org,2024-10-18:oas/3.0/dialect +2> at line 1 +2> at column 1 +2> at file path [CWD]/schema.json +2> at location "" +EOF + +COMPARE result.txt AGAINST expected.txt + +// Schema input error +RUN bundle 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": "The dialect that this schema declares does not define the keyword that declares it", +1> "keyword": "$schema", +1> "dialect": "tag:spec.openapis.org,2024-10-18:oas/3.0/dialect", +1> "line": 1, +1> "column": 1, +1> "filePath": "[CWD]/schema.json", +1> "location": "" +1> } +EOF + +COMPARE result_json.txt AGAINST expected_json.txt diff --git a/test/bundle/fail_openapi_reference_names_a_schema.clitest b/test/bundle/fail_openapi_reference_names_a_schema.clitest index 9f73158fc..4d6b76837 100644 --- a/test/bundle/fail_openapi_reference_names_a_schema.clitest +++ b/test/bundle/fail_openapi_reference_names_a_schema.clitest @@ -29,7 +29,7 @@ REPLACE $CWD_URI WITH '[CWD_URI]' IN result_0.txt REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF -2> error: This reference must name a place within the document it points at +2> error: This reference must name a document that holds an OpenAPI Description 2> at identifier https://example.com/schemas/pet 2> at line 8 2> at column 20 @@ -48,7 +48,7 @@ REPLACE $CWD WITH '[CWD]' IN result_1.txt WRITE expected_1.txt UNTIL EOF 1> { -1> "error": "This reference must name a place within the document it points at", +1> "error": "This reference must name a document that holds an OpenAPI Description", 1> "identifier": "https://example.com/schemas/pet", 1> "line": 8, 1> "column": 20, diff --git a/test/bundle/fail_openapi_referenced_revision.clitest b/test/bundle/fail_openapi_referenced_revision.clitest index 38edd18da..c7d8ba24d 100644 --- a/test/bundle/fail_openapi_referenced_revision.clitest +++ b/test/bundle/fail_openapi_referenced_revision.clitest @@ -16,7 +16,7 @@ EOF WRITE shared.json UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Shared", "version": "1.0.0" }, "components": { "responses": { @@ -33,7 +33,7 @@ REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF 2> error: This OpenAPI Specification revision is not supported -2> at value 3.0.3 +2> at value 4.0.0 2> at file path [CWD]/shared.json EOF @@ -46,7 +46,7 @@ REPLACE $CWD WITH '[CWD]' IN result_1.txt WRITE expected_1.txt UNTIL EOF 2> error: This OpenAPI Specification revision is not supported -2> at value 3.0.3 +2> at value 4.0.0 2> at file path [CWD]/shared.json EOF @@ -60,7 +60,7 @@ REPLACE $CWD WITH '[CWD]' IN result_2.txt WRITE expected_2.txt UNTIL EOF 1> { 1> "error": "This OpenAPI Specification revision is not supported", -1> "value": "3.0.3", +1> "value": "4.0.0", 1> "filePath": "[CWD]/shared.json" 1> } EOF diff --git a/test/bundle/fail_openapi_version.clitest b/test/bundle/fail_openapi_version.clitest index 74463b88a..04f7e8225 100644 --- a/test/bundle/fail_openapi_version.clitest +++ b/test/bundle/fail_openapi_version.clitest @@ -1,6 +1,6 @@ WRITE openapi.json UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "paths": {} } @@ -13,7 +13,7 @@ REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF 2> error: This OpenAPI Specification revision is not supported -2> at value 3.0.3 +2> at value 4.0.0 2> at file path [CWD]/openapi.json EOF @@ -27,7 +27,7 @@ REPLACE $CWD WITH '[CWD]' IN result_1.txt WRITE expected_1.txt UNTIL EOF 1> { 1> "error": "This OpenAPI Specification revision is not supported", -1> "value": "3.0.3", +1> "value": "4.0.0", 1> "filePath": "[CWD]/openapi.json" 1> } EOF diff --git a/test/bundle/fail_openapi_version_stdin.clitest b/test/bundle/fail_openapi_version_stdin.clitest index 257fdc7ce..21c911438 100644 --- a/test/bundle/fail_openapi_version_stdin.clitest +++ b/test/bundle/fail_openapi_version_stdin.clitest @@ -1,6 +1,6 @@ WRITE stdin_input UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "paths": {} } @@ -11,7 +11,7 @@ RUN bundle - STDIN stdin_input IN . INTO result_0.txt EXPECTING 3 WRITE expected_0.txt UNTIL EOF 2> error: This OpenAPI Specification revision is not supported -2> at value 3.0.3 +2> at value 4.0.0 2> at file path tag:sourcemeta.com,2026:openapi/stdin EOF @@ -23,7 +23,7 @@ RUN bundle - --json STDIN stdin_input IN . INTO result_1.txt EXPECTING 3 WRITE expected_1.txt UNTIL EOF 1> { 1> "error": "This OpenAPI Specification revision is not supported", -1> "value": "3.0.3", +1> "value": "4.0.0", 1> "filePath": "tag:sourcemeta.com,2026:openapi/stdin" 1> } EOF diff --git a/test/bundle/pass_bigint.clitest b/test/bundle/pass_bigint.clitest index 43017662d..0adf5c533 100644 --- a/test/bundle/pass_bigint.clitest +++ b/test/bundle/pass_bigint.clitest @@ -40,7 +40,7 @@ WRITE expected_0.txt UNTIL EOF 1> "title": "Test", 1> "description": "Test schema", 1> "type": "integer", -1> "exclusiveMaximum": 9.223372036854776000e+18 +1> "exclusiveMaximum": 9223372036854776000 1> } 1> } 1> } diff --git a/test/bundle/pass_openapi_3_0.clitest b/test/bundle/pass_openapi_3_0.clitest new file mode 100755 index 000000000..165f9dcde --- /dev/null +++ b/test/bundle/pass_openapi_3_0.clitest @@ -0,0 +1,79 @@ +// What a v3.0 description spans is embedded as the plain v3.0 it already was, +// keeping the keywords only that revision spells, and the reference that +// reached it names where it landed + +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.0.3", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "./pet.json#/components/schemas/Pet" } + } + } + } + } + } + } + } +} +EOF + +WRITE pet.json UNTIL EOF +{ + "openapi": "3.0.3", + "info": { "title": "Pets", "version": "1.0.0" }, + "paths": {}, + "components": { + "schemas": { + "Pet": { "type": "string", "nullable": true } + } + } +} +EOF + +RUN bundle openapi.json --resolve pet.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> { +1> "openapi": "3.0.3", +1> "info": { +1> "title": "Example", +1> "version": "1.0.0" +1> }, +1> "paths": { +1> "/pets": { +1> "get": { +1> "responses": { +1> "200": { +1> "description": "OK", +1> "content": { +1> "application/json": { +1> "schema": { +1> "$ref": "#/components/schemas/Pet" +1> } +1> } +1> } +1> } +1> } +1> } +1> } +1> }, +1> "components": { +1> "schemas": { +1> "Pet": { +1> "type": "string", +1> "nullable": true +1> } +1> } +1> } +1> } +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/codegen/fail_schema_invalid_yaml.clitest b/test/codegen/fail_schema_invalid_yaml.clitest old mode 100644 new mode 100755 index 6a84ae1b1..0814d2fa6 --- a/test/codegen/fail_schema_invalid_yaml.clitest +++ b/test/codegen/fail_schema_invalid_yaml.clitest @@ -1,7 +1,5 @@ WRITE schema.yaml UNTIL EOF -type: string ---- -type: integer +type: [string EOF // Other input error @@ -10,9 +8,9 @@ RUN codegen schema.yaml --target typescript STDIN /dev/null IN . INTO result.txt REPLACE $CWD WITH '[CWD]' IN result.txt WRITE expected_0.txt UNTIL EOF -2> error: Unexpected content after document -2> at line 3 -2> at column 1 +2> error: Missing comma in flow sequence +2> at line 2 +2> at column 0 2> at file path [CWD]/schema.yaml EOF @@ -25,9 +23,9 @@ REPLACE $CWD WITH '[CWD]' IN result_json.txt WRITE expected_1.txt UNTIL EOF 1> { -1> "error": "Unexpected content after document", -1> "line": 3, -1> "column": 1, +1> "error": "Missing comma in flow sequence", +1> "line": 2, +1> "column": 0, 1> "filePath": "[CWD]/schema.yaml" 1> } EOF diff --git a/test/codegen/fail_yaml_multidocument.clitest b/test/codegen/fail_yaml_multidocument.clitest new file mode 100755 index 000000000..1631051e9 --- /dev/null +++ b/test/codegen/fail_yaml_multidocument.clitest @@ -0,0 +1,33 @@ +WRITE schema.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 codegen schema.yaml --target typescript STDIN /dev/null IN . INTO result.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result.txt + +WRITE expected_0.txt UNTIL EOF +2> error: This command does not support input with multiple documents +2> at file path [CWD]/schema.yaml +EOF + +COMPARE result.txt AGAINST expected_0.txt + +// Not supported +RUN codegen schema.yaml --target typescript --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]/schema.yaml" +1> } +EOF + +COMPARE result_json.txt AGAINST expected_1.txt diff --git a/test/compile/fail_openapi_version.clitest b/test/compile/fail_openapi_version.clitest index 4959e8a40..391dcef79 100644 --- a/test/compile/fail_openapi_version.clitest +++ b/test/compile/fail_openapi_version.clitest @@ -3,7 +3,7 @@ WRITE openapi.json UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "paths": {} } @@ -16,7 +16,7 @@ REPLACE $CWD WITH '[CWD]' IN result.txt WRITE expected.txt UNTIL EOF 2> error: This OpenAPI Specification revision is not supported -2> at value 3.0.3 +2> at value 4.0.0 2> at file path [CWD]/openapi.json EOF diff --git a/test/compile/fail_openapi_version_json.clitest b/test/compile/fail_openapi_version_json.clitest index d71d1e5a4..560b90276 100644 --- a/test/compile/fail_openapi_version_json.clitest +++ b/test/compile/fail_openapi_version_json.clitest @@ -3,7 +3,7 @@ WRITE openapi.json UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "paths": {} } @@ -17,7 +17,7 @@ REPLACE $CWD WITH '[CWD]' IN result.txt WRITE expected.txt UNTIL EOF 1> { 1> "error": "This OpenAPI Specification revision is not supported", -1> "value": "3.0.3", +1> "value": "4.0.0", 1> "filePath": "[CWD]/openapi.json" 1> } EOF diff --git a/test/compile/fail_yaml_multidocument.clitest b/test/compile/fail_yaml_multidocument.clitest new file mode 100755 index 000000000..b307ad8e0 --- /dev/null +++ b/test/compile/fail_yaml_multidocument.clitest @@ -0,0 +1,33 @@ +WRITE schema.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 compile schema.yaml STDIN /dev/null IN . INTO result.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result.txt + +WRITE expected_0.txt UNTIL EOF +2> error: This command does not support input with multiple documents +2> at file path [CWD]/schema.yaml +EOF + +COMPARE result.txt AGAINST expected_0.txt + +// Not supported +RUN compile schema.yaml --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]/schema.yaml" +1> } +EOF + +COMPARE result_json.txt AGAINST expected_1.txt diff --git a/test/compile/pass.clitest b/test/compile/pass.clitest index e962bf67f..c8c9abe1c 100644 --- a/test/compile/pass.clitest +++ b/test/compile/pass.clitest @@ -14,13 +14,13 @@ RUN compile schema.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 50, +1> 52, 1> [ "description" ], 1> [], 1> "https://example.com#/description", @@ -29,7 +29,7 @@ WRITE expected.txt UNTIL EOF 1> [ 1, "Test schema" ] 1> ], 1> [ -1> 50, +1> 52, 1> [ "title" ], 1> [], 1> "https://example.com#/title", @@ -38,7 +38,7 @@ WRITE expected.txt UNTIL EOF 1> [ 1, "Test" ] 1> ], 1> [ -1> 68, +1> 70, 1> [ "additionalProperties" ], 1> [], 1> "https://example.com#/additionalProperties", @@ -56,7 +56,7 @@ WRITE expected.txt UNTIL EOF 1> [ 8, 4 ] 1> ], 1> [ -1> 53, +1> 55, 1> [], 1> [], 1> "https://example.com#/additionalProperties", diff --git a/test/compile/pass_2020_12_http.clitest b/test/compile/pass_2020_12_http.clitest index 3f84859f0..2115c69ab 100644 --- a/test/compile/pass_2020_12_http.clitest +++ b/test/compile/pass_2020_12_http.clitest @@ -14,13 +14,13 @@ RUN compile schema.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 50, +1> 52, 1> [ "description" ], 1> [], 1> "https://example.com#/description", @@ -29,7 +29,7 @@ WRITE expected.txt UNTIL EOF 1> [ 1, "Test schema" ] 1> ], 1> [ -1> 50, +1> 52, 1> [ "title" ], 1> [], 1> "https://example.com#/title", @@ -38,7 +38,7 @@ WRITE expected.txt UNTIL EOF 1> [ 1, "Test" ] 1> ], 1> [ -1> 68, +1> 70, 1> [ "additionalProperties" ], 1> [], 1> "https://example.com#/additionalProperties", @@ -56,7 +56,7 @@ WRITE expected.txt UNTIL EOF 1> [ 8, 4 ] 1> ], 1> [ -1> 53, +1> 55, 1> [], 1> [], 1> "https://example.com#/additionalProperties", diff --git a/test/compile/pass_bundled_metaschema.clitest b/test/compile/pass_bundled_metaschema.clitest index 029d339b2..742d46279 100644 --- a/test/compile/pass_bundled_metaschema.clitest +++ b/test/compile/pass_bundled_metaschema.clitest @@ -21,7 +21,7 @@ RUN compile schema.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ diff --git a/test/compile/pass_configuration_option.clitest b/test/compile/pass_configuration_option.clitest index c31893912..a7a488eff 100644 --- a/test/compile/pass_configuration_option.clitest +++ b/test/compile/pass_configuration_option.clitest @@ -21,13 +21,13 @@ REPLACE $CWD WITH '[CWD]' IN result.txt WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 68, +1> 70, 1> [ "additionalProperties" ], 1> [], 1> "https://example.com#/additionalProperties", @@ -45,7 +45,7 @@ WRITE expected.txt UNTIL EOF 1> [ 8, 4 ] 1> ], 1> [ -1> 53, +1> 55, 1> [], 1> [], 1> "https://example.com#/additionalProperties", diff --git a/test/compile/pass_custom_extension_json.clitest b/test/compile/pass_custom_extension_json.clitest index 3a125981a..79282d8a0 100644 --- a/test/compile/pass_custom_extension_json.clitest +++ b/test/compile/pass_custom_extension_json.clitest @@ -10,7 +10,7 @@ RUN compile schema.custom STDIN /dev/null IN . INTO result.txt EXPECTING 0 WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ diff --git a/test/compile/pass_custom_extension_yaml.clitest b/test/compile/pass_custom_extension_yaml.clitest index 117aa2dcd..d9990ff9c 100644 --- a/test/compile/pass_custom_extension_yaml.clitest +++ b/test/compile/pass_custom_extension_yaml.clitest @@ -8,7 +8,7 @@ RUN compile schema.custom STDIN /dev/null IN . INTO result.txt EXPECTING 0 WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ diff --git a/test/compile/pass_default_dialect.clitest b/test/compile/pass_default_dialect.clitest index f396cd9e5..883ddd547 100644 --- a/test/compile/pass_default_dialect.clitest +++ b/test/compile/pass_default_dialect.clitest @@ -11,13 +11,13 @@ RUN compile schema.json --default-dialect https://json-schema.org/draft/2020-12/ WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 68, +1> 70, 1> [ "additionalProperties" ], 1> [], 1> "https://example.com#/additionalProperties", @@ -35,7 +35,7 @@ WRITE expected.txt UNTIL EOF 1> [ 8, 4 ] 1> ], 1> [ -1> 53, +1> 55, 1> [], 1> [], 1> "https://example.com#/additionalProperties", diff --git a/test/compile/pass_default_dialect_config.clitest b/test/compile/pass_default_dialect_config.clitest index 3f89e837c..c34e70810 100644 --- a/test/compile/pass_default_dialect_config.clitest +++ b/test/compile/pass_default_dialect_config.clitest @@ -17,13 +17,13 @@ RUN compile $CWD/schema.json --verbose STDIN /dev/null IN . INTO result.txt EXPE WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 68, +1> 70, 1> [ "additionalProperties" ], 1> [], 1> "https://example.com#/additionalProperties", @@ -41,7 +41,7 @@ WRITE expected.txt UNTIL EOF 1> [ 8, 4 ] 1> ], 1> [ -1> 53, +1> 55, 1> [], 1> [], 1> "https://example.com#/additionalProperties", diff --git a/test/compile/pass_default_dialect_config_relative.clitest b/test/compile/pass_default_dialect_config_relative.clitest index bf7216e56..93c8e3245 100644 --- a/test/compile/pass_default_dialect_config_relative.clitest +++ b/test/compile/pass_default_dialect_config_relative.clitest @@ -17,13 +17,13 @@ RUN compile schema.json --verbose STDIN /dev/null IN . INTO result.txt EXPECTING WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 68, +1> 70, 1> [ "additionalProperties" ], 1> [], 1> "https://example.com#/additionalProperties", @@ -41,7 +41,7 @@ WRITE expected.txt UNTIL EOF 1> [ 8, 4 ] 1> ], 1> [ -1> 53, +1> 55, 1> [], 1> [], 1> "https://example.com#/additionalProperties", diff --git a/test/compile/pass_draft3_https.clitest b/test/compile/pass_draft3_https.clitest index c34927205..21925a0c7 100644 --- a/test/compile/pass_draft3_https.clitest +++ b/test/compile/pass_draft3_https.clitest @@ -14,13 +14,13 @@ RUN compile schema.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 68, +1> 70, 1> [ "additionalProperties" ], 1> [], 1> "https://example.com#/additionalProperties", diff --git a/test/compile/pass_draft7_https.clitest b/test/compile/pass_draft7_https.clitest index ef068a3b0..82dbed798 100644 --- a/test/compile/pass_draft7_https.clitest +++ b/test/compile/pass_draft7_https.clitest @@ -14,13 +14,13 @@ RUN compile schema.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 68, +1> 70, 1> [ "additionalProperties" ], 1> [], 1> "https://example.com#/additionalProperties", diff --git a/test/compile/pass_entrypoint_pointer.clitest b/test/compile/pass_entrypoint_pointer.clitest index 2c7e44a04..e589ad43e 100644 --- a/test/compile/pass_entrypoint_pointer.clitest +++ b/test/compile/pass_entrypoint_pointer.clitest @@ -16,7 +16,7 @@ RUN compile schema.json --entrypoint '/$$defs/Name' STDIN /dev/null IN . INTO re WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ diff --git a/test/compile/pass_entrypoint_pointer_fragment.clitest b/test/compile/pass_entrypoint_pointer_fragment.clitest index 372fecb42..4b4e73b3f 100644 --- a/test/compile/pass_entrypoint_pointer_fragment.clitest +++ b/test/compile/pass_entrypoint_pointer_fragment.clitest @@ -16,7 +16,7 @@ RUN compile schema.json --entrypoint '#/$$defs/Name' STDIN /dev/null IN . INTO r WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ diff --git a/test/compile/pass_entrypoint_pointer_fragment_space.clitest b/test/compile/pass_entrypoint_pointer_fragment_space.clitest index 224bd6429..90b07117c 100644 --- a/test/compile/pass_entrypoint_pointer_fragment_space.clitest +++ b/test/compile/pass_entrypoint_pointer_fragment_space.clitest @@ -16,7 +16,7 @@ RUN compile schema.json --entrypoint '#/$$defs/My Name' STDIN /dev/null IN . INT WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ diff --git a/test/compile/pass_entrypoint_pointer_space.clitest b/test/compile/pass_entrypoint_pointer_space.clitest index 3d0ba5afa..fd7222b1d 100644 --- a/test/compile/pass_entrypoint_pointer_space.clitest +++ b/test/compile/pass_entrypoint_pointer_space.clitest @@ -16,7 +16,7 @@ RUN compile schema.json --entrypoint '/$$defs/My Name' STDIN /dev/null IN . INTO WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ diff --git a/test/compile/pass_entrypoint_uri.clitest b/test/compile/pass_entrypoint_uri.clitest index 2c7a58108..8c97f1d0a 100644 --- a/test/compile/pass_entrypoint_uri.clitest +++ b/test/compile/pass_entrypoint_uri.clitest @@ -16,7 +16,7 @@ RUN compile schema.json --entrypoint 'https://example.com/schema#/$$defs/Name' S WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ diff --git a/test/compile/pass_fast.clitest b/test/compile/pass_fast.clitest index 52802b0d9..60d57b0db 100644 --- a/test/compile/pass_fast.clitest +++ b/test/compile/pass_fast.clitest @@ -14,13 +14,13 @@ RUN compile schema.json --fast STDIN /dev/null IN . INTO result.txt EXPECTING 0 WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> false, 1> [ 1> [ 1> [ -1> 78, +1> 80, 1> [ "additionalProperties", "type" ], 1> [], 1> "https://example.com#/additionalProperties/type", diff --git a/test/compile/pass_format_assertion.clitest b/test/compile/pass_format_assertion.clitest index 74e297ff4..b8deba664 100644 --- a/test/compile/pass_format_assertion.clitest +++ b/test/compile/pass_format_assertion.clitest @@ -24,13 +24,13 @@ RUN compile schema.json --resolve metaschema.json STDIN /dev/null IN . INTO resu REPLACE $CWD_URI WITH '[CWD_URI]' IN result.txt WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 40, +1> 42, 1> [ "format" ], 1> [], 1> "[CWD_URI]/schema.json#/format", @@ -39,7 +39,7 @@ WRITE expected.txt UNTIL EOF 1> [ 14, 5 ] 1> ], 1> [ -1> 97, +1> 99, 1> [], 1> [], 1> "[CWD_URI]/schema.json#/format", @@ -48,7 +48,7 @@ WRITE expected.txt UNTIL EOF 1> [ 8, 4 ], 1> [ 1> [ -1> 50, +1> 52, 1> [ "format" ], 1> [], 1> "[CWD_URI]/schema.json#/format", diff --git a/test/compile/pass_format_assertion_flag.clitest b/test/compile/pass_format_assertion_flag.clitest index ed4ea7416..3442573d7 100644 --- a/test/compile/pass_format_assertion_flag.clitest +++ b/test/compile/pass_format_assertion_flag.clitest @@ -11,13 +11,13 @@ RUN compile schema.json --format-assertion STDIN /dev/null IN . INTO result.txt REPLACE $CWD_URI WITH '[CWD_URI]' IN result.txt WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 40, +1> 42, 1> [ "format" ], 1> [], 1> "[CWD_URI]/schema.json#/format", @@ -26,7 +26,7 @@ WRITE expected.txt UNTIL EOF 1> [ 14, 5 ] 1> ], 1> [ -1> 97, +1> 99, 1> [], 1> [], 1> "[CWD_URI]/schema.json#/format", @@ -35,7 +35,7 @@ WRITE expected.txt UNTIL EOF 1> [ 8, 4 ], 1> [ 1> [ -1> 50, +1> 52, 1> [ "format" ], 1> [], 1> "[CWD_URI]/schema.json#/format", diff --git a/test/compile/pass_include.sh b/test/compile/pass_include.sh index dca81116b..1469a6589 100755 --- a/test/compile/pass_include.sh +++ b/test/compile/pass_include.sh @@ -27,7 +27,7 @@ cat << 'EOF' > "$TMP/expected.h" #endif static const char TEST_SCHEMA_DATA[] = { - 0x5b, 0x37, 0x2c, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2c, 0x74, 0x72, 0x75, + 0x5b, 0x39, 0x2c, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2c, 0x74, 0x72, 0x75, 0x65, 0x2c, 0x5b, 0x5b, 0x5b, 0x31, 0x31, 0x2c, 0x5b, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x5d, 0x2c, 0x5b, 0x5d, 0x2c, 0x22, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, diff --git a/test/compile/pass_include_lowercase.clitest b/test/compile/pass_include_lowercase.clitest index 6e9dbd866..2f0fdc963 100644 --- a/test/compile/pass_include_lowercase.clitest +++ b/test/compile/pass_include_lowercase.clitest @@ -18,7 +18,7 @@ WRITE expected.txt UNTIL EOF 1> #endif 1> 1> static const char TEST_SCHEMA_DATA[] = { -1> 0x5b, 0x37, 0x2c, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2c, 0x74, 0x72, 0x75, +1> 0x5b, 0x39, 0x2c, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2c, 0x74, 0x72, 0x75, 1> 0x65, 0x2c, 0x5b, 0x5b, 0x5b, 0x31, 0x31, 0x2c, 0x5b, 0x22, 0x74, 0x79, 1> 0x70, 0x65, 0x22, 0x5d, 0x2c, 0x5b, 0x5d, 0x2c, 0x22, 0x68, 0x74, 0x74, 1> 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, diff --git a/test/compile/pass_include_short.clitest b/test/compile/pass_include_short.clitest index 5bbfa513e..5767af33e 100644 --- a/test/compile/pass_include_short.clitest +++ b/test/compile/pass_include_short.clitest @@ -18,7 +18,7 @@ WRITE expected.txt UNTIL EOF 1> #endif 1> 1> static const char TEST_SCHEMA_DATA[] = { -1> 0x5b, 0x37, 0x2c, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2c, 0x74, 0x72, 0x75, +1> 0x5b, 0x39, 0x2c, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2c, 0x74, 0x72, 0x75, 1> 0x65, 0x2c, 0x5b, 0x5b, 0x5b, 0x31, 0x31, 0x2c, 0x5b, 0x22, 0x74, 0x79, 1> 0x70, 0x65, 0x22, 0x5d, 0x2c, 0x5b, 0x5d, 0x2c, 0x22, 0x68, 0x74, 0x74, 1> 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, diff --git a/test/compile/pass_minify.clitest b/test/compile/pass_minify.clitest index f4f6af316..3fba446b4 100644 --- a/test/compile/pass_minify.clitest +++ b/test/compile/pass_minify.clitest @@ -13,7 +13,7 @@ EOF RUN compile --minify schema.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 WRITE expected.txt UNTIL EOF -1> [7,false,true,[[[50,["description"],[],"https://example.com#/description",2,1,[1,"Test schema"]],[50,["title"],[],"https://example.com#/title",2,1,[1,"Test"]],[68,["additionalProperties"],[],"https://example.com#/additionalProperties",2,3,[0],[[11,["type"],[],"https://example.com#/additionalProperties/type",2,2,[8,4]],[53,[],[],"https://example.com#/additionalProperties",2,3,[0]]]]]],[],["https://json-schema.org/draft/2020-12/vocab/meta-data","https://json-schema.org/draft/2020-12/vocab/validation","https://json-schema.org/draft/2020-12/vocab/applicator"]] +1> [9,false,true,[[[52,["description"],[],"https://example.com#/description",2,1,[1,"Test schema"]],[52,["title"],[],"https://example.com#/title",2,1,[1,"Test"]],[70,["additionalProperties"],[],"https://example.com#/additionalProperties",2,3,[0],[[11,["type"],[],"https://example.com#/additionalProperties/type",2,2,[8,4]],[55,[],[],"https://example.com#/additionalProperties",2,3,[0]]]]]],[],["https://json-schema.org/draft/2020-12/vocab/meta-data","https://json-schema.org/draft/2020-12/vocab/validation","https://json-schema.org/draft/2020-12/vocab/applicator"]] EOF COMPARE result.txt AGAINST expected.txt diff --git a/test/compile/pass_no_identifier.clitest b/test/compile/pass_no_identifier.clitest index 5119a270b..a426f1f80 100644 --- a/test/compile/pass_no_identifier.clitest +++ b/test/compile/pass_no_identifier.clitest @@ -14,13 +14,13 @@ RUN compile foo/bar/baz/qux/very/long/path/foo/bar/baz/qux/schema.json STDIN /de REPLACE $CWD_URI WITH '[CWD_URI]' IN result.txt WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 50, +1> 52, 1> [ "description" ], 1> [], 1> "[CWD_URI]/foo/bar/baz/qux/very/long/path/foo/bar/baz/qux/schema.json#/description", @@ -29,7 +29,7 @@ WRITE expected.txt UNTIL EOF 1> [ 1, "Test schema" ] 1> ], 1> [ -1> 50, +1> 52, 1> [ "title" ], 1> [], 1> "[CWD_URI]/foo/bar/baz/qux/very/long/path/foo/bar/baz/qux/schema.json#/title", @@ -38,7 +38,7 @@ WRITE expected.txt UNTIL EOF 1> [ 1, "Test" ] 1> ], 1> [ -1> 68, +1> 70, 1> [ "additionalProperties" ], 1> [], 1> "[CWD_URI]/foo/bar/baz/qux/very/long/path/foo/bar/baz/qux/schema.json#/additionalProperties", @@ -56,7 +56,7 @@ WRITE expected.txt UNTIL EOF 1> [ 8, 4 ] 1> ], 1> [ -1> 53, +1> 55, 1> [], 1> [], 1> "[CWD_URI]/foo/bar/baz/qux/very/long/path/foo/bar/baz/qux/schema.json#/additionalProperties", diff --git a/test/compile/pass_openapi_3_0_entrypoint_uri.clitest b/test/compile/pass_openapi_3_0_entrypoint_uri.clitest new file mode 100755 index 000000000..44bcf4386 --- /dev/null +++ b/test/compile/pass_openapi_3_0_entrypoint_uri.clitest @@ -0,0 +1,41 @@ +// A v3.0 description declares no identity of its own, so its Schema Objects +// answer to where the description was read from plus the pointer that reaches +// them, which names them as well as a pointer on its own does. + +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.0.3", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": {}, + "components": { "schemas": { "Person": { "type": "string" } } } +} +EOF + +RUN compile openapi.json --entrypoint $CWD_URI/openapi.json#/components/schemas/Person STDIN /dev/null IN . INTO result_0.txt EXPECTING 0 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +1> [ +1> 9, +1> false, +1> true, +1> [ +1> [ +1> [ +1> 11, +1> [ "type" ], +1> [], +1> "[CWD_URI]/openapi.json#/components/schemas/Person/type", +1> 1, +1> 1, +1> [ 8, 4 ] +1> ] +1> ] +1> ], +1> [], +1> [ "tag:spec.openapis.org,2017:oas/3.0/vocab/base" ] +1> ] +EOF + +COMPARE result_0.txt AGAINST expected_0.txt diff --git a/test/compile/pass_openapi_entrypoint_pointer.clitest b/test/compile/pass_openapi_entrypoint_pointer.clitest index 4c7826136..fba09db4a 100644 --- a/test/compile/pass_openapi_entrypoint_pointer.clitest +++ b/test/compile/pass_openapi_entrypoint_pointer.clitest @@ -24,7 +24,7 @@ REPLACE $CWD_URI WITH '[CWD_URI]' IN result.txt WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ @@ -45,7 +45,7 @@ WRITE expected.txt UNTIL EOF 1> ] 1> ], 1> [ -1> 61, +1> 63, 1> [ "properties" ], 1> [], 1> "[CWD_URI]/openapi.json#/components/schemas/Person/properties", @@ -63,7 +63,7 @@ WRITE expected.txt UNTIL EOF 1> [ 8, 4 ] 1> ], 1> [ -1> 51, +1> 53, 1> [], 1> [], 1> "[CWD_URI]/openapi.json#/components/schemas/Person/properties", diff --git a/test/compile/pass_openapi_entrypoint_pointer_fragment.clitest b/test/compile/pass_openapi_entrypoint_pointer_fragment.clitest index 8c7a40a6c..78f25153d 100644 --- a/test/compile/pass_openapi_entrypoint_pointer_fragment.clitest +++ b/test/compile/pass_openapi_entrypoint_pointer_fragment.clitest @@ -23,7 +23,7 @@ REPLACE $CWD_URI WITH '[CWD_URI]' IN result.txt WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ @@ -44,7 +44,7 @@ WRITE expected.txt UNTIL EOF 1> ] 1> ], 1> [ -1> 61, +1> 63, 1> [ "properties" ], 1> [], 1> "[CWD_URI]/openapi.json#/components/schemas/Person/properties", @@ -62,7 +62,7 @@ WRITE expected.txt UNTIL EOF 1> [ 8, 4 ] 1> ], 1> [ -1> 51, +1> 53, 1> [], 1> [], 1> "[CWD_URI]/openapi.json#/components/schemas/Person/properties", diff --git a/test/compile/pass_openapi_entrypoint_selects_schema.clitest b/test/compile/pass_openapi_entrypoint_selects_schema.clitest index 33778a31a..b675e9676 100644 --- a/test/compile/pass_openapi_entrypoint_selects_schema.clitest +++ b/test/compile/pass_openapi_entrypoint_selects_schema.clitest @@ -21,7 +21,7 @@ REPLACE $CWD_URI WITH '[CWD_URI]' IN result.txt WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ diff --git a/test/compile/pass_openapi_entrypoint_uri.clitest b/test/compile/pass_openapi_entrypoint_uri.clitest index 8909136bc..1fd552904 100644 --- a/test/compile/pass_openapi_entrypoint_uri.clitest +++ b/test/compile/pass_openapi_entrypoint_uri.clitest @@ -15,7 +15,7 @@ RUN compile openapi.json --entrypoint https://example.com/api#/components/schema WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ diff --git a/test/compile/pass_resolve_remap.clitest b/test/compile/pass_resolve_remap.clitest index 7a9df2444..9b809d99f 100644 --- a/test/compile/pass_resolve_remap.clitest +++ b/test/compile/pass_resolve_remap.clitest @@ -30,13 +30,13 @@ RUN compile $CWD/schema.json --resolve $CWD/remote.json --verbose STDIN /dev/nul WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 100, +1> 102, 1> [ "$ref" ], 1> [], 1> "https://example.com#/$ref", @@ -45,7 +45,7 @@ WRITE expected.txt UNTIL EOF 1> [ 10, 1 ] 1> ], 1> [ -1> 50, +1> 52, 1> [ "description" ], 1> [], 1> "https://example.com#/description", @@ -54,7 +54,7 @@ WRITE expected.txt UNTIL EOF 1> [ 1, "Test schema" ] 1> ], 1> [ -1> 50, +1> 52, 1> [ "title" ], 1> [], 1> "https://example.com#/title", @@ -65,7 +65,7 @@ WRITE expected.txt UNTIL EOF 1> ], 1> [ 1> [ -1> 50, +1> 52, 1> [ "description" ], 1> [], 1> "https://example.com/nested#/description", @@ -74,7 +74,7 @@ WRITE expected.txt UNTIL EOF 1> [ 1, "Test schema" ] 1> ], 1> [ -1> 50, +1> 52, 1> [ "title" ], 1> [], 1> "https://example.com/nested#/title", diff --git a/test/compile/pass_resolve_remap_debug.clitest b/test/compile/pass_resolve_remap_debug.clitest index 36ff0b522..e59105852 100644 --- a/test/compile/pass_resolve_remap_debug.clitest +++ b/test/compile/pass_resolve_remap_debug.clitest @@ -33,13 +33,13 @@ REPLACE $CWD WITH '[CWD]' IN result.txt WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 100, +1> 102, 1> [ "$ref" ], 1> [], 1> "https://example.com#/$ref", @@ -48,7 +48,7 @@ WRITE expected.txt UNTIL EOF 1> [ 10, 1 ] 1> ], 1> [ -1> 50, +1> 52, 1> [ "description" ], 1> [], 1> "https://example.com#/description", @@ -57,7 +57,7 @@ WRITE expected.txt UNTIL EOF 1> [ 1, "Test schema" ] 1> ], 1> [ -1> 50, +1> 52, 1> [ "title" ], 1> [], 1> "https://example.com#/title", @@ -68,7 +68,7 @@ WRITE expected.txt UNTIL EOF 1> ], 1> [ 1> [ -1> 50, +1> 52, 1> [ "description" ], 1> [], 1> "https://example.com/nested#/description", @@ -77,7 +77,7 @@ WRITE expected.txt UNTIL EOF 1> [ 1, "Test schema" ] 1> ], 1> [ -1> 50, +1> 52, 1> [ "title" ], 1> [], 1> "https://example.com/nested#/title", diff --git a/test/compile/pass_resolve_remap_relative.clitest b/test/compile/pass_resolve_remap_relative.clitest index 80e79ec76..d675d280a 100644 --- a/test/compile/pass_resolve_remap_relative.clitest +++ b/test/compile/pass_resolve_remap_relative.clitest @@ -30,13 +30,13 @@ RUN compile schema.json --resolve remote.json --verbose STDIN /dev/null IN . INT WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 100, +1> 102, 1> [ "$ref" ], 1> [], 1> "https://example.com#/$ref", @@ -45,7 +45,7 @@ WRITE expected.txt UNTIL EOF 1> [ 10, 1 ] 1> ], 1> [ -1> 50, +1> 52, 1> [ "description" ], 1> [], 1> "https://example.com#/description", @@ -54,7 +54,7 @@ WRITE expected.txt UNTIL EOF 1> [ 1, "Test schema" ] 1> ], 1> [ -1> 50, +1> 52, 1> [ "title" ], 1> [], 1> "https://example.com#/title", @@ -65,7 +65,7 @@ WRITE expected.txt UNTIL EOF 1> ], 1> [ 1> [ -1> 50, +1> 52, 1> [ "description" ], 1> [], 1> "https://example.com/nested#/description", @@ -74,7 +74,7 @@ WRITE expected.txt UNTIL EOF 1> [ 1, "Test schema" ] 1> ], 1> [ -1> 50, +1> 52, 1> [ "title" ], 1> [], 1> "https://example.com/nested#/title", diff --git a/test/compile/pass_without_extension_json.clitest b/test/compile/pass_without_extension_json.clitest index f85e02943..9203adf44 100644 --- a/test/compile/pass_without_extension_json.clitest +++ b/test/compile/pass_without_extension_json.clitest @@ -13,13 +13,13 @@ RUN compile schema STDIN /dev/null IN . INTO result.txt EXPECTING 0 WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 100, +1> 102, 1> [ "$ref" ], 1> [], 1> "https://example.com#/$ref", diff --git a/test/compile/pass_without_extension_yaml.clitest b/test/compile/pass_without_extension_yaml.clitest index 017abdcd3..f38d2da5f 100644 --- a/test/compile/pass_without_extension_yaml.clitest +++ b/test/compile/pass_without_extension_yaml.clitest @@ -10,13 +10,13 @@ RUN compile schema STDIN /dev/null IN . INTO result.txt EXPECTING 0 WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 100, +1> 102, 1> [ "$ref" ], 1> [], 1> "https://example.com#/$ref", diff --git a/test/compile/pass_yaml.clitest b/test/compile/pass_yaml.clitest index 1527230b5..02df68a0f 100644 --- a/test/compile/pass_yaml.clitest +++ b/test/compile/pass_yaml.clitest @@ -10,13 +10,13 @@ RUN compile schema.yaml STDIN /dev/null IN . INTO result.txt EXPECTING 0 WRITE expected.txt UNTIL EOF 1> [ -1> 7, +1> 9, 1> false, 1> true, 1> [ 1> [ 1> [ -1> 100, +1> 102, 1> [ "$ref" ], 1> [], 1> "https://example.com#/$ref", diff --git a/test/format/fail_openapi_version.clitest b/test/format/fail_openapi_version.clitest index 1ce2fa731..e1d9ab712 100755 --- a/test/format/fail_openapi_version.clitest +++ b/test/format/fail_openapi_version.clitest @@ -1,6 +1,6 @@ WRITE openapi.json UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "paths": {} } @@ -13,7 +13,7 @@ REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF 2> error: This OpenAPI Specification revision is not supported -2> at value 3.0.3 +2> at value 4.0.0 2> at file path [CWD]/openapi.json EOF @@ -27,7 +27,7 @@ REPLACE $CWD WITH '[CWD]' IN result_1.txt WRITE expected_1.txt UNTIL EOF 1> { 1> "error": "This OpenAPI Specification revision is not supported", -1> "value": "3.0.3", +1> "value": "4.0.0", 1> "filePath": "[CWD]/openapi.json" 1> } EOF diff --git a/test/format/fail_openapi_version_stdin.clitest b/test/format/fail_openapi_version_stdin.clitest index 70cd719c5..aad530731 100755 --- a/test/format/fail_openapi_version_stdin.clitest +++ b/test/format/fail_openapi_version_stdin.clitest @@ -1,6 +1,6 @@ WRITE stdin_input UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "paths": {} } @@ -11,7 +11,7 @@ RUN fmt - STDIN stdin_input IN . INTO result_0.txt EXPECTING 3 WRITE expected_0.txt UNTIL EOF 2> error: This OpenAPI Specification revision is not supported -2> at value 3.0.3 +2> at value 4.0.0 2> at file path tag:sourcemeta.com,2026:openapi/stdin EOF @@ -23,7 +23,7 @@ RUN fmt - --json STDIN stdin_input IN . INTO result_1.txt EXPECTING 3 WRITE expected_1.txt UNTIL EOF 1> { 1> "error": "This OpenAPI Specification revision is not supported", -1> "value": "3.0.3", +1> "value": "4.0.0", 1> "filePath": "tag:sourcemeta.com,2026:openapi/stdin" 1> } EOF diff --git a/test/format/pass_bignum_real.clitest b/test/format/pass_bignum_real.clitest index feff46a22..845947692 100644 --- a/test/format/pass_bignum_real.clitest +++ b/test/format/pass_bignum_real.clitest @@ -19,7 +19,7 @@ WRITE expected_file_0.txt UNTIL EOF "$schema": "http://json-schema.org/draft-04/schema#", "title": "Test", "description": "Test schema", - "minimum": 1e-5 + "minimum": 0.00001 } EOF diff --git a/test/format/pass_openapi_3_0.clitest b/test/format/pass_openapi_3_0.clitest new file mode 100755 index 000000000..a73b6de69 --- /dev/null +++ b/test/format/pass_openapi_3_0.clitest @@ -0,0 +1,41 @@ +WRITE openapi.json UNTIL EOF +{ + "paths": {}, + "info": { "version": "1.0.0", "title": "Example" }, + "openapi": "3.0.3", + "components": { + "schemas": { + "Pet": { "type": "string", "nullable": true, "title": "Pet" } + } + } +} +EOF + +RUN fmt openapi.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +EOF + +COMPARE result.txt AGAINST expected.txt + +WRITE expected_file_0.txt UNTIL EOF +{ + "openapi": "3.0.3", + "info": { + "title": "Example", + "version": "1.0.0" + }, + "paths": {}, + "components": { + "schemas": { + "Pet": { + "title": "Pet", + "type": "string", + "nullable": true + } + } + } +} +EOF + +COMPARE openapi.json AGAINST expected_file_0.txt diff --git a/test/inspect/fail_impossible_dialect.clitest b/test/inspect/fail_impossible_dialect.clitest new file mode 100755 index 000000000..be03d4cd6 --- /dev/null +++ b/test/inspect/fail_impossible_dialect.clitest @@ -0,0 +1,42 @@ +WRITE schema.json UNTIL EOF +{ + "$schema": "tag:spec.openapis.org,2024-10-18:oas/3.0/dialect", + "type": "string" +} +EOF + +// Schema input error +RUN inspect 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: The dialect that this schema declares does not define the keyword that declares it +2> at keyword $schema +2> at dialect tag:spec.openapis.org,2024-10-18:oas/3.0/dialect +2> at line 1 +2> at column 1 +2> at file path [CWD]/schema.json +2> at location "" +EOF + +COMPARE result.txt AGAINST expected.txt + +// Schema input error +RUN inspect 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": "The dialect that this schema declares does not define the keyword that declares it", +1> "keyword": "$schema", +1> "dialect": "tag:spec.openapis.org,2024-10-18:oas/3.0/dialect", +1> "line": 1, +1> "column": 1, +1> "filePath": "[CWD]/schema.json", +1> "location": "" +1> } +EOF + +COMPARE result_json.txt AGAINST expected_json.txt diff --git a/test/inspect/fail_openapi_version.clitest b/test/inspect/fail_openapi_version.clitest index 719f67fc6..f9a461a7e 100644 --- a/test/inspect/fail_openapi_version.clitest +++ b/test/inspect/fail_openapi_version.clitest @@ -3,7 +3,7 @@ WRITE openapi.json UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "paths": {} } @@ -16,7 +16,7 @@ REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF 2> error: This OpenAPI Specification revision is not supported -2> at value 3.0.3 +2> at value 4.0.0 2> at line 2 2> at column 3 2> at file path [CWD]/openapi.json @@ -32,7 +32,7 @@ REPLACE $CWD WITH '[CWD]' IN result_1.txt WRITE expected_1.txt UNTIL EOF 1> { 1> "error": "This OpenAPI Specification revision is not supported", -1> "value": "3.0.3", +1> "value": "4.0.0", 1> "line": 2, 1> "column": 3, 1> "filePath": "[CWD]/openapi.json" diff --git a/test/inspect/fail_schema_invalid_yaml.clitest b/test/inspect/fail_schema_invalid_yaml.clitest old mode 100644 new mode 100755 index 5be33ec13..a7264a202 --- a/test/inspect/fail_schema_invalid_yaml.clitest +++ b/test/inspect/fail_schema_invalid_yaml.clitest @@ -1,7 +1,5 @@ WRITE schema.yaml UNTIL EOF -type: string ---- -type: integer +type: [string EOF // Other input error @@ -10,9 +8,9 @@ RUN inspect schema.yaml STDIN /dev/null IN . INTO result.txt EXPECTING 6 REPLACE $CWD WITH '[CWD]' IN result.txt WRITE expected.txt UNTIL EOF -2> error: Unexpected content after document -2> at line 3 -2> at column 1 +2> error: Missing comma in flow sequence +2> at line 2 +2> at column 0 2> at file path [CWD]/schema.yaml EOF @@ -25,9 +23,9 @@ REPLACE $CWD WITH '[CWD]' IN result_json.txt WRITE expected_json.txt UNTIL EOF 1> { -1> "error": "Unexpected content after document", -1> "line": 3, -1> "column": 1, +1> "error": "Missing comma in flow sequence", +1> "line": 2, +1> "column": 0, 1> "filePath": "[CWD]/schema.yaml" 1> } EOF diff --git a/test/inspect/fail_yaml_multidocument.clitest b/test/inspect/fail_yaml_multidocument.clitest new file mode 100755 index 000000000..95f96db85 --- /dev/null +++ b/test/inspect/fail_yaml_multidocument.clitest @@ -0,0 +1,33 @@ +WRITE schema.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 inspect schema.yaml STDIN /dev/null IN . INTO result.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result.txt + +WRITE expected.txt UNTIL EOF +2> error: This command does not support input with multiple documents +2> at file path [CWD]/schema.yaml +EOF + +COMPARE result.txt AGAINST expected.txt + +// Not supported +RUN inspect schema.yaml --json STDIN /dev/null IN . INTO result_json.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_json.txt + +WRITE expected_json.txt UNTIL EOF +1> { +1> "error": "This command does not support input with multiple documents", +1> "filePath": "[CWD]/schema.yaml" +1> } +EOF + +COMPARE result_json.txt AGAINST expected_json.txt diff --git a/test/inspect/pass_openapi_3_0.clitest b/test/inspect/pass_openapi_3_0.clitest new file mode 100755 index 000000000..d9546fcb1 --- /dev/null +++ b/test/inspect/pass_openapi_3_0.clitest @@ -0,0 +1,95 @@ +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.0.3", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": {} +} +EOF + +RUN inspect openapi.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 0 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +1> (OPENAPI) URI: [CWD_URI]/openapi.json +1> Pointer : +1> File Position : 1:1 +1> Parent : +1> Default Dialect : tag:spec.openapis.org,2024-10-18:oas/3.0/dialect +1> Version : 3.0 +1> Base : [CWD_URI]/openapi.json +1> Standalone : yes +1> Info : +1> Title : Example +1> Version : 1.0.0 +1> Summary : +1> Description : +1> Terms of Service: +1> Contact : +1> License : +1> +1> (INFO) URI: [CWD_URI]/openapi.json#/info +1> Pointer : /info +1> File Position : 3:3 +1> Parent : [CWD_URI]/openapi.json +1> +1> (PATHS) URI: [CWD_URI]/openapi.json#/paths +1> Pointer : /paths +1> File Position : 4:3 +1> Parent : [CWD_URI]/openapi.json +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +RUN inspect openapi.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 0 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "version": "3.0", +1> "base": "[CWD_URI]/openapi.json", +1> "standalone": true, +1> "schemas": { +1> "mode": "references", +1> "locations": { +1> "static": {}, +1> "dynamic": {} +1> }, +1> "references": [] +1> }, +1> "info": { +1> "title": "Example", +1> "version": "1.0.0", +1> "summary": null, +1> "description": null, +1> "termsOfService": null, +1> "contact": null, +1> "license": null +1> }, +1> "locations": { +1> "[CWD_URI]/openapi.json": { +1> "type": "openapi", +1> "pointer": "", +1> "position": [ 1, 1, 5, 1 ], +1> "parent": null, +1> "defaultDialect": "tag:spec.openapis.org,2024-10-18:oas/3.0/dialect" +1> }, +1> "[CWD_URI]/openapi.json#/info": { +1> "type": "info", +1> "pointer": "/info", +1> "position": [ 3, 3, 3, 52 ], +1> "parent": "[CWD_URI]/openapi.json" +1> }, +1> "[CWD_URI]/openapi.json#/paths": { +1> "type": "paths", +1> "pointer": "/paths", +1> "position": [ 4, 3, 4, 13 ], +1> "parent": "[CWD_URI]/openapi.json" +1> } +1> }, +1> "operations": [] +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/lint/fail_lint_impossible_dialect.clitest b/test/lint/fail_lint_impossible_dialect.clitest new file mode 100755 index 000000000..2029d9c8c --- /dev/null +++ b/test/lint/fail_lint_impossible_dialect.clitest @@ -0,0 +1,78 @@ +WRITE schema.json UNTIL EOF +{ + "$schema": "tag:spec.openapis.org,2024-10-18:oas/3.0/dialect", + "type": "string" +} +EOF + +// Schema input error +RUN lint schema.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: The dialect that this schema declares does not define the keyword that declares it +2> at keyword $schema +2> at dialect tag:spec.openapis.org,2024-10-18:oas/3.0/dialect +2> at line 1 +2> at column 1 +2> at file path [CWD]/schema.json +2> at location "" +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Schema input error +RUN lint schema.json --fix STDIN /dev/null IN . INTO result_1.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +2> error: The dialect that this schema declares does not define the keyword that declares it +2> at keyword $schema +2> at dialect tag:spec.openapis.org,2024-10-18:oas/3.0/dialect +2> at line 1 +2> at column 1 +2> at file path [CWD]/schema.json +2> at location "" +EOF + +COMPARE result_1.txt AGAINST expected_1.txt + +// Schema input error +RUN lint schema.json --json STDIN /dev/null IN . INTO result_2.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result_2.txt + +WRITE expected_2.txt UNTIL EOF +1> { +1> "error": "The dialect that this schema declares does not define the keyword that declares it", +1> "keyword": "$schema", +1> "dialect": "tag:spec.openapis.org,2024-10-18:oas/3.0/dialect", +1> "line": 1, +1> "column": 1, +1> "filePath": "[CWD]/schema.json", +1> "location": "" +1> } +EOF + +COMPARE result_2.txt AGAINST expected_2.txt + +// Schema input error +RUN lint schema.json --fix --json STDIN /dev/null IN . INTO result_3.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result_3.txt + +WRITE expected_3.txt UNTIL EOF +1> { +1> "error": "The dialect that this schema declares does not define the keyword that declares it", +1> "keyword": "$schema", +1> "dialect": "tag:spec.openapis.org,2024-10-18:oas/3.0/dialect", +1> "line": 1, +1> "column": 1, +1> "filePath": "[CWD]/schema.json", +1> "location": "" +1> } +EOF + +COMPARE result_3.txt AGAINST expected_3.txt diff --git a/test/lint/fail_lint_no_core_vocabulary.clitest b/test/lint/fail_lint_no_core_vocabulary.clitest new file mode 100755 index 000000000..ccefe7ebb --- /dev/null +++ b/test/lint/fail_lint_no_core_vocabulary.clitest @@ -0,0 +1,68 @@ +WRITE meta.json UNTIL EOF +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/validation": true + } +} +EOF + +WRITE schema.json UNTIL EOF +{ + "$schema": "https://example.com/meta", + "type": "string" +} +EOF + +// Schema input error +RUN lint schema.json --resolve meta.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: The core vocabulary must always be present +2> at file path [CWD]/schema.json +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Schema input error +RUN lint schema.json --resolve meta.json --fix STDIN /dev/null IN . INTO result_1.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +2> error: The core vocabulary must always be present +2> at file path [CWD]/schema.json +EOF + +COMPARE result_1.txt AGAINST expected_1.txt + +// Schema input error +RUN lint schema.json --resolve meta.json --json STDIN /dev/null IN . INTO result_2.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result_2.txt + +WRITE expected_2.txt UNTIL EOF +1> { +1> "error": "The core vocabulary must always be present", +1> "filePath": "[CWD]/schema.json" +1> } +EOF + +COMPARE result_2.txt AGAINST expected_2.txt + +// Schema input error +RUN lint schema.json --resolve meta.json --fix --json STDIN /dev/null IN . INTO result_3.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result_3.txt + +WRITE expected_3.txt UNTIL EOF +1> { +1> "error": "The core vocabulary must always be present", +1> "filePath": "[CWD]/schema.json" +1> } +EOF + +COMPARE result_3.txt AGAINST expected_3.txt diff --git a/test/lint/fail_lint_openapi_3_0.clitest b/test/lint/fail_lint_openapi_3_0.clitest new file mode 100755 index 000000000..b102060c4 --- /dev/null +++ b/test/lint/fail_lint_openapi_3_0.clitest @@ -0,0 +1,84 @@ +// A v3.0 Schema Object is linted against its own dialect, so the rules that +// name a keyword it does not have stay quiet. There is no `const` to rewrite +// the single `enum` choice into, and no `examples` to ask the top level for + +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.0.3", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": {}, + "components": { + "schemas": { + "Pet": { "type": "string", "enum": [ "dog" ] } + } + } +} +EOF + +// Validation failure +RUN lint openapi.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 2 + +WRITE expected_0.txt UNTIL EOF +1> openapi.json:7:34: +1> Setting `type` alongside `enum` is considered an anti-pattern, as the enumeration choices already imply their respective types (enum_with_type) +1> at location "/components/schemas/Pet/enum" +1> openapi.json:7:16: +1> Setting `type` alongside `enum` is considered an anti-pattern, as the enumeration choices already imply their respective types (enum_with_type) +1> at location "/components/schemas/Pet/type" +1> openapi.json:7:7: +1> Set a concise non-empty title at the top level of the schema to explain what the definition is about (top_level_title) +1> at location "/components/schemas/Pet" +1> openapi.json:7:7: +1> Set a non-empty description at the top level of the schema to explain what the definition is about in detail (top_level_description) +1> at location "/components/schemas/Pet" +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Validation failure +RUN lint openapi.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 2 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "valid": false, +1> "health": 0, +1> "errors": [ +1> { +1> "path": "[CWD]/openapi.json", +1> "id": "enum_with_type", +1> "message": "Setting `type` alongside `enum` is considered an anti-pattern, as the enumeration choices already imply their respective types", +1> "description": null, +1> "schemaLocation": [ "components", "schemas", "Pet", "enum" ], +1> "position": [ 7, 34, 7, 50 ] +1> }, +1> { +1> "path": "[CWD]/openapi.json", +1> "id": "enum_with_type", +1> "message": "Setting `type` alongside `enum` is considered an anti-pattern, as the enumeration choices already imply their respective types", +1> "description": null, +1> "schemaLocation": [ "components", "schemas", "Pet", "type" ], +1> "position": [ 7, 16, 7, 31 ] +1> }, +1> { +1> "path": "[CWD]/openapi.json", +1> "id": "top_level_title", +1> "message": "Set a concise non-empty title at the top level of the schema to explain what the definition is about", +1> "description": null, +1> "schemaLocation": [ "components", "schemas", "Pet" ], +1> "position": [ 7, 7, 7, 52 ] +1> }, +1> { +1> "path": "[CWD]/openapi.json", +1> "id": "top_level_description", +1> "message": "Set a non-empty description at the top level of the schema to explain what the definition is about in detail", +1> "description": null, +1> "schemaLocation": [ "components", "schemas", "Pet" ], +1> "position": [ 7, 7, 7, 52 ] +1> } +1> ] +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/lint/fail_lint_openapi_3_0_nullable.clitest b/test/lint/fail_lint_openapi_3_0_nullable.clitest new file mode 100755 index 000000000..2646da918 --- /dev/null +++ b/test/lint/fail_lint_openapi_3_0_nullable.clitest @@ -0,0 +1,77 @@ +// A v3.0 schema spells its optional null with `nullable` rather than a `null` +// member of `type`, and the rules read it as such. Pinning a number between +// equal bounds leaves one value in the first schema below, but two in the +// second, so only the first is reported + +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.0.3", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": {}, + "components": { + "schemas": { + "Exact": { + "title": "Exact", + "description": "A quantity pinned to one number", + "type": "integer", + "minimum": 5, + "maximum": 5 + }, + "ExactOrNull": { + "title": "Exact or null", + "description": "The same quantity, or nothing at all", + "type": "integer", + "nullable": true, + "minimum": 5, + "maximum": 5 + } + } + } +} +EOF + +// Validation failure +RUN lint openapi.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 2 + +WRITE expected_0.txt UNTIL EOF +1> openapi.json:11:9: +1> Setting `minimum` and `maximum` to the same number only leaves one possible value (equal_numeric_bounds_to_enum) +1> at location "/components/schemas/Exact/minimum" +1> openapi.json:12:9: +1> Setting `minimum` and `maximum` to the same number only leaves one possible value (equal_numeric_bounds_to_enum) +1> at location "/components/schemas/Exact/maximum" +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Validation failure +RUN lint openapi.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 2 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "valid": false, +1> "health": 50, +1> "errors": [ +1> { +1> "path": "[CWD]/openapi.json", +1> "id": "equal_numeric_bounds_to_enum", +1> "message": "Setting `minimum` and `maximum` to the same number only leaves one possible value", +1> "description": null, +1> "schemaLocation": [ "components", "schemas", "Exact", "minimum" ], +1> "position": [ 11, 9, 11, 20 ] +1> }, +1> { +1> "path": "[CWD]/openapi.json", +1> "id": "equal_numeric_bounds_to_enum", +1> "message": "Setting `minimum` and `maximum` to the same number only leaves one possible value", +1> "description": null, +1> "schemaLocation": [ "components", "schemas", "Exact", "maximum" ], +1> "position": [ 12, 9, 12, 20 ] +1> } +1> ] +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/lint/fail_lint_openapi_version.clitest b/test/lint/fail_lint_openapi_version.clitest index d2c28dc18..51aedf88e 100755 --- a/test/lint/fail_lint_openapi_version.clitest +++ b/test/lint/fail_lint_openapi_version.clitest @@ -1,6 +1,6 @@ WRITE openapi.json UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "components": { "schemas": { @@ -20,7 +20,7 @@ REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF 2> error: This OpenAPI Specification revision is not supported -2> at value 3.0.3 +2> at value 4.0.0 2> at file path [CWD]/openapi.json EOF @@ -34,7 +34,7 @@ REPLACE $CWD WITH '[CWD]' IN result_1.txt WRITE expected_1.txt UNTIL EOF 1> { 1> "error": "This OpenAPI Specification revision is not supported", -1> "value": "3.0.3", +1> "value": "4.0.0", 1> "filePath": "[CWD]/openapi.json" 1> } EOF @@ -48,7 +48,7 @@ REPLACE $CWD WITH '[CWD]' IN result_2.txt WRITE expected_2.txt UNTIL EOF 2> error: This OpenAPI Specification revision is not supported -2> at value 3.0.3 +2> at value 4.0.0 2> at file path [CWD]/openapi.json EOF @@ -56,7 +56,7 @@ COMPARE result_2.txt AGAINST expected_2.txt WRITE expected_file_0.txt UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "components": { "schemas": { diff --git a/test/lint/fail_lint_rule_invalid_yaml.clitest b/test/lint/fail_lint_rule_invalid_yaml.clitest old mode 100644 new mode 100755 index ea7f4c7f0..2f26df9b4 --- a/test/lint/fail_lint_rule_invalid_yaml.clitest +++ b/test/lint/fail_lint_rule_invalid_yaml.clitest @@ -1,7 +1,5 @@ WRITE rule.yaml UNTIL EOF -title: require_type ---- -title: another +title: [require_type EOF WRITE schema.json UNTIL EOF @@ -17,9 +15,9 @@ RUN lint --rule rule.yaml schema.json STDIN /dev/null IN . INTO result_0.txt EXP REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF -2> error: Unexpected content after document -2> at line 3 -2> at column 1 +2> error: Missing comma in flow sequence +2> at line 2 +2> at column 0 2> at file path [CWD]/rule.yaml EOF @@ -32,9 +30,9 @@ REPLACE $CWD WITH '[CWD]' IN result_1.txt WRITE expected_1.txt UNTIL EOF 1> { -1> "error": "Unexpected content after document", -1> "line": 3, -1> "column": 1, +1> "error": "Missing comma in flow sequence", +1> "line": 2, +1> "column": 0, 1> "filePath": "[CWD]/rule.yaml" 1> } EOF diff --git a/test/lint/fail_lint_rule_multidocument.clitest b/test/lint/fail_lint_rule_multidocument.clitest new file mode 100755 index 000000000..7e9f480c0 --- /dev/null +++ b/test/lint/fail_lint_rule_multidocument.clitest @@ -0,0 +1,38 @@ +WRITE rule.yaml UNTIL EOF +title: require_type +--- +title: another +EOF + +WRITE schema.json UNTIL EOF +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +// Not supported +RUN lint --rule rule.yaml schema.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: A lint rule cannot be read from input with multiple documents +2> at file path [CWD]/rule.yaml +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Not supported +RUN lint --rule rule.yaml --json schema.json STDIN /dev/null IN . INTO result_1.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "A lint rule cannot be read from input with multiple documents", +1> "filePath": "[CWD]/rule.yaml" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/lint/pass_lint_openapi_3_0_fix.clitest b/test/lint/pass_lint_openapi_3_0_fix.clitest new file mode 100755 index 000000000..36be4b591 --- /dev/null +++ b/test/lint/pass_lint_openapi_3_0_fix.clitest @@ -0,0 +1,53 @@ +// What the fixes leave behind must remain a v3.0 Schema Object. The single +// remaining `enum` choice stays an `enum`, as the dialect has no `const` + +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.0.3", + "info": { + "title": "Example", + "version": "1.0.0" + }, + "paths": {}, + "components": { + "schemas": { + "Pet": { + "title": "Pet ", + "description": "A pet in the store.", + "type": "string", + "enum": [ "dog", "dog" ] + } + } + } +} +EOF + +RUN lint openapi.json --fix STDIN /dev/null IN . INTO result_0.txt EXPECTING 0 + +WRITE expected_0.txt UNTIL EOF +2> .... +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +WRITE expected_file_0.txt UNTIL EOF +{ + "openapi": "3.0.3", + "info": { + "title": "Example", + "version": "1.0.0" + }, + "paths": {}, + "components": { + "schemas": { + "Pet": { + "title": "Pet", + "description": "A pet in the store", + "enum": [ "dog" ] + } + } + } +} +EOF + +COMPARE openapi.json AGAINST expected_file_0.txt diff --git a/test/metaschema/fail_openapi_version.clitest b/test/metaschema/fail_openapi_version.clitest index ae50cff8c..cb97e02b3 100644 --- a/test/metaschema/fail_openapi_version.clitest +++ b/test/metaschema/fail_openapi_version.clitest @@ -3,7 +3,7 @@ WRITE openapi.json UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "paths": {} } @@ -16,7 +16,7 @@ REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF 2> error: This OpenAPI Specification revision is not supported -2> at value 3.0.3 +2> at value 4.0.0 2> at file path [CWD]/openapi.json EOF diff --git a/test/metaschema/fail_openapi_version_json.clitest b/test/metaschema/fail_openapi_version_json.clitest index 0b490a0f0..1880426b6 100644 --- a/test/metaschema/fail_openapi_version_json.clitest +++ b/test/metaschema/fail_openapi_version_json.clitest @@ -3,7 +3,7 @@ WRITE openapi.json UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "paths": {} } @@ -17,7 +17,7 @@ REPLACE $CWD WITH '[CWD]' IN result.txt WRITE expected.txt UNTIL EOF 1> { 1> "error": "This OpenAPI Specification revision is not supported", -1> "value": "3.0.3", +1> "value": "4.0.0", 1> "filePath": "[CWD]/openapi.json" 1> } EOF diff --git a/test/metaschema/pass_openapi_3_0.clitest b/test/metaschema/pass_openapi_3_0.clitest new file mode 100755 index 000000000..a1c4279a7 --- /dev/null +++ b/test/metaschema/pass_openapi_3_0.clitest @@ -0,0 +1,32 @@ +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.0.3", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/p": { + "get": { + "responses": { + "200": { + "description": "ok", + "content": { "application/json": { "schema": { "type": "object" } } } + } + } + } + } + }, + "components": { "schemas": { "Person": { "type": "string" } } } +} +EOF + +RUN metaschema openapi.json --verbose STDIN /dev/null IN . INTO result_0.txt EXPECTING 0 + +WRITE expected_0.txt UNTIL EOF +2> ok: openapi.json#/components/schemas/Person +2> matches tag:spec.openapis.org,2024-10-18:oas/3.0/dialect +2> ok: openapi.json#/paths/~1p/get/responses/200/content/application~1json/schema +2> matches tag:spec.openapis.org,2024-10-18:oas/3.0/dialect +2> +2> 2 validated, 2 passed, 0 failed +EOF + +COMPARE result_0.txt AGAINST expected_0.txt diff --git a/test/rdf/fail_compact_context_yaml_multidocument.clitest b/test/rdf/fail_compact_context_yaml_multidocument.clitest new file mode 100755 index 000000000..018753531 --- /dev/null +++ b/test/rdf/fail_compact_context_yaml_multidocument.clitest @@ -0,0 +1,46 @@ +WRITE schema.json UNTIL EOF +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "x-jsonld-type": "https://schema.org/Person", + "properties": { + "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" } + } +} +EOF + +WRITE instance.json UNTIL EOF +{ "name": "Ada" } +EOF + +WRITE context.yaml UNTIL EOF +name: https://schema.org/name +--- +name: https://example.com/name +EOF + +// Not supported +RUN rdf schema.json instance.json --compact context.yaml STDIN /dev/null IN . INTO result_0.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: A context cannot be read from input with multiple documents +2> at file path [CWD]/context.yaml +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Not supported +RUN rdf schema.json instance.json --compact context.yaml --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "A context cannot be read from input with multiple documents", +1> "filePath": "[CWD]/context.yaml" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/rdf/fail_openapi_version.clitest b/test/rdf/fail_openapi_version.clitest index 430ef0c12..d7db0d1c1 100644 --- a/test/rdf/fail_openapi_version.clitest +++ b/test/rdf/fail_openapi_version.clitest @@ -3,7 +3,7 @@ WRITE openapi.json UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "paths": {} } @@ -20,7 +20,7 @@ REPLACE $CWD WITH '[CWD]' IN result.txt WRITE expected.txt UNTIL EOF 2> error: This OpenAPI Specification revision is not supported -2> at value 3.0.3 +2> at value 4.0.0 2> at file path [CWD]/openapi.json EOF diff --git a/test/rdf/fail_openapi_version_json.clitest b/test/rdf/fail_openapi_version_json.clitest index 55375d986..a96cf8810 100644 --- a/test/rdf/fail_openapi_version_json.clitest +++ b/test/rdf/fail_openapi_version_json.clitest @@ -3,7 +3,7 @@ WRITE openapi.json UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "paths": {} } @@ -21,7 +21,7 @@ REPLACE $CWD WITH '[CWD]' IN result.txt WRITE expected.txt UNTIL EOF 1> { 1> "error": "This OpenAPI Specification revision is not supported", -1> "value": "3.0.3", +1> "value": "4.0.0", 1> "filePath": "[CWD]/openapi.json" 1> } EOF diff --git a/test/rdf/fail_schema_yaml_multidocument.clitest b/test/rdf/fail_schema_yaml_multidocument.clitest new file mode 100755 index 000000000..c96b11ab0 --- /dev/null +++ b/test/rdf/fail_schema_yaml_multidocument.clitest @@ -0,0 +1,38 @@ +WRITE schema.yaml UNTIL EOF +$schema: https://json-schema.org/draft/2020-12/schema +type: object +x-jsonld-type: https://schema.org/Person +--- +$schema: https://json-schema.org/draft/2020-12/schema +type: string +EOF + +WRITE instance.json UNTIL EOF +{ "name": "Ada" } +EOF + +// Not supported +RUN rdf schema.yaml instance.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: This command does not support input with multiple documents +2> at file path [CWD]/schema.yaml +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Not supported +RUN rdf schema.yaml instance.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "This command does not support input with multiple documents", +1> "filePath": "[CWD]/schema.yaml" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/rdf/fail_yaml_instance_syntax.clitest b/test/rdf/fail_yaml_instance_syntax.clitest index 5718f4155..69b9a00bb 100644 --- a/test/rdf/fail_yaml_instance_syntax.clitest +++ b/test/rdf/fail_yaml_instance_syntax.clitest @@ -20,7 +20,7 @@ REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF 2> error: Missing comma in flow sequence -2> at line 3 +2> at line 2 2> at column 0 2> at file path [CWD]/instance.yaml EOF @@ -35,7 +35,7 @@ REPLACE $CWD WITH '[CWD]' IN result_1.txt WRITE expected_1.txt UNTIL EOF 1> { 1> "error": "Missing comma in flow sequence", -1> "line": 3, +1> "line": 2, 1> "column": 0, 1> "filePath": "[CWD]/instance.yaml" 1> } diff --git a/test/rdf/fail_yaml_multidocument_syntax.clitest b/test/rdf/fail_yaml_multidocument_syntax.clitest index f51bbf3f5..b9d543333 100644 --- a/test/rdf/fail_yaml_multidocument_syntax.clitest +++ b/test/rdf/fail_yaml_multidocument_syntax.clitest @@ -22,7 +22,7 @@ REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF 2> error: Missing comma in flow sequence -2> at line 4 +2> at line 3 2> at column 0 2> at file path [CWD]/instances.yaml EOF @@ -37,7 +37,7 @@ REPLACE $CWD WITH '[CWD]' IN result_1.txt WRITE expected_1.txt UNTIL EOF 1> { 1> "error": "Missing comma in flow sequence", -1> "line": 4, +1> "line": 3, 1> "column": 0, 1> "filePath": "[CWD]/instances.yaml" 1> } diff --git a/test/test/fail_openapi_reference_names_a_schema.clitest b/test/test/fail_openapi_reference_names_a_schema.clitest index f81631ee2..b92a985c2 100644 --- a/test/test/fail_openapi_reference_names_a_schema.clitest +++ b/test/test/fail_openapi_reference_names_a_schema.clitest @@ -39,7 +39,7 @@ REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF 1> [CWD]/test.json: -2> error: This reference must name a place within the document it points at +2> error: This reference must name a document that holds an OpenAPI Description 2> at identifier [CWD_URI]/plain.json 2> at file path [CWD]/test.json 2> at location "/paths/~1people/$ref" diff --git a/test/test/fail_openapi_reference_names_a_schema_json.clitest b/test/test/fail_openapi_reference_names_a_schema_json.clitest index c6d63df07..a7b008056 100644 --- a/test/test/fail_openapi_reference_names_a_schema_json.clitest +++ b/test/test/fail_openapi_reference_names_a_schema_json.clitest @@ -36,7 +36,7 @@ REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF 1> { -1> "error": "This reference must name a place within the document it points at", +1> "error": "This reference must name a document that holds an OpenAPI Description", 1> "identifier": "[CWD_URI]/plain.json", 1> "filePath": "[CWD]/test.json", 1> "location": "/paths/~1people/$ref", diff --git a/test/test/fail_openapi_version.clitest b/test/test/fail_openapi_version.clitest index 964683f1e..eca33726b 100644 --- a/test/test/fail_openapi_version.clitest +++ b/test/test/fail_openapi_version.clitest @@ -3,7 +3,7 @@ WRITE openapi.json UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "paths": {}, "components": { @@ -35,7 +35,7 @@ REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF 1> [CWD]/test.json: 2> error: This OpenAPI Specification revision is not supported -2> at value 3.0.3 +2> at value 4.0.0 2> at file path [CWD]/openapi.json EOF diff --git a/test/test/fail_openapi_version_json.clitest b/test/test/fail_openapi_version_json.clitest index 22dcce94f..bd5a3c45e 100644 --- a/test/test/fail_openapi_version_json.clitest +++ b/test/test/fail_openapi_version_json.clitest @@ -1,6 +1,6 @@ WRITE openapi.json UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "paths": {}, "components": { @@ -32,7 +32,7 @@ REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF 1> { 1> "error": "This OpenAPI Specification revision is not supported", -1> "value": "3.0.3", +1> "value": "4.0.0", 1> "filePath": "[CWD]/openapi.json" 1> } EOF diff --git a/test/test/pass_openapi_target_3_0.clitest b/test/test/pass_openapi_target_3_0.clitest new file mode 100755 index 000000000..3a9a139ee --- /dev/null +++ b/test/test/pass_openapi_target_3_0.clitest @@ -0,0 +1,48 @@ +// A Schema Object of a v3.0 description is reachable by pointer like any +// other, and `nullable` admits the null that the declared type does not. + +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.0.3", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": {}, + "components": { + "schemas": { + "Name": { "type": "string", "nullable": true, "maxLength": 5 } + } + } +} +EOF + +WRITE test.json UNTIL EOF +{ + "target": "./openapi.json#/components/schemas/Name", + "tests": [ + { + "description": "A short name", + "valid": true, + "data": "Juan" + }, + { + "description": "A null name", + "valid": true, + "data": null + }, + { + "description": "A name that is too long", + "valid": false, + "data": "Juan Cruz" + } + ] +} +EOF + +RUN test test.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 0 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +1> [CWD]/test.json: PASS 3/3 +EOF + +COMPARE result_0.txt AGAINST expected_0.txt diff --git a/test/test/pass_openapi_target_3_0_exclusive_minimum.clitest b/test/test/pass_openapi_target_3_0_exclusive_minimum.clitest new file mode 100755 index 000000000..c9fcfc8ad --- /dev/null +++ b/test/test/pass_openapi_target_3_0_exclusive_minimum.clitest @@ -0,0 +1,43 @@ +// The v3.0 Schema Object spells an exclusive bound as a boolean alongside +// `minimum`, rather than as the numeric keyword later revisions use. + +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.0.3", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": {}, + "components": { + "schemas": { + "Count": { "type": "integer", "minimum": 5, "exclusiveMinimum": true } + } + } +} +EOF + +WRITE test.json UNTIL EOF +{ + "target": "./openapi.json#/components/schemas/Count", + "tests": [ + { + "description": "Above the bound", + "valid": true, + "data": 6 + }, + { + "description": "On the bound", + "valid": false, + "data": 5 + } + ] +} +EOF + +RUN test test.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 0 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +1> [CWD]/test.json: PASS 2/2 +EOF + +COMPARE result_0.txt AGAINST expected_0.txt diff --git a/test/upgrade/pass_openapi_3_0_source.clitest b/test/upgrade/pass_openapi_3_0_source.clitest new file mode 100755 index 000000000..12d18f514 --- /dev/null +++ b/test/upgrade/pass_openapi_3_0_source.clitest @@ -0,0 +1,38 @@ +// A v3.0 Schema Object has no `$schema` keyword to declare itself with, so the +// dialect is named from the outside. The keywords v3.0 spells its own way are +// translated along the way + +WRITE schema.json UNTIL EOF +{ + "type": "object", + "properties": { + "age": { "type": "integer", "minimum": 0, "exclusiveMinimum": true }, + "nick": { "type": "string", "nullable": true }, + "kind": { "type": "string", "enum": [ "dog" ] } + } +} +EOF + +RUN upgrade schema.json --default-dialect tag:spec.openapis.org,2024-10-18:oas/3.0/dialect STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> { +1> "$schema": "https://json-schema.org/draft/2020-12/schema", +1> "type": "object", +1> "properties": { +1> "age": { +1> "type": "integer", +1> "exclusiveMinimum": 0 +1> }, +1> "nick": { +1> "type": [ "string", "null" ] +1> }, +1> "kind": { +1> "type": "string", +1> "const": "dog" +1> } +1> } +1> } +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/validate/fail_openapi_version.clitest b/test/validate/fail_openapi_version.clitest index 15f7531e4..ef3349421 100644 --- a/test/validate/fail_openapi_version.clitest +++ b/test/validate/fail_openapi_version.clitest @@ -3,7 +3,7 @@ WRITE openapi.json UNTIL EOF { - "openapi": "3.0.3", + "openapi": "4.0.0", "info": { "title": "Example", "version": "1.0.0" }, "paths": {} } @@ -20,7 +20,7 @@ REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF 2> error: This OpenAPI Specification revision is not supported -2> at value 3.0.3 +2> at value 4.0.0 2> at file path [CWD]/openapi.json EOF @@ -34,7 +34,7 @@ REPLACE $CWD WITH '[CWD]' IN result_1.txt WRITE expected_1.txt UNTIL EOF 1> { 1> "error": "This OpenAPI Specification revision is not supported", -1> "value": "3.0.3", +1> "value": "4.0.0", 1> "filePath": "[CWD]/openapi.json" 1> } EOF diff --git a/test/validate/fail_schema_yaml_multidocument.clitest b/test/validate/fail_schema_yaml_multidocument.clitest new file mode 100755 index 000000000..b1e11cd4f --- /dev/null +++ b/test/validate/fail_schema_yaml_multidocument.clitest @@ -0,0 +1,37 @@ +WRITE schema.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 + +WRITE instance.json UNTIL EOF +"foo" +EOF + +// Not supported +RUN validate schema.yaml instance.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: This command does not support input with multiple documents +2> at file path [CWD]/schema.yaml +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Not supported +RUN validate schema.yaml instance.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "This command does not support input with multiple documents", +1> "filePath": "[CWD]/schema.yaml" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/vendor/blaze/CMakeLists.txt b/vendor/blaze/CMakeLists.txt index 0c07dcffd..b6723e97e 100644 --- a/vendor/blaze/CMakeLists.txt +++ b/vendor/blaze/CMakeLists.txt @@ -132,6 +132,8 @@ if(PROJECT_IS_TOP_LEVEL) "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_2019_09_invalid.json" "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_2020_12.json" "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_2020_12_invalid.json" + "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_openapi_3_0.json" + "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_openapi_3_0_invalid.json" "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_openapi_3_1.json" "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_openapi_3_2.json" "${PROJECT_SOURCE_DIR}/test/output/output_standard_basic.json" diff --git a/vendor/blaze/DEPENDENCIES b/vendor/blaze/DEPENDENCIES index c0a910cc6..cb5e4a31c 100644 --- a/vendor/blaze/DEPENDENCIES +++ b/vendor/blaze/DEPENDENCIES @@ -1,3 +1,3 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core 783d6646d159d0862755aca042dda0c426e918b2 +core https://github.com/sourcemeta/core 5181fd3a3ea5f08df23c833a0cf60b08cd7be00f jsonschema-test-suite https://github.com/json-schema-org/JSON-Schema-Test-Suite 6648e8194c69697b2e1a15fe76a06a480b183a51 diff --git a/vendor/blaze/ports/javascript/describe.mjs b/vendor/blaze/ports/javascript/describe.mjs index 00609784e..4d2718df0 100644 --- a/vendor/blaze/ports/javascript/describe.mjs +++ b/vendor/blaze/ports/javascript/describe.mjs @@ -18,6 +18,8 @@ import { ASSERTION_UNIQUE, ASSERTION_DIVISIBLE, ASSERTION_TYPE_INTEGER_BOUNDED, ASSERTION_TYPE_INTEGER_BOUNDED_STRICT, ASSERTION_TYPE_INTEGER_LOWER_BOUND, ASSERTION_TYPE_INTEGER_LOWER_BOUND_STRICT, + ASSERTION_NUMBER_INTEGER_BOUNDED, + ASSERTION_NUMBER_TYPE, ASSERTION_STRING_TYPE, ASSERTION_PROPERTY_TYPE, ASSERTION_PROPERTY_TYPE_EVALUATE, ASSERTION_PROPERTY_TYPE_STRICT, ASSERTION_PROPERTY_TYPE_STRICT_EVALUATE, @@ -702,6 +704,12 @@ export function describe(valid, instruction, evaluatePath, return message; } + if (keyword === 'x-format-assertion' && typeof annotation === 'boolean') { + return annotation + ? 'A sibling `format` keyword was expected to be enforced as an assertion' + : 'A sibling `format` keyword was expected to be collected as an annotation'; + } + return 'The unrecognized keyword ' + escapeString(keyword) + ' was collected as the annotation ' + stringifyValue(annotation); } @@ -832,6 +840,21 @@ export function describe(valid, instruction, evaluatePath, return 'The value was expected to be an integer above the given minimum'; } + if (opcode === ASSERTION_NUMBER_TYPE) { + return 'The ' + valueTypeName(target) + ' value ' + + stringifyValue(target) + + ' was expected to be exactly representable as an IEEE 754 ' + + (value === 0 ? 'single' : 'double') + + ' precision floating point number'; + } + + if (opcode === ASSERTION_NUMBER_INTEGER_BOUNDED) { + return 'The ' + valueTypeName(target) + ' value ' + + stringifyValue(target) + + ' was expected to be an integer between ' + value[0] + + ' and ' + value[1]; + } + if (opcode === ASSERTION_OBJECT_PROPERTIES_SIMPLE) { return 'The object value was expected to validate against the defined property subschemas'; } @@ -1049,6 +1072,30 @@ export function describe(valid, instruction, evaluatePath, 'string is short enough to satisfy a negative maximum length'; } + // The lower bounds below compile the same way when the bound is too large + // to hold, as nothing can be big enough to satisfy one + if (keyword === 'minLength') { + return 'The value was expected to be of a type other than string, as no ' + + 'string is long enough to satisfy a minimum length that large'; + } + + if (keyword === 'minItems') { + return 'The value was expected to be of a type other than array, as no ' + + 'array has enough items to satisfy a minimum size that large'; + } + + if (keyword === 'minProperties') { + return 'The value was expected to be of a type other than object, as no ' + + 'object has enough properties to satisfy a minimum size that large'; + } + + // `contains` compiles the same way when its `minContains` is too large to + // hold, as no array has that many items for the subschema to match + if (keyword === 'contains') { + return 'The value was expected to be of a type other than array, as no ' + + 'array has enough matching items to satisfy a minimum count that large'; + } + return describeNotTypesCheck(valid, targetType, value); } diff --git a/vendor/blaze/ports/javascript/index.mjs b/vendor/blaze/ports/javascript/index.mjs index 3a1d47e47..e804a445e 100644 --- a/vendor/blaze/ports/javascript/index.mjs +++ b/vendor/blaze/ports/javascript/index.mjs @@ -7,7 +7,7 @@ import { CONTROL_JUMP, CONTROL_DYNAMIC_ANCHOR_JUMP } from './opcodes.mjs'; -const JSON_VERSION = 7; +const JSON_VERSION = 9; const DEPTH_LIMIT = 300; const URI_REGEX = /^[a-zA-Z][a-zA-Z0-9+\-.]*:[^\s]*$/; @@ -300,48 +300,50 @@ function compileInstructionToCode(instruction, captures, visited, budget) { case 34: return fb(34); case 35: return fb(35); case 36: return fb(36); case 37: return fb(37); case 38: return fb(38); case 39: return fb(39); case 40: return fb(40); - case 41: { var r=R('t'); return r?IO+r+'if(t===void 0)return true;var a=_jt(t);return a==='+value+'||('+value+'===2&&_ii(t));':null; } + case 41: return fb(41); case 42: return fb(42); - case 43: { var r=R('t'); return r?IO+r+'if(t===void 0)return true;return _es(t)==='+value+';':null; } + case 43: { var r=R('t'); return r?IO+r+'if(t===void 0)return true;var a=_jt(t);return a==='+value+'||('+value+'===2&&_ii(t));':null; } case 44: return fb(44); - case 45: { var r=R('t'); return r?IO+r+'if(t===void 0)return true;return('+value+'&(1<<_es(t)))!==0;':null; } - case 46: return fb(46); case 47: return fb(47); case 48: return fb(48); - case 49: return fb(49); - case 50: case 51: case 52: case 53: case 54: return 'return true;'; - case 55: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return 'return false;'; var c=r; for(var j=0;j0)c+=seq(children,'t'); return c+'return true;'; } - case 62: { var r=R('t'); if(!r)return null; var c=r+TO+'return true;if(!Object.hasOwn(t,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'t'); return c+'return true;'; } - case 63: { var r=R('t'); if(!r)return null; var c=r+'if(!Array.isArray(t)||t.length<='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'t'); return c+'return true;'; } - case 64: return fb(64); case 65: return fb(65); - case 66: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; var mi=captures.length; captures.push(value); var gf=''; for(var gi=0;gi0){gf+='function(i,d,_t,_v){'+lb(gc,'i')+'return true;},';}else{gf+='null,';}} return r+TO+'return true;var __cg=['+gf+'];for(var k in t){var __mi=_c['+mi+'][k];if(__mi!==void 0){var __cf=__cg[__mi];if(__cf&&!__cf(t,d,_t,_v))return false;}}return true;'; } - case 67: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; var mi=captures.length; captures.push(value); var gf=''; for(var gi=0;gi0){gf+='function(i,d,_t,_v){'+lb(gc,'i')+'return true;},';}else{gf+='null,';}} return r+TO+'return true;var __cg=['+gf+'];for(var k in t){var __mi=_c['+mi+'][k];if(__mi===void 0)return false;var __cf=__cg[__mi];if(__cf&&!__cf(t,d,_t,_v))return false;}return true;'; } - case 68: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; return r+TO+'return true;for(var k in t){'+lb(children,'t[k]')+'}return true;'; } - case 69: return fb(69); case 70: return fb(70); case 71: return fb(71); case 72: return fb(72); case 73: return fb(73); - case 74: return fb(74); case 75: return fb(75); case 76: return fb(76); - case 77: return fb(77); - case 78: { var r=R('t'); return r?r+TO+'return true;for(var k in t){if(_es(t[k])!=='+value+')return false;}return true;':null; } - case 79: return fb(79); case 80: return fb(80); case 81: return fb(81); - case 82: return fb(82); - case 83: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; return r+'if(!Array.isArray(t))return true;for(var j=0;j=t.length)return true;for(var j='+value+';j0)c+=seq(children,'i'); return c+'return true;'; } - case 96: { var c=IO+'if(!Object.hasOwn(i,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } - case 97: { var c='if(_jt(i)!=='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } - case 98: return 'return true;'; - case 99: return fb(99); - case 100: { if(!value)return 'return true;'; if(visited&&visited.has(instruction))return fb(100); if(!visited)visited=new Set(); visited.add(instruction); var r=R('t'); if(!r)return fb(100); var c=r; for(var j=0;j0)c+=seq(children,'t'); return c+'return true;'; } + case 64: { var r=R('t'); if(!r)return null; var c=r+TO+'return true;if(!Object.hasOwn(t,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'t'); return c+'return true;'; } + case 65: { var r=R('t'); if(!r)return null; var c=r+'if(!Array.isArray(t)||t.length<='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'t'); return c+'return true;'; } + case 66: return fb(66); case 67: return fb(67); + case 68: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; var mi=captures.length; captures.push(value); var gf=''; for(var gi=0;gi0){gf+='function(i,d,_t,_v){'+lb(gc,'i')+'return true;},';}else{gf+='null,';}} return r+TO+'return true;var __cg=['+gf+'];for(var k in t){var __mi=_c['+mi+'][k];if(__mi!==void 0){var __cf=__cg[__mi];if(__cf&&!__cf(t,d,_t,_v))return false;}}return true;'; } + case 69: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; var mi=captures.length; captures.push(value); var gf=''; for(var gi=0;gi0){gf+='function(i,d,_t,_v){'+lb(gc,'i')+'return true;},';}else{gf+='null,';}} return r+TO+'return true;var __cg=['+gf+'];for(var k in t){var __mi=_c['+mi+'][k];if(__mi===void 0)return false;var __cf=__cg[__mi];if(__cf&&!__cf(t,d,_t,_v))return false;}return true;'; } + case 70: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; return r+TO+'return true;for(var k in t){'+lb(children,'t[k]')+'}return true;'; } + case 71: return fb(71); case 72: return fb(72); case 73: return fb(73); case 74: return fb(74); case 75: return fb(75); + case 76: return fb(76); case 77: return fb(77); case 78: return fb(78); + case 79: return fb(79); + case 80: { var r=R('t'); return r?r+TO+'return true;for(var k in t){if(_es(t[k])!=='+value+')return false;}return true;':null; } + case 81: return fb(81); case 82: return fb(82); case 83: return fb(83); + case 84: return fb(84); + case 85: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; return r+'if(!Array.isArray(t))return true;for(var j=0;j=t.length)return true;for(var j='+value+';j0)c+=seq(children,'i'); return c+'return true;'; } + case 98: { var c=IO+'if(!Object.hasOwn(i,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } + case 99: { var c='if(_jt(i)!=='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } + case 100: return 'return true;'; + case 101: return fb(101); + case 102: { if(!value)return 'return true;'; if(visited&&visited.has(instruction))return fb(102); if(!visited)visited=new Set(); visited.add(instruction); var r=R('t'); if(!r)return fb(102); var c=r; for(var j=0;j= range[0] && target <= range[1]; + if (evaluator.callbackMode) evaluator.callbackPop(instruction, __result); + return __result; +}; + +function AssertionNumberType(instruction, instance, depth, template, evaluator) { + const target = resolveInstance(instance, instruction[2]); + if (typeof target !== 'number' && typeof target !== 'bigint') return true; + if (evaluator.callbackMode) evaluator.callbackPush(instruction); + const __result = numberTypeMatches(target, instruction[6]); + if (evaluator.callbackMode) evaluator.callbackPop(instruction, __result); + return __result; +}; + function AssertionTypeIntegerBoundedStrict(instruction, instance, depth, template, evaluator) { const target = resolveInstance(instance, instruction[2]); if (evaluator.callbackMode) evaluator.callbackPush(instruction); @@ -2433,8 +2469,8 @@ function LoopItemsPropertiesExactlyTypeStrictHash(instruction, instance, depth, if (evaluator.callbackMode) evaluator.callbackPush(instruction); const target = resolveInstance(instance, instruction[2]); if (!Array.isArray(target)) { - if (evaluator.callbackMode) evaluator.callbackPop(instruction, false); - return false; + if (evaluator.callbackMode) evaluator.callbackPop(instruction, true); + return true; } const expectedType = instruction[6][0]; const entries = instruction[6][1][0]; @@ -2702,67 +2738,69 @@ const handlers = [ AssertionTypeIntegerBoundedStrict, // 37 AssertionTypeIntegerLowerBound, // 38 AssertionTypeIntegerLowerBoundStrict, // 39 - AssertionStringType, // 40 - AssertionPropertyType, // 41 - AssertionPropertyTypeEvaluate, // 42 - AssertionPropertyTypeStrict, // 43 - AssertionPropertyTypeStrictEvaluate, // 44 - AssertionPropertyTypeStrictAny, // 45 - AssertionPropertyTypeStrictAnyEvaluate, // 46 - AssertionArrayPrefix, // 47 - AssertionArrayPrefixEvaluate, // 48 - AssertionObjectPropertiesSimple, // 49 - AnnotationEmit, // 50 - AnnotationEmitWrapped, // 51 - AnnotationToParent, // 52 - AnnotationBasenameToParent, // 53 - Evaluate, // 54 - LogicalNot, // 55 - LogicalNotEvaluate, // 56 - LogicalOr, // 57 - LogicalAnd, // 58 - LogicalXor, // 59 - LogicalCondition, // 60 - LogicalWhenType, // 61 - LogicalWhenDefines, // 62 - LogicalWhenArraySizeGreater, // 63 - LoopPropertiesUnevaluated, // 64 - LoopPropertiesUnevaluatedExcept, // 65 - LoopPropertiesMatch, // 66 - LoopPropertiesMatchClosed, // 67 - LoopProperties, // 68 - LoopPropertiesEvaluate, // 69 - LoopPropertiesRegex, // 70 - LoopPropertiesRegexClosed, // 71 - LoopPropertiesStartsWith, // 72 - LoopPropertiesExcept, // 73 - LoopPropertiesType, // 74 - LoopPropertiesTypeEvaluate, // 75 - LoopPropertiesExactlyTypeStrict, // 76 - LoopPropertiesExactlyTypeStrictHash, // 77 - LoopPropertiesTypeStrict, // 78 - LoopPropertiesTypeStrictEvaluate, // 79 - LoopPropertiesTypeStrictAny, // 80 - LoopPropertiesTypeStrictAnyEvaluate, // 81 - LoopKeys, // 82 - LoopItems, // 83 - LoopItemsFrom, // 84 - LoopItemsUnevaluated, // 85 - LoopItemsType, // 86 - LoopItemsTypeStrict, // 87 - LoopItemsTypeStrictAny, // 88 - LoopItemsPropertiesExactlyTypeStrictHash, // 89 - LoopItemsPropertiesExactlyTypeStrictHash, // 90 - LoopItemsIntegerBounded, // 91 - LoopItemsIntegerBoundedSized, // 92 - LoopContains, // 93 - ControlGroup, // 94 - ControlGroupWhenDefines, // 95 - ControlGroupWhenDefinesDirect, // 96 - ControlGroupWhenType, // 97 - ControlEvaluate, // 98 - ControlDynamicAnchorJump, // 99 - ControlJump // 100 + AssertionNumberIntegerBounded, // 40 + AssertionNumberType, // 41 + AssertionStringType, // 42 + AssertionPropertyType, // 43 + AssertionPropertyTypeEvaluate, // 44 + AssertionPropertyTypeStrict, // 45 + AssertionPropertyTypeStrictEvaluate, // 46 + AssertionPropertyTypeStrictAny, // 47 + AssertionPropertyTypeStrictAnyEvaluate, // 48 + AssertionArrayPrefix, // 49 + AssertionArrayPrefixEvaluate, // 50 + AssertionObjectPropertiesSimple, // 51 + AnnotationEmit, // 52 + AnnotationEmitWrapped, // 53 + AnnotationToParent, // 54 + AnnotationBasenameToParent, // 55 + Evaluate, // 56 + LogicalNot, // 57 + LogicalNotEvaluate, // 58 + LogicalOr, // 59 + LogicalAnd, // 60 + LogicalXor, // 61 + LogicalCondition, // 62 + LogicalWhenType, // 63 + LogicalWhenDefines, // 64 + LogicalWhenArraySizeGreater, // 65 + LoopPropertiesUnevaluated, // 66 + LoopPropertiesUnevaluatedExcept, // 67 + LoopPropertiesMatch, // 68 + LoopPropertiesMatchClosed, // 69 + LoopProperties, // 70 + LoopPropertiesEvaluate, // 71 + LoopPropertiesRegex, // 72 + LoopPropertiesRegexClosed, // 73 + LoopPropertiesStartsWith, // 74 + LoopPropertiesExcept, // 75 + LoopPropertiesType, // 76 + LoopPropertiesTypeEvaluate, // 77 + LoopPropertiesExactlyTypeStrict, // 78 + LoopPropertiesExactlyTypeStrictHash, // 79 + LoopPropertiesTypeStrict, // 80 + LoopPropertiesTypeStrictEvaluate, // 81 + LoopPropertiesTypeStrictAny, // 82 + LoopPropertiesTypeStrictAnyEvaluate, // 83 + LoopKeys, // 84 + LoopItems, // 85 + LoopItemsFrom, // 86 + LoopItemsUnevaluated, // 87 + LoopItemsType, // 88 + LoopItemsTypeStrict, // 89 + LoopItemsTypeStrictAny, // 90 + LoopItemsPropertiesExactlyTypeStrictHash, // 91 + LoopItemsPropertiesExactlyTypeStrictHash, // 92 + LoopItemsIntegerBounded, // 93 + LoopItemsIntegerBoundedSized, // 94 + LoopContains, // 95 + ControlGroup, // 96 + ControlGroupWhenDefines, // 97 + ControlGroupWhenDefinesDirect, // 98 + ControlGroupWhenType, // 99 + ControlEvaluate, // 100 + ControlDynamicAnchorJump, // 101 + ControlJump // 102 ]; function AssertionTypeArrayBounded_fast(instruction, instance, depth, template, evaluator) { @@ -3873,7 +3911,7 @@ function LoopItemsUnevaluated_fast(instruction, instance, depth, template, evalu function LoopItemsPropertiesExactlyTypeStrictHash_fast(instruction, instance, depth, template, evaluator) { const target = resolveInstance(instance, instruction[2]); - if (!Array.isArray(target)) return false; + if (!Array.isArray(target)) return true; const expectedType = instruction[6][0]; const entries = instruction[6][1][0]; const expectedCount = entries.length; @@ -3946,6 +3984,19 @@ function AssertionTypeIntegerBounded_fast(instruction, instance, depth, template return (typeof target === 'bigint' || Number.isInteger(target)) && target >= range[0] && target <= range[1]; } +function AssertionNumberIntegerBounded_fast(instruction, instance, depth, template, evaluator) { + const target = resolveInstance(instance, instruction[2]); + if (typeof target !== 'number' && typeof target !== 'bigint') return true; + const range = instruction[6]; + return (typeof target === 'bigint' || Number.isInteger(target)) && target >= range[0] && target <= range[1]; +} + +function AssertionNumberType_fast(instruction, instance, depth, template, evaluator) { + const target = resolveInstance(instance, instruction[2]); + if (typeof target !== 'number' && typeof target !== 'bigint') return true; + return numberTypeMatches(target, instruction[6]); +} + function AssertionTypeIntegerBoundedStrict_fast(instruction, instance, depth, template, evaluator) { const target = resolveInstance(instance, instruction[2]); const range = instruction[6]; @@ -3966,35 +4017,35 @@ function AssertionTypeIntegerLowerBoundStrict_fast(instruction, instance, depth, const fastHandlers = handlers.slice(); fastHandlers[16] = AssertionTypeArrayBounded_fast; -fastHandlers[88] = LoopItemsTypeStrictAny_fast; -fastHandlers[43] = AssertionPropertyTypeStrict_fast; +fastHandlers[90] = LoopItemsTypeStrictAny_fast; +fastHandlers[45] = AssertionPropertyTypeStrict_fast; fastHandlers[11] = AssertionTypeStrict_fast; fastHandlers[4] = AssertionDefinesAllStrict_fast; fastHandlers[27] = AssertionEqual_fast; -fastHandlers[66] = LoopPropertiesMatch_fast; -fastHandlers[57] = LogicalOr_fast; -fastHandlers[100] = ControlJump_fast; +fastHandlers[68] = LoopPropertiesMatch_fast; +fastHandlers[59] = LogicalOr_fast; +fastHandlers[102] = ControlJump_fast; fastHandlers[29] = AssertionEqualsAnyStringHash_fast; -fastHandlers[59] = LogicalXor_fast; +fastHandlers[61] = LogicalXor_fast; fastHandlers[2] = AssertionDefinesStrict_fast; -fastHandlers[83] = LoopItems_fast; -fastHandlers[67] = LoopPropertiesMatchClosed_fast; +fastHandlers[85] = LoopItems_fast; +fastHandlers[69] = LoopPropertiesMatchClosed_fast; fastHandlers[14] = AssertionTypeStringBounded_fast; -fastHandlers[58] = LogicalAnd_fast; +fastHandlers[60] = LogicalAnd_fast; fastHandlers[8] = AssertionPropertyDependencies_fast; fastHandlers[10] = AssertionTypeAny_fast; -fastHandlers[60] = LogicalCondition_fast; -fastHandlers[73] = LoopPropertiesExcept_fast; +fastHandlers[62] = LogicalCondition_fast; +fastHandlers[75] = LoopPropertiesExcept_fast; fastHandlers[20] = AssertionRegex_fast; -fastHandlers[68] = LoopProperties_fast; +fastHandlers[70] = LoopProperties_fast; fastHandlers[1] = AssertionDefines_fast; -fastHandlers[61] = LogicalWhenType_fast; -fastHandlers[62] = LogicalWhenDefines_fast; +fastHandlers[63] = LogicalWhenType_fast; +fastHandlers[64] = LogicalWhenDefines_fast; fastHandlers[0] = AssertionFail_fast; -fastHandlers[93] = LoopContains_fast; -fastHandlers[55] = LogicalNot_fast; -fastHandlers[86] = LoopItemsType_fast; -fastHandlers[87] = LoopItemsTypeStrict_fast; +fastHandlers[95] = LoopContains_fast; +fastHandlers[57] = LogicalNot_fast; +fastHandlers[88] = LoopItemsType_fast; +fastHandlers[89] = LoopItemsTypeStrict_fast; fastHandlers[28] = AssertionEqualsAny_fast; fastHandlers[3] = AssertionDefinesAll_fast; fastHandlers[5] = AssertionDefinesExactly_fast; @@ -4023,44 +4074,46 @@ fastHandlers[36] = AssertionTypeIntegerBounded_fast; fastHandlers[37] = AssertionTypeIntegerBoundedStrict_fast; fastHandlers[38] = AssertionTypeIntegerLowerBound_fast; fastHandlers[39] = AssertionTypeIntegerLowerBoundStrict_fast; -fastHandlers[40] = AssertionStringType_fast; -fastHandlers[41] = AssertionPropertyType_fast; -fastHandlers[42] = AssertionPropertyTypeEvaluate_fast; -fastHandlers[44] = AssertionPropertyTypeStrictEvaluate_fast; -fastHandlers[45] = AssertionPropertyTypeStrictAny_fast; -fastHandlers[46] = AssertionPropertyTypeStrictAnyEvaluate_fast; -fastHandlers[47] = AssertionArrayPrefix_fast; -fastHandlers[48] = AssertionArrayPrefixEvaluate_fast; -fastHandlers[49] = AssertionObjectPropertiesSimple_fast; -fastHandlers[50] = AnnotationEmit_fast; -fastHandlers[51] = AnnotationEmitWrapped_fast; -fastHandlers[52] = AnnotationToParent_fast; -fastHandlers[53] = AnnotationBasenameToParent_fast; -fastHandlers[54] = Evaluate_fast; -fastHandlers[56] = LogicalNotEvaluate_fast; -fastHandlers[63] = LogicalWhenArraySizeGreater_fast; -fastHandlers[64] = LoopPropertiesUnevaluated_fast; -fastHandlers[65] = LoopPropertiesUnevaluatedExcept_fast; -fastHandlers[69] = LoopPropertiesEvaluate_fast; -fastHandlers[70] = LoopPropertiesRegex_fast; -fastHandlers[71] = LoopPropertiesRegexClosed_fast; -fastHandlers[72] = LoopPropertiesStartsWith_fast; -fastHandlers[74] = LoopPropertiesType_fast; -fastHandlers[75] = LoopPropertiesTypeEvaluate_fast; -fastHandlers[76] = LoopPropertiesExactlyTypeStrict_fast; -fastHandlers[77] = LoopPropertiesExactlyTypeStrictHash_fast; -fastHandlers[78] = LoopPropertiesTypeStrict_fast; -fastHandlers[79] = LoopPropertiesTypeStrictEvaluate_fast; -fastHandlers[80] = LoopPropertiesTypeStrictAny_fast; -fastHandlers[81] = LoopPropertiesTypeStrictAnyEvaluate_fast; -fastHandlers[82] = LoopKeys_fast; -fastHandlers[84] = LoopItemsFrom_fast; -fastHandlers[85] = LoopItemsUnevaluated_fast; -fastHandlers[89] = LoopItemsPropertiesExactlyTypeStrictHash_fast; -fastHandlers[90] = LoopItemsPropertiesExactlyTypeStrictHash_fast; -fastHandlers[91] = LoopItemsIntegerBounded_fast; -fastHandlers[92] = LoopItemsIntegerBoundedSized_fast; -fastHandlers[99] = ControlDynamicAnchorJump_fast; +fastHandlers[40] = AssertionNumberIntegerBounded_fast; +fastHandlers[41] = AssertionNumberType_fast; +fastHandlers[42] = AssertionStringType_fast; +fastHandlers[43] = AssertionPropertyType_fast; +fastHandlers[44] = AssertionPropertyTypeEvaluate_fast; +fastHandlers[46] = AssertionPropertyTypeStrictEvaluate_fast; +fastHandlers[47] = AssertionPropertyTypeStrictAny_fast; +fastHandlers[48] = AssertionPropertyTypeStrictAnyEvaluate_fast; +fastHandlers[49] = AssertionArrayPrefix_fast; +fastHandlers[50] = AssertionArrayPrefixEvaluate_fast; +fastHandlers[51] = AssertionObjectPropertiesSimple_fast; +fastHandlers[52] = AnnotationEmit_fast; +fastHandlers[53] = AnnotationEmitWrapped_fast; +fastHandlers[54] = AnnotationToParent_fast; +fastHandlers[55] = AnnotationBasenameToParent_fast; +fastHandlers[56] = Evaluate_fast; +fastHandlers[58] = LogicalNotEvaluate_fast; +fastHandlers[65] = LogicalWhenArraySizeGreater_fast; +fastHandlers[66] = LoopPropertiesUnevaluated_fast; +fastHandlers[67] = LoopPropertiesUnevaluatedExcept_fast; +fastHandlers[71] = LoopPropertiesEvaluate_fast; +fastHandlers[72] = LoopPropertiesRegex_fast; +fastHandlers[73] = LoopPropertiesRegexClosed_fast; +fastHandlers[74] = LoopPropertiesStartsWith_fast; +fastHandlers[76] = LoopPropertiesType_fast; +fastHandlers[77] = LoopPropertiesTypeEvaluate_fast; +fastHandlers[78] = LoopPropertiesExactlyTypeStrict_fast; +fastHandlers[79] = LoopPropertiesExactlyTypeStrictHash_fast; +fastHandlers[80] = LoopPropertiesTypeStrict_fast; +fastHandlers[81] = LoopPropertiesTypeStrictEvaluate_fast; +fastHandlers[82] = LoopPropertiesTypeStrictAny_fast; +fastHandlers[83] = LoopPropertiesTypeStrictAnyEvaluate_fast; +fastHandlers[84] = LoopKeys_fast; +fastHandlers[86] = LoopItemsFrom_fast; +fastHandlers[87] = LoopItemsUnevaluated_fast; +fastHandlers[91] = LoopItemsPropertiesExactlyTypeStrictHash_fast; +fastHandlers[92] = LoopItemsPropertiesExactlyTypeStrictHash_fast; +fastHandlers[93] = LoopItemsIntegerBounded_fast; +fastHandlers[94] = LoopItemsIntegerBoundedSized_fast; +fastHandlers[101] = ControlDynamicAnchorJump_fast; import { describe } from './describe.mjs'; diff --git a/vendor/blaze/ports/javascript/opcodes.mjs b/vendor/blaze/ports/javascript/opcodes.mjs index 9fdfa858b..54a1eccf4 100644 --- a/vendor/blaze/ports/javascript/opcodes.mjs +++ b/vendor/blaze/ports/javascript/opcodes.mjs @@ -38,67 +38,69 @@ export const ASSERTION_TYPE_INTEGER_BOUNDED = 36; export const ASSERTION_TYPE_INTEGER_BOUNDED_STRICT = 37; export const ASSERTION_TYPE_INTEGER_LOWER_BOUND = 38; export const ASSERTION_TYPE_INTEGER_LOWER_BOUND_STRICT = 39; -export const ASSERTION_STRING_TYPE = 40; -export const ASSERTION_PROPERTY_TYPE = 41; -export const ASSERTION_PROPERTY_TYPE_EVALUATE = 42; -export const ASSERTION_PROPERTY_TYPE_STRICT = 43; -export const ASSERTION_PROPERTY_TYPE_STRICT_EVALUATE = 44; -export const ASSERTION_PROPERTY_TYPE_STRICT_ANY = 45; -export const ASSERTION_PROPERTY_TYPE_STRICT_ANY_EVALUATE = 46; -export const ASSERTION_ARRAY_PREFIX = 47; -export const ASSERTION_ARRAY_PREFIX_EVALUATE = 48; -export const ASSERTION_OBJECT_PROPERTIES_SIMPLE = 49; -export const ANNOTATION_EMIT = 50; -export const ANNOTATION_EMIT_WRAPPED = 51; -export const ANNOTATION_TO_PARENT = 52; -export const ANNOTATION_BASENAME_TO_PARENT = 53; -export const EVALUATE = 54; -export const LOGICAL_NOT = 55; -export const LOGICAL_NOT_EVALUATE = 56; -export const LOGICAL_OR = 57; -export const LOGICAL_AND = 58; -export const LOGICAL_XOR = 59; -export const LOGICAL_CONDITION = 60; -export const LOGICAL_WHEN_TYPE = 61; -export const LOGICAL_WHEN_DEFINES = 62; -export const LOGICAL_WHEN_ARRAY_SIZE_GREATER = 63; -export const LOOP_PROPERTIES_UNEVALUATED = 64; -export const LOOP_PROPERTIES_UNEVALUATED_EXCEPT = 65; -export const LOOP_PROPERTIES_MATCH = 66; -export const LOOP_PROPERTIES_MATCH_CLOSED = 67; -export const LOOP_PROPERTIES = 68; -export const LOOP_PROPERTIES_EVALUATE = 69; -export const LOOP_PROPERTIES_REGEX = 70; -export const LOOP_PROPERTIES_REGEX_CLOSED = 71; -export const LOOP_PROPERTIES_STARTS_WITH = 72; -export const LOOP_PROPERTIES_EXCEPT = 73; -export const LOOP_PROPERTIES_TYPE = 74; -export const LOOP_PROPERTIES_TYPE_EVALUATE = 75; -export const LOOP_PROPERTIES_EXACTLY_TYPE_STRICT = 76; -export const LOOP_PROPERTIES_EXACTLY_TYPE_STRICT_HASH = 77; -export const LOOP_PROPERTIES_TYPE_STRICT = 78; -export const LOOP_PROPERTIES_TYPE_STRICT_EVALUATE = 79; -export const LOOP_PROPERTIES_TYPE_STRICT_ANY = 80; -export const LOOP_PROPERTIES_TYPE_STRICT_ANY_EVALUATE = 81; -export const LOOP_KEYS = 82; -export const LOOP_ITEMS = 83; -export const LOOP_ITEMS_FROM = 84; -export const LOOP_ITEMS_UNEVALUATED = 85; -export const LOOP_ITEMS_TYPE = 86; -export const LOOP_ITEMS_TYPE_STRICT = 87; -export const LOOP_ITEMS_TYPE_STRICT_ANY = 88; -export const LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH = 89; -export const LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH3 = 90; -export const LOOP_ITEMS_INTEGER_BOUNDED = 91; -export const LOOP_ITEMS_INTEGER_BOUNDED_SIZED = 92; -export const LOOP_CONTAINS = 93; -export const CONTROL_GROUP = 94; -export const CONTROL_GROUP_WHEN_DEFINES = 95; -export const CONTROL_GROUP_WHEN_DEFINES_DIRECT = 96; -export const CONTROL_GROUP_WHEN_TYPE = 97; -export const CONTROL_EVALUATE = 98; -export const CONTROL_DYNAMIC_ANCHOR_JUMP = 99; -export const CONTROL_JUMP = 100; +export const ASSERTION_NUMBER_INTEGER_BOUNDED = 40; +export const ASSERTION_NUMBER_TYPE = 41; +export const ASSERTION_STRING_TYPE = 42; +export const ASSERTION_PROPERTY_TYPE = 43; +export const ASSERTION_PROPERTY_TYPE_EVALUATE = 44; +export const ASSERTION_PROPERTY_TYPE_STRICT = 45; +export const ASSERTION_PROPERTY_TYPE_STRICT_EVALUATE = 46; +export const ASSERTION_PROPERTY_TYPE_STRICT_ANY = 47; +export const ASSERTION_PROPERTY_TYPE_STRICT_ANY_EVALUATE = 48; +export const ASSERTION_ARRAY_PREFIX = 49; +export const ASSERTION_ARRAY_PREFIX_EVALUATE = 50; +export const ASSERTION_OBJECT_PROPERTIES_SIMPLE = 51; +export const ANNOTATION_EMIT = 52; +export const ANNOTATION_EMIT_WRAPPED = 53; +export const ANNOTATION_TO_PARENT = 54; +export const ANNOTATION_BASENAME_TO_PARENT = 55; +export const EVALUATE = 56; +export const LOGICAL_NOT = 57; +export const LOGICAL_NOT_EVALUATE = 58; +export const LOGICAL_OR = 59; +export const LOGICAL_AND = 60; +export const LOGICAL_XOR = 61; +export const LOGICAL_CONDITION = 62; +export const LOGICAL_WHEN_TYPE = 63; +export const LOGICAL_WHEN_DEFINES = 64; +export const LOGICAL_WHEN_ARRAY_SIZE_GREATER = 65; +export const LOOP_PROPERTIES_UNEVALUATED = 66; +export const LOOP_PROPERTIES_UNEVALUATED_EXCEPT = 67; +export const LOOP_PROPERTIES_MATCH = 68; +export const LOOP_PROPERTIES_MATCH_CLOSED = 69; +export const LOOP_PROPERTIES = 70; +export const LOOP_PROPERTIES_EVALUATE = 71; +export const LOOP_PROPERTIES_REGEX = 72; +export const LOOP_PROPERTIES_REGEX_CLOSED = 73; +export const LOOP_PROPERTIES_STARTS_WITH = 74; +export const LOOP_PROPERTIES_EXCEPT = 75; +export const LOOP_PROPERTIES_TYPE = 76; +export const LOOP_PROPERTIES_TYPE_EVALUATE = 77; +export const LOOP_PROPERTIES_EXACTLY_TYPE_STRICT = 78; +export const LOOP_PROPERTIES_EXACTLY_TYPE_STRICT_HASH = 79; +export const LOOP_PROPERTIES_TYPE_STRICT = 80; +export const LOOP_PROPERTIES_TYPE_STRICT_EVALUATE = 81; +export const LOOP_PROPERTIES_TYPE_STRICT_ANY = 82; +export const LOOP_PROPERTIES_TYPE_STRICT_ANY_EVALUATE = 83; +export const LOOP_KEYS = 84; +export const LOOP_ITEMS = 85; +export const LOOP_ITEMS_FROM = 86; +export const LOOP_ITEMS_UNEVALUATED = 87; +export const LOOP_ITEMS_TYPE = 88; +export const LOOP_ITEMS_TYPE_STRICT = 89; +export const LOOP_ITEMS_TYPE_STRICT_ANY = 90; +export const LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH = 91; +export const LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH3 = 92; +export const LOOP_ITEMS_INTEGER_BOUNDED = 93; +export const LOOP_ITEMS_INTEGER_BOUNDED_SIZED = 94; +export const LOOP_CONTAINS = 95; +export const CONTROL_GROUP = 96; +export const CONTROL_GROUP_WHEN_DEFINES = 97; +export const CONTROL_GROUP_WHEN_DEFINES_DIRECT = 98; +export const CONTROL_GROUP_WHEN_TYPE = 99; +export const CONTROL_EVALUATE = 100; +export const CONTROL_DYNAMIC_ANCHOR_JUMP = 101; +export const CONTROL_JUMP = 102; export const INSTRUCTION_NAMES = { "AssertionFail": ASSERTION_FAIL, @@ -141,6 +143,8 @@ export const INSTRUCTION_NAMES = { "AssertionTypeIntegerBoundedStrict": ASSERTION_TYPE_INTEGER_BOUNDED_STRICT, "AssertionTypeIntegerLowerBound": ASSERTION_TYPE_INTEGER_LOWER_BOUND, "AssertionTypeIntegerLowerBoundStrict": ASSERTION_TYPE_INTEGER_LOWER_BOUND_STRICT, + "AssertionNumberIntegerBounded": ASSERTION_NUMBER_INTEGER_BOUNDED, + "AssertionNumberType": ASSERTION_NUMBER_TYPE, "AssertionStringType": ASSERTION_STRING_TYPE, "AssertionPropertyType": ASSERTION_PROPERTY_TYPE, "AssertionPropertyTypeEvaluate": ASSERTION_PROPERTY_TYPE_EVALUATE, diff --git a/vendor/blaze/schemas/canonical-draft3.json b/vendor/blaze/schemas/canonical-draft3.json index 283acb2b8..c269051b6 100644 --- a/vendor/blaze/schemas/canonical-draft3.json +++ b/vendor/blaze/schemas/canonical-draft3.json @@ -19,20 +19,6 @@ ], "$ref": "#/$defs/document", "$defs": { - "metadata": { - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "default": true - }, - "patternProperties": { - "^x-": true - } - }, "document": { "x-lint-exclude": "simple_properties_identifiers", "type": "object", @@ -46,11 +32,12 @@ "maxItems": 1, "minItems": 1, "items": { - "$ref": "#/$defs/entry" + "$ref": "#/$defs/edge" } }, "definitions": { "type": "object", + "minProperties": 1, "propertyNames": { "pattern": "^(0|[1-9][0-9]*)$" }, @@ -59,20 +46,9 @@ } } }, - "unevaluatedProperties": false + "additionalProperties": false }, "edge": { - "x-lint-exclude": "simple_properties_identifiers", - "type": "object", - "required": [ "$ref" ], - "properties": { - "$ref": { - "type": "string" - } - }, - "unevaluatedProperties": false - }, - "entry": { "x-lint-exclude": "simple_properties_identifiers", "type": "object", "required": [ "$ref" ], @@ -82,7 +58,7 @@ "pattern": "^#/definitions/(0|[1-9][0-9]*)$" } }, - "unevaluatedProperties": false + "additionalProperties": false }, "subproperty": { "anyOf": [ @@ -105,7 +81,7 @@ } } }, - "unevaluatedProperties": false + "additionalProperties": false } ] }, @@ -119,7 +95,6 @@ }, { "x-lint-exclude": "simple_properties_identifiers", - "$ref": "#/$defs/metadata", "type": "object", "required": [ "$ref" ], "properties": { @@ -127,10 +102,9 @@ "type": "string" } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "enum" ], "properties": { @@ -172,10 +146,9 @@ "uniqueItems": true } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "type", "minLength" ], "properties": { @@ -197,10 +170,9 @@ "type": "string" } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "type", "divisibleBy" ], "properties": { @@ -218,10 +190,9 @@ "type": "number" } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "type" ], "properties": { @@ -245,10 +216,9 @@ "type": "boolean" } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "type", @@ -276,10 +246,9 @@ "$ref": "#/$defs/edge" } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "type", "items", "minItems", "uniqueItems" ], "properties": { @@ -301,10 +270,9 @@ "type": "boolean" } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "type", @@ -339,10 +307,9 @@ "type": "boolean" } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "type" ], "properties": { @@ -354,10 +321,9 @@ } } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "extends" ], "properties": { @@ -369,10 +335,9 @@ } } }, - "unevaluatedProperties": false + "additionalProperties": false }, { - "$ref": "#/$defs/metadata", "type": "object", "required": [ "disallow" ], "properties": { @@ -385,7 +350,7 @@ } } }, - "unevaluatedProperties": false + "additionalProperties": false } ] } diff --git a/vendor/blaze/src/alterschema/alterschema.cc b/vendor/blaze/src/alterschema/alterschema.cc index e27e0eb61..f3739950d 100644 --- a/vendor/blaze/src/alterschema/alterschema.cc +++ b/vendor/blaze/src/alterschema/alterschema.cc @@ -84,6 +84,21 @@ inline auto is_known_type_form(const JSON &type, }); } +// OpenAPI 3.0 bans `type: "null"` and offers `nullable` in its place, which +// widens a sibling `type` and nothing else. It takes effect only where `type` +// is present, so on its own it constrains nothing +inline auto is_nullable(const JSON &schema, + const SchemaVocabularies &vocabularies) -> bool { + if (!vocabularies.contains(SchemaVocabularies::Known::OPENAPI_3_0_BASE) || + !schema.is_object()) { + return false; + } + + const auto *nullable{schema.try_at("nullable")}; + return nullable != nullptr && nullable->is_boolean() && + nullable->to_boolean(); +} + // Walk up from a schema location, continuing as long as the traversal // predicate returns true for each keyword type encountered. Returns a // reference to the pointer of the ancestor where the match callback returned diff --git a/vendor/blaze/src/alterschema/common/anyof_true_simplify.h b/vendor/blaze/src/alterschema/common/anyof_true_simplify.h index ddf027bda..c471c8322 100644 --- a/vendor/blaze/src/alterschema/common/anyof_true_simplify.h +++ b/vendor/blaze/src/alterschema/common/anyof_true_simplify.h @@ -23,7 +23,8 @@ class AnyOfTrueSimplify final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_2019_09_APPLICATOR, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object() && schema.defines(KEYWORD) && schema.at(KEYWORD).is_array()); diff --git a/vendor/blaze/src/alterschema/common/double_negation_elimination.h b/vendor/blaze/src/alterschema/common/double_negation_elimination.h index 93d275221..41a9be1d1 100644 --- a/vendor/blaze/src/alterschema/common/double_negation_elimination.h +++ b/vendor/blaze/src/alterschema/common/double_negation_elimination.h @@ -18,6 +18,9 @@ class DoubleNegationElimination final : public SchemaTransformRule { const sourcemeta::core::SchemaResolver &) const -> SchemaTransformRule::Result override { static const JSON::String KEYWORD{"not"}; + // The OpenAPI 3.0 Schema Object is deliberately absent, as merging the + // inner schema can place a `nullable` beside a `type` it never sat next + // to, which widens what that type admits ONLY_CONTINUE_IF( vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_2020_12_APPLICATOR, diff --git a/vendor/blaze/src/alterschema/common/draft_ref_siblings.h b/vendor/blaze/src/alterschema/common/draft_ref_siblings.h index 4fc78aba5..0c553cff4 100644 --- a/vendor/blaze/src/alterschema/common/draft_ref_siblings.h +++ b/vendor/blaze/src/alterschema/common/draft_ref_siblings.h @@ -24,7 +24,8 @@ class DraftRefSiblings final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object() && schema.defines("$ref")); std::vector locations; diff --git a/vendor/blaze/src/alterschema/common/drop_allof_empty_schemas.h b/vendor/blaze/src/alterschema/common/drop_allof_empty_schemas.h index e5ffcf341..a9b1a91e5 100644 --- a/vendor/blaze/src/alterschema/common/drop_allof_empty_schemas.h +++ b/vendor/blaze/src/alterschema/common/drop_allof_empty_schemas.h @@ -16,7 +16,8 @@ class DropAllOfEmptySchemas final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_2019_09_APPLICATOR, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object()); const auto *all_of{schema.try_at("allOf")}; ONLY_CONTINUE_IF(all_of && all_of->is_array() && !all_of->empty()); diff --git a/vendor/blaze/src/alterschema/common/duplicate_allof_branches.h b/vendor/blaze/src/alterschema/common/duplicate_allof_branches.h index 7d4833495..ba05b500b 100644 --- a/vendor/blaze/src/alterschema/common/duplicate_allof_branches.h +++ b/vendor/blaze/src/alterschema/common/duplicate_allof_branches.h @@ -25,7 +25,8 @@ class DuplicateAllOfBranches final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_2019_09_APPLICATOR, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object()); const auto *all_of{schema.try_at("allOf")}; diff --git a/vendor/blaze/src/alterschema/common/duplicate_anyof_branches.h b/vendor/blaze/src/alterschema/common/duplicate_anyof_branches.h index c66e51332..b29468797 100644 --- a/vendor/blaze/src/alterschema/common/duplicate_anyof_branches.h +++ b/vendor/blaze/src/alterschema/common/duplicate_anyof_branches.h @@ -25,7 +25,8 @@ class DuplicateAnyOfBranches final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_2019_09_APPLICATOR, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object()); const auto *any_of{schema.try_at("anyOf")}; diff --git a/vendor/blaze/src/alterschema/common/duplicate_enum_values.h b/vendor/blaze/src/alterschema/common/duplicate_enum_values.h index 21907b0bb..2864185c0 100644 --- a/vendor/blaze/src/alterschema/common/duplicate_enum_values.h +++ b/vendor/blaze/src/alterschema/common/duplicate_enum_values.h @@ -26,7 +26,8 @@ class DuplicateEnumValues final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object()); const auto *enum_value{schema.try_at("enum")}; diff --git a/vendor/blaze/src/alterschema/common/duplicate_required_values.h b/vendor/blaze/src/alterschema/common/duplicate_required_values.h index b4b91d734..160de6344 100644 --- a/vendor/blaze/src/alterschema/common/duplicate_required_values.h +++ b/vendor/blaze/src/alterschema/common/duplicate_required_values.h @@ -23,7 +23,8 @@ class DuplicateRequiredValues final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_2019_09_VALIDATION, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object()); const auto *required{schema.try_at("required")}; diff --git a/vendor/blaze/src/alterschema/common/enum_with_type.h b/vendor/blaze/src/alterschema/common/enum_with_type.h index f381f0cf2..2fdb1f987 100644 --- a/vendor/blaze/src/alterschema/common/enum_with_type.h +++ b/vendor/blaze/src/alterschema/common/enum_with_type.h @@ -26,7 +26,8 @@ class EnumWithType final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object()); const auto *type{schema.try_at("type")}; ONLY_CONTINUE_IF(type); diff --git a/vendor/blaze/src/alterschema/common/equal_numeric_bounds_to_enum.h b/vendor/blaze/src/alterschema/common/equal_numeric_bounds_to_enum.h index 50224471e..d795cee43 100644 --- a/vendor/blaze/src/alterschema/common/equal_numeric_bounds_to_enum.h +++ b/vendor/blaze/src/alterschema/common/equal_numeric_bounds_to_enum.h @@ -23,9 +23,14 @@ class EqualNumericBoundsToEnum final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object()); + // The rewrite drops `type`, which is what makes a sibling `nullable` take + // effect, so null would stop being a permitted value + ONLY_CONTINUE_IF(!is_nullable(schema, vocabularies)); + const auto *type{schema.try_at("type")}; ONLY_CONTINUE_IF( type && type->is_string() && diff --git a/vendor/blaze/src/alterschema/common/exclusive_bounds_false_drop.h b/vendor/blaze/src/alterschema/common/exclusive_bounds_false_drop.h index 04c0c12bf..76b587414 100644 --- a/vendor/blaze/src/alterschema/common/exclusive_bounds_false_drop.h +++ b/vendor/blaze/src/alterschema/common/exclusive_bounds_false_drop.h @@ -20,7 +20,8 @@ class ExclusiveBoundsFalseDrop final : public SchemaTransformRule { ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object()); const auto *type{schema.try_at("type")}; diff --git a/vendor/blaze/src/alterschema/common/flatten_nested_allof.h b/vendor/blaze/src/alterschema/common/flatten_nested_allof.h index 03d644f5f..2567543fb 100644 --- a/vendor/blaze/src/alterschema/common/flatten_nested_allof.h +++ b/vendor/blaze/src/alterschema/common/flatten_nested_allof.h @@ -24,7 +24,8 @@ class FlattenNestedAllOf final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_2019_09_APPLICATOR, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object() && schema.defines(KEYWORD) && schema.at(KEYWORD).is_array()); diff --git a/vendor/blaze/src/alterschema/common/flatten_nested_anyof.h b/vendor/blaze/src/alterschema/common/flatten_nested_anyof.h index da7cf45ac..0dc5480b9 100644 --- a/vendor/blaze/src/alterschema/common/flatten_nested_anyof.h +++ b/vendor/blaze/src/alterschema/common/flatten_nested_anyof.h @@ -24,7 +24,8 @@ class FlattenNestedAnyOf final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_2019_09_APPLICATOR, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object() && schema.defines(KEYWORD) && schema.at(KEYWORD).is_array()); diff --git a/vendor/blaze/src/alterschema/common/maximum_real_for_integer.h b/vendor/blaze/src/alterschema/common/maximum_real_for_integer.h index 7cf8ee1e4..d93ce7b79 100644 --- a/vendor/blaze/src/alterschema/common/maximum_real_for_integer.h +++ b/vendor/blaze/src/alterschema/common/maximum_real_for_integer.h @@ -27,7 +27,8 @@ class MaximumRealForInteger final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object()); const auto *type{schema.try_at("type")}; diff --git a/vendor/blaze/src/alterschema/common/minimum_real_for_integer.h b/vendor/blaze/src/alterschema/common/minimum_real_for_integer.h index a97722e07..9bed43055 100644 --- a/vendor/blaze/src/alterschema/common/minimum_real_for_integer.h +++ b/vendor/blaze/src/alterschema/common/minimum_real_for_integer.h @@ -27,7 +27,8 @@ class MinimumRealForInteger final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object()); const auto *type{schema.try_at("type")}; diff --git a/vendor/blaze/src/alterschema/common/non_applicable_enum_validation_keywords.h b/vendor/blaze/src/alterschema/common/non_applicable_enum_validation_keywords.h index 0a2e048d5..b56733ce3 100644 --- a/vendor/blaze/src/alterschema/common/non_applicable_enum_validation_keywords.h +++ b/vendor/blaze/src/alterschema/common/non_applicable_enum_validation_keywords.h @@ -28,7 +28,8 @@ class NonApplicableEnumValidationKeywords final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1_HYPER}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1_HYPER, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object() && !schema.defines("type")); const auto *enum_value{schema.try_at("enum")}; diff --git a/vendor/blaze/src/alterschema/common/non_applicable_type_specific_keywords.h b/vendor/blaze/src/alterschema/common/non_applicable_type_specific_keywords.h index c9d12c906..67f79198a 100644 --- a/vendor/blaze/src/alterschema/common/non_applicable_type_specific_keywords.h +++ b/vendor/blaze/src/alterschema/common/non_applicable_type_specific_keywords.h @@ -32,7 +32,8 @@ class NonApplicableTypeSpecificKeywords final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0_HYPER}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0_HYPER, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && (type_value != nullptr) && is_known_type_form(*type_value, vocabularies) ? parse_schema_type(*type_value) @@ -46,7 +47,8 @@ class NonApplicableTypeSpecificKeywords final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1})) { + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})) { const auto *enum_value{schema.try_at("enum")}; if ((enum_value != nullptr) && enum_value->is_array()) { for (const auto &entry : enum_value->as_array()) { diff --git a/vendor/blaze/src/alterschema/common/oneof_to_anyof_disjoint_types.h b/vendor/blaze/src/alterschema/common/oneof_to_anyof_disjoint_types.h index e5c1b359f..810b58a44 100644 --- a/vendor/blaze/src/alterschema/common/oneof_to_anyof_disjoint_types.h +++ b/vendor/blaze/src/alterschema/common/oneof_to_anyof_disjoint_types.h @@ -24,7 +24,8 @@ class OneOfToAnyOfDisjointTypes final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_2019_09_APPLICATOR, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object()); const auto *oneof_value{schema.try_at(KEYWORD)}; @@ -38,7 +39,8 @@ class OneOfToAnyOfDisjointTypes final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1})}; + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})}; const auto has_const_vocabulary{vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_2020_12_VALIDATION, @@ -60,8 +62,11 @@ class OneOfToAnyOfDisjointTypes final : public SchemaTransformRule { const auto has_enum{(enum_value != nullptr) && enum_value->is_array()}; if (type_value != nullptr) { - const auto branch_types{parse_schema_type(*type_value)}; + auto branch_types{parse_schema_type(*type_value)}; ONLY_CONTINUE_IF(branch_types.any()); + if (is_nullable(branch, vocabularies)) { + branch_types.set(std::to_underlying(JSON::Type::Null)); + } type_sets.push_back(branch_types); } else if ((const_value != nullptr) && !has_enum) { JSON::TypeSet branch_types; diff --git a/vendor/blaze/src/alterschema/common/required_properties_in_properties.h b/vendor/blaze/src/alterschema/common/required_properties_in_properties.h index dd7a3299f..e6b8b2bf1 100644 --- a/vendor/blaze/src/alterschema/common/required_properties_in_properties.h +++ b/vendor/blaze/src/alterschema/common/required_properties_in_properties.h @@ -29,7 +29,8 @@ class RequiredPropertiesInProperties final : public SchemaTransformRule { vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4})) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})) && schema.is_object()); const auto *required{schema.try_at("required")}; @@ -68,7 +69,8 @@ class RequiredPropertiesInProperties final : public SchemaTransformRule { for (const auto &location : result.locations) { const auto &property{ schema.at("required").at(location.at(1).to_index()).to_string()}; - schema.at("properties").assign(property, sourcemeta::core::JSON{true}); + schema.at("properties") + .assign(property, sourcemeta::core::JSON::make_object()); } } diff --git a/vendor/blaze/src/alterschema/common/unknown_local_ref.h b/vendor/blaze/src/alterschema/common/unknown_local_ref.h index 66a5fa3f8..ae7db851e 100644 --- a/vendor/blaze/src/alterschema/common/unknown_local_ref.h +++ b/vendor/blaze/src/alterschema/common/unknown_local_ref.h @@ -29,7 +29,8 @@ class UnknownLocalRef final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object() && schema.defines(KEYWORD) && schema.at(KEYWORD).is_string()); diff --git a/vendor/blaze/src/alterschema/common/unnecessary_allof_ref_wrapper_draft.h b/vendor/blaze/src/alterschema/common/unnecessary_allof_ref_wrapper_draft.h index a90af79ea..30294d5e7 100644 --- a/vendor/blaze/src/alterschema/common/unnecessary_allof_ref_wrapper_draft.h +++ b/vendor/blaze/src/alterschema/common/unnecessary_allof_ref_wrapper_draft.h @@ -19,7 +19,8 @@ class UnnecessaryAllOfRefWrapperDraft final : public SchemaTransformRule { ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object() && schema.size() == 1); const auto *all_of{schema.try_at("allOf")}; ONLY_CONTINUE_IF(all_of && all_of->is_array()); diff --git a/vendor/blaze/src/alterschema/common/unsatisfiable_drop_validation.h b/vendor/blaze/src/alterschema/common/unsatisfiable_drop_validation.h index 0fa44de51..7fed6c34a 100644 --- a/vendor/blaze/src/alterschema/common/unsatisfiable_drop_validation.h +++ b/vendor/blaze/src/alterschema/common/unsatisfiable_drop_validation.h @@ -22,10 +22,24 @@ class UnsatisfiableDropValidation final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_2019_09_APPLICATOR, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object()); + // In Draft 7 and older dialects, a `$ref` suppresses its siblings, so the + // negation this reads is itself ignored and there is nothing to drop + if (vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})) { + ONLY_CONTINUE_IF(!schema.defines("$ref")); + } + const bool is_draft_3{vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER})}; diff --git a/vendor/blaze/src/alterschema/common/unsatisfiable_in_place_applicator_type.h b/vendor/blaze/src/alterschema/common/unsatisfiable_in_place_applicator_type.h index 8f800535e..d37d99824 100644 --- a/vendor/blaze/src/alterschema/common/unsatisfiable_in_place_applicator_type.h +++ b/vendor/blaze/src/alterschema/common/unsatisfiable_in_place_applicator_type.h @@ -18,6 +18,8 @@ class UnsatisfiableInPlaceApplicatorType final : public SchemaTransformRule { const sourcemeta::core::SchemaResolver &) const -> SchemaTransformRule::Result override { ONLY_CONTINUE_IF(schema.is_object() && schema.defines("type")); + // The OpenAPI 3.0 Schema Object is deliberately absent, as the branch this + // rewrites to the `false` schema has no spelling in that dialect ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_2020_12_VALIDATION, SchemaVocabularies::Known::JSON_SCHEMA_2019_09_VALIDATION, diff --git a/vendor/blaze/src/alterschema/linter/conflicting_readonly_writeonly.h b/vendor/blaze/src/alterschema/linter/conflicting_readonly_writeonly.h index 976dd3db2..a575707dd 100644 --- a/vendor/blaze/src/alterschema/linter/conflicting_readonly_writeonly.h +++ b/vendor/blaze/src/alterschema/linter/conflicting_readonly_writeonly.h @@ -19,7 +19,8 @@ class ConflictingReadOnlyWriteOnly final : public SchemaTransformRule { ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_2020_12_META_DATA, SchemaVocabularies::Known::JSON_SCHEMA_2019_09_META_DATA, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object()); const auto *read_only{schema.try_at("readOnly")}; const auto *write_only{schema.try_at("writeOnly")}; diff --git a/vendor/blaze/src/alterschema/linter/description_trailing_period.h b/vendor/blaze/src/alterschema/linter/description_trailing_period.h index 35d2c242e..f2e6b11ab 100644 --- a/vendor/blaze/src/alterschema/linter/description_trailing_period.h +++ b/vendor/blaze/src/alterschema/linter/description_trailing_period.h @@ -26,7 +26,8 @@ class DescriptionTrailingPeriod final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object()); ONLY_CONTINUE_IF(schema.defines("description")); ONLY_CONTINUE_IF(schema.at("description").is_string()); diff --git a/vendor/blaze/src/alterschema/linter/description_trim.h b/vendor/blaze/src/alterschema/linter/description_trim.h index 825bd8e72..dc1f8d4b0 100644 --- a/vendor/blaze/src/alterschema/linter/description_trim.h +++ b/vendor/blaze/src/alterschema/linter/description_trim.h @@ -26,7 +26,8 @@ class DescriptionTrim final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object()); ONLY_CONTINUE_IF(schema.defines("description")); ONLY_CONTINUE_IF(schema.at("description").is_string()); diff --git a/vendor/blaze/src/alterschema/linter/forbid_empty_enum.h b/vendor/blaze/src/alterschema/linter/forbid_empty_enum.h index 21c5f09a3..3d5806c69 100644 --- a/vendor/blaze/src/alterschema/linter/forbid_empty_enum.h +++ b/vendor/blaze/src/alterschema/linter/forbid_empty_enum.h @@ -22,7 +22,8 @@ class ForbidEmptyEnum final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_2019_09_VALIDATION, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object() && !schema.defines("not")); const auto *enum_value{schema.try_at("enum")}; diff --git a/vendor/blaze/src/alterschema/linter/invalid_external_ref.h b/vendor/blaze/src/alterschema/linter/invalid_external_ref.h index 3bbebeadb..a5f08c09f 100644 --- a/vendor/blaze/src/alterschema/linter/invalid_external_ref.h +++ b/vendor/blaze/src/alterschema/linter/invalid_external_ref.h @@ -22,7 +22,8 @@ class InvalidExternalRef final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object() && schema.defines(KEYWORD) && schema.at(KEYWORD).is_string()); diff --git a/vendor/blaze/src/alterschema/linter/items_schema_default.h b/vendor/blaze/src/alterschema/linter/items_schema_default.h index 3eeec498d..6169d3768 100644 --- a/vendor/blaze/src/alterschema/linter/items_schema_default.h +++ b/vendor/blaze/src/alterschema/linter/items_schema_default.h @@ -32,12 +32,22 @@ class ItemsSchemaDefault final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1_HYPER}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1_HYPER, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object() && schema.defines(KEYWORD) && ((schema.at(KEYWORD).is_boolean() && schema.at(KEYWORD).to_boolean()) || (schema.at(KEYWORD).is_object() && schema.at(KEYWORD).empty()))); ONLY_CONTINUE_IF(!frame.has_references_through( location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); + + // OpenAPI 3.0 asks that `items` be present whenever `type` names `array`, + // so the keyword has to stay however little it constrains + if (vocabularies.contains(SchemaVocabularies::Known::OPENAPI_3_0_BASE)) { + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(!(type != nullptr && type->is_string() && + type->to_string() == "array")); + } + return applies_to_keywords(KEYWORD); } diff --git a/vendor/blaze/src/alterschema/linter/multiple_of_default.h b/vendor/blaze/src/alterschema/linter/multiple_of_default.h index f45bb9964..39b35dbbc 100644 --- a/vendor/blaze/src/alterschema/linter/multiple_of_default.h +++ b/vendor/blaze/src/alterschema/linter/multiple_of_default.h @@ -22,7 +22,8 @@ class MultipleOfDefault final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_2019_09_VALIDATION, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object()); const auto *type{schema.try_at("type")}; diff --git a/vendor/blaze/src/alterschema/linter/pattern_non_ecma_regex.h b/vendor/blaze/src/alterschema/linter/pattern_non_ecma_regex.h index 7a9a2574c..332ce9611 100644 --- a/vendor/blaze/src/alterschema/linter/pattern_non_ecma_regex.h +++ b/vendor/blaze/src/alterschema/linter/pattern_non_ecma_regex.h @@ -33,7 +33,8 @@ class PatternNonEcmaRegex final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0_HYPER})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0_HYPER, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object()); const auto *pattern_value{schema.try_at("pattern")}; diff --git a/vendor/blaze/src/alterschema/linter/properties_default.h b/vendor/blaze/src/alterschema/linter/properties_default.h index 41f3ee018..0cb7246dd 100644 --- a/vendor/blaze/src/alterschema/linter/properties_default.h +++ b/vendor/blaze/src/alterschema/linter/properties_default.h @@ -29,7 +29,8 @@ class PropertiesDefault final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1_HYPER}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1_HYPER, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object()); const auto *properties{schema.try_at("properties")}; diff --git a/vendor/blaze/src/alterschema/linter/simple_properties_identifiers.h b/vendor/blaze/src/alterschema/linter/simple_properties_identifiers.h index e9f9871f9..a060062ea 100644 --- a/vendor/blaze/src/alterschema/linter/simple_properties_identifiers.h +++ b/vendor/blaze/src/alterschema/linter/simple_properties_identifiers.h @@ -30,7 +30,8 @@ class SimplePropertiesIdentifiers final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1_HYPER})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1_HYPER, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object()); const auto *properties{schema.try_at("properties")}; ONLY_CONTINUE_IF(properties && properties->is_object() && diff --git a/vendor/blaze/src/alterschema/linter/title_description_equal.h b/vendor/blaze/src/alterschema/linter/title_description_equal.h index c0d31369d..7fd657d95 100644 --- a/vendor/blaze/src/alterschema/linter/title_description_equal.h +++ b/vendor/blaze/src/alterschema/linter/title_description_equal.h @@ -26,7 +26,8 @@ class TitleDescriptionEqual final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object()); ONLY_CONTINUE_IF(schema.defines("title") && schema.defines("description")); ONLY_CONTINUE_IF(schema.at("title").is_string() && diff --git a/vendor/blaze/src/alterschema/linter/title_trailing_period.h b/vendor/blaze/src/alterschema/linter/title_trailing_period.h index 06df64fb3..a667960ba 100644 --- a/vendor/blaze/src/alterschema/linter/title_trailing_period.h +++ b/vendor/blaze/src/alterschema/linter/title_trailing_period.h @@ -26,7 +26,8 @@ class TitleTrailingPeriod final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object()); ONLY_CONTINUE_IF(schema.defines("title")); ONLY_CONTINUE_IF(schema.at("title").is_string()); diff --git a/vendor/blaze/src/alterschema/linter/title_trim.h b/vendor/blaze/src/alterschema/linter/title_trim.h index 8b779aaa1..e7ca5becd 100644 --- a/vendor/blaze/src/alterschema/linter/title_trim.h +++ b/vendor/blaze/src/alterschema/linter/title_trim.h @@ -25,7 +25,8 @@ class TitleTrim final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object()); ONLY_CONTINUE_IF(schema.defines("title")); ONLY_CONTINUE_IF(schema.at("title").is_string()); diff --git a/vendor/blaze/src/alterschema/linter/top_level_description.h b/vendor/blaze/src/alterschema/linter/top_level_description.h index 7480e0c59..564ed9abd 100644 --- a/vendor/blaze/src/alterschema/linter/top_level_description.h +++ b/vendor/blaze/src/alterschema/linter/top_level_description.h @@ -27,7 +27,8 @@ class TopLevelDescription final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object()); const auto *description{schema.try_at("description")}; if ((description != nullptr) && description->is_string() && diff --git a/vendor/blaze/src/alterschema/linter/top_level_title.h b/vendor/blaze/src/alterschema/linter/top_level_title.h index face2a284..71f1457f9 100644 --- a/vendor/blaze/src/alterschema/linter/top_level_title.h +++ b/vendor/blaze/src/alterschema/linter/top_level_title.h @@ -27,7 +27,8 @@ class TopLevelTitle final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object()); const auto *title{schema.try_at("title")}; if ((title != nullptr) && title->is_string() && title->empty()) { diff --git a/vendor/blaze/src/alterschema/linter/unknown_format_prefix.h b/vendor/blaze/src/alterschema/linter/unknown_format_prefix.h index 356c2e9ff..0185869f4 100644 --- a/vendor/blaze/src/alterschema/linter/unknown_format_prefix.h +++ b/vendor/blaze/src/alterschema/linter/unknown_format_prefix.h @@ -72,6 +72,9 @@ class UnknownFormatPrefix final : public SchemaTransformRule { vocabularies.contains(Known::JSON_SCHEMA_2020_12_FORMAT_ASSERTION)) { return &DRAFT_2020_12_FORMATS; } + if (vocabularies.contains(Known::OPENAPI_3_0_BASE)) { + return &OPENAPI_3_0_FORMATS; + } return nullptr; } @@ -104,6 +107,13 @@ class UnknownFormatPrefix final : public SchemaTransformRule { "iri", "iri-reference", "uuid", "uri-template", "json-pointer", "relative-json-pointer", "regex"}; + // The OpenAPI 3.0 Schema Object relies on the formats of the JSON Schema + // revision it is drawn from, Draft Wright-00, and adds a few of its own + // NOLINTNEXTLINE(cert-err58-cpp,bugprone-throwing-static-initialization) + static inline const std::unordered_set OPENAPI_3_0_FORMATS{ + "date-time", "email", "hostname", "ipv4", "ipv6", + "uri", "uriref", "int32", "int64", "float", + "double", "byte", "binary", "date", "password"}; static inline const std::unordered_set // NOLINTNEXTLINE(cert-err58-cpp,bugprone-throwing-static-initialization) DRAFT_2020_12_FORMATS{ diff --git a/vendor/blaze/src/alterschema/linter/unnecessary_allof_wrapper.h b/vendor/blaze/src/alterschema/linter/unnecessary_allof_wrapper.h index 3e49d26de..553372c0c 100644 --- a/vendor/blaze/src/alterschema/linter/unnecessary_allof_wrapper.h +++ b/vendor/blaze/src/alterschema/linter/unnecessary_allof_wrapper.h @@ -22,7 +22,8 @@ class UnnecessaryAllOfWrapper final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_2019_09_APPLICATOR, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4})); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE})); ONLY_CONTINUE_IF(schema.is_object()); const auto *all_of_value{schema.try_at(KEYWORD)}; @@ -68,7 +69,8 @@ class UnnecessaryAllOfWrapper final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_2019_09_VALIDATION, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) ? parse_schema_type(*parent_type_value) : JSON::TypeSet{}}; @@ -110,6 +112,15 @@ class UnnecessaryAllOfWrapper final : public SchemaTransformRule { const auto &keyword{keyword_entry.first}; const auto &metadata{walker(keyword, vocabularies)}; + // In OpenAPI 3.0, `nullable` widens the `type` that sits next to it, so + // moving either one out from under the other changes what the branch + // and the parent each admit + if ((keyword == "nullable" && is_nullable(entry, vocabularies)) || + (keyword == "type" && (is_nullable(schema, vocabularies) || + is_nullable(entry, vocabularies)))) { + return false; + } + if (elevated.contains(keyword) || (schema.defines(keyword) && schema.at(keyword) != keyword_entry.second)) { @@ -251,6 +262,11 @@ class UnnecessaryAllOfWrapper final : public SchemaTransformRule { return !entry.defines("id"); } + // The OpenAPI 3.0 Schema Object has no identifier keyword at all + if (vocabularies.contains(SchemaVocabularies::Known::OPENAPI_3_0_BASE)) { + return true; + } + return false; } }; diff --git a/vendor/blaze/src/alterschema/linter/unsatisfiable_min_properties.h b/vendor/blaze/src/alterschema/linter/unsatisfiable_min_properties.h index 3f07a1eeb..79faf1734 100644 --- a/vendor/blaze/src/alterschema/linter/unsatisfiable_min_properties.h +++ b/vendor/blaze/src/alterschema/linter/unsatisfiable_min_properties.h @@ -23,7 +23,8 @@ class UnsatisfiableMinProperties final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_2019_09_VALIDATION, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::OPENAPI_3_0_BASE}) && schema.is_object()); const auto *min_properties{schema.try_at("minProperties")}; diff --git a/vendor/blaze/src/alterschema/linter/valid_default.h b/vendor/blaze/src/alterschema/linter/valid_default.h index 792b3f33e..bb5da8bba 100644 --- a/vendor/blaze/src/alterschema/linter/valid_default.h +++ b/vendor/blaze/src/alterschema/linter/valid_default.h @@ -24,13 +24,14 @@ class ValidDefault final : public SchemaTransformRule { {Known::JSON_SCHEMA_2020_12_META_DATA, Known::JSON_SCHEMA_2019_09_META_DATA, Known::JSON_SCHEMA_DRAFT_7, Known::JSON_SCHEMA_DRAFT_6, Known::JSON_SCHEMA_DRAFT_4, - Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, + Known::OPENAPI_3_0_BASE}) && schema.is_object() && schema.defines("default")); if (vocabularies.contains_any( {Known::JSON_SCHEMA_DRAFT_7, Known::JSON_SCHEMA_DRAFT_6, Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_3, - Known::JSON_SCHEMA_DRAFT_3_HYPER})) { + Known::JSON_SCHEMA_DRAFT_3_HYPER, Known::OPENAPI_3_0_BASE})) { ONLY_CONTINUE_IF(!schema.defines("$ref")); } diff --git a/vendor/blaze/src/alterschema/schema_helpers.h b/vendor/blaze/src/alterschema/schema_helpers.h index 736fd8756..cd8848d26 100644 --- a/vendor/blaze/src/alterschema/schema_helpers.h +++ b/vendor/blaze/src/alterschema/schema_helpers.h @@ -29,6 +29,7 @@ inline auto parse_schema_type_string(const sourcemeta::core::JSON::String &type, } else if (type == "number") { result.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); result.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); + result.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); } else if (type == "integer") { result.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); } else if (type == "string") { diff --git a/vendor/blaze/src/alterschema/wrap.cc b/vendor/blaze/src/alterschema/wrap.cc index 2b65fca3c..63eeeea93 100644 --- a/vendor/blaze/src/alterschema/wrap.cc +++ b/vendor/blaze/src/alterschema/wrap.cc @@ -80,7 +80,7 @@ auto wrap(const sourcemeta::core::JSON &schema, sourcemeta::core::URI uri{identifier}; try { - schema_reidentify(copy, identifier, resolver, location.dialect); + schema_reidentify(copy, identifier, walker, resolver, location.dialect); // Otherwise we will get an error with the `WRAPPER_IDENTIFIER`, which will // be confusing to end users diff --git a/vendor/blaze/src/canonicalizer/CMakeLists.txt b/vendor/blaze/src/canonicalizer/CMakeLists.txt index e22d6c208..81dd3e87b 100644 --- a/vendor/blaze/src/canonicalizer/CMakeLists.txt +++ b/vendor/blaze/src/canonicalizer/CMakeLists.txt @@ -83,6 +83,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME canonicalizer rules/maximum_can_equal_integer_fold.h rules/maximum_can_equal_true_drop.h rules/maximum_real_for_integer.h + rules/metadata_keywords_drop.h rules/min_contains_without_contains.h rules/min_items_given_min_contains.h rules/min_length_implicit.h @@ -122,6 +123,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME canonicalizer rules/type_with_applicator_to_extends.h rules/unevaluated_items_to_items.h rules/unevaluated_properties_to_additional_properties.h + rules/unknown_keywords_drop.h rules/unknown_keywords_prefix.h rules/unknown_local_ref.h rules/unnecessary_allof_ref_wrapper_draft.h diff --git a/vendor/blaze/src/canonicalizer/canonicalize.cc b/vendor/blaze/src/canonicalizer/canonicalize.cc index a9653c0b8..55bf51853 100644 --- a/vendor/blaze/src/canonicalizer/canonicalize.cc +++ b/vendor/blaze/src/canonicalizer/canonicalize.cc @@ -405,9 +405,13 @@ auto edge_reference(const sourcemeta::core::JSON &body) return nullptr; } -/// Whether the given body is an edge rather than a node +/// Whether the given body is an edge rather than a node. Only a reference +/// that stays inside the document can reach an entry, so one that leaves it +/// becomes an entry of its own and every edge really is an edge of the graph auto is_edge(const sourcemeta::core::JSON &body) -> bool { - return edge_reference(body) != nullptr; + const auto *reference{edge_reference(body)}; + return reference != nullptr && reference->is_string() && + reference->to_string().starts_with('#'); } /// What a subschema says, with the keywords that carry the graph itself taken @@ -423,13 +427,6 @@ auto without_containers(sourcemeta::core::JSON body) -> sourcemeta::core::JSON { return body; } -/// Where following a chain of edges ends up: an entry of the graph, or a -/// reference that leaves the document. A chain that only ever reaches other -/// edges reaches neither, and cannot be expressed as a graph at all -struct Destination { - std::optional entry; - std::optional outside; -}; /// Every place inside a node body where a reference can sit, as a pointer to /// the `$ref` itself. /// @@ -526,8 +523,6 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, // Every reference the document declares, before edges are followed through std::vector> links; std::vector> reference_targets; - // References whose chain ends outside this document keep their own URI - std::vector> outside_targets; { const core::SchemaFrame frame{core::SchemaFrame::Mode::References, @@ -538,15 +533,13 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, default_id, core::SchemaFrame::IdentifierMode::Fallback}; - // Hyper-schema is left out, unlike identifier elimination above. Its - // `links` hold link descriptions rather than subschemas, so lifting them - // into `definitions` and referring to them would say something the - // document never said. The canonical form this pass produces is the one - // `canonical-draft3.json` describes, and that is Draft 3 proper + // Hyper-schema comes this far too. Nothing its vocabulary declares takes + // part in whether an instance is valid, so the rules have dropped all of + // it by now, and what is left is Draft 3 proper however the document + // spells its dialect if (frame.any_subschema( [](const core::SchemaFrame::Location &location) -> bool { - return location.base_dialect != - core::SchemaBaseDialect::JSON_SCHEMA_DRAFT_3; + return !is_draft3(location.base_dialect); })) { return; } @@ -598,14 +591,13 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, static const core::JSON::String REF_KEYWORD{"$ref"}; static const core::JSON::String EXTENDS_KEYWORD{"extends"}; - const auto resolve{[&nodes, &links, - &schema](const core::Pointer &start) -> Destination { + const auto resolve{[&nodes, &links](const core::Pointer &start) + -> std::optional { auto target{start}; for (std::size_t hop = 0; hop <= links.size(); hop++) { const auto match{std::ranges::find(nodes, target)}; if (match != nodes.end()) { - return {.entry = static_cast(match - nodes.begin()), - .outside = std::nullopt}; + return static_cast(match - nodes.begin()); } const auto direct{ @@ -629,43 +621,36 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, continue; } - // Nothing in this document takes the chain any further, so it ends at an - // edge that points out of it. Whatever referred here has to point there - // too, since the edge itself is about to go away - // Held in a named value: the reference points into it, so it has to - // outlive the check below - const auto subschema{without_containers(core::get(schema, target))}; - const auto *outside{edge_reference(subschema)}; - // Only a reference that leaves the document can be carried over. One - // that stays inside it names a subschema this pass is about to take - // away, and a `#/definitions/...` pointer would then be read back as - // one of the entries this pass itself named - if (outside != nullptr && outside->is_string() && - !outside->to_string().starts_with('#')) { - return {.entry = std::nullopt, .outside = outside->to_string()}; - } - break; } - return {.entry = std::nullopt, .outside = std::nullopt}; + return std::nullopt; }}; + // A reference whose chain goes round in circles without ever reaching a + // subschema that says anything never gets to assert anything either, so it + // is the empty schema written the long way round. Each one is replaced by an + // empty schema and the pass starts over, since the document it framed has + // changed underneath it. Every round takes a reference away, so this ends + std::vector vacuous; for (const auto &entry : links) { const auto destination{resolve(entry.second)}; - if (destination.entry.has_value()) { - reference_targets.emplace_back(entry.first, destination.entry.value()); - } else if (destination.outside.has_value()) { - outside_targets.emplace_back(entry.first, destination.outside.value()); + if (destination.has_value()) { + reference_targets.emplace_back(entry.first, destination.value()); } else { - // The chain goes round in circles without ever reaching a subschema that - // says anything. There is no entry for such a reference to point at, and - // dropping it would change what the document means, so the graph form - // does not apply here - return; + vacuous.push_back(entry.first.initial()); } } + if (!vacuous.empty()) { + for (const auto &pointer : vacuous) { + core::set(schema, pointer, sourcemeta::core::JSON::make_object()); + } + + lift_subschemas(schema, walker, resolver, default_dialect, default_id); + return; + } + // The innermost node that contains the given pointer, if any const auto enclosing{ [&nodes](const core::Pointer &pointer) -> std::optional { @@ -711,7 +696,7 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, return wrapper; }}; - const auto entry_node{resolve(core::Pointer{}).entry}; + const auto entry_node{resolve(core::Pointer{})}; if (!entry_node.has_value()) { return; } @@ -748,18 +733,6 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, std::to_string(entry.second)}); } - // A reference that ends up outside the document is written out in full, - // since the edges it used to travel through are not entries of the graph - for (const auto &entry : outside_targets) { - if (!entry.first.starts_with(pointer) || - enclosing(entry.first.initial()) != index) { - continue; - } - - core::set(body, entry.first.resolve_from(pointer), - sourcemeta::core::JSON{entry.second}); - } - if (body.is_object()) { body.erase("$schema"); body.erase("required"); @@ -1011,11 +984,16 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, } }}; + // The entries are written in the order they are numbered, so that the same + // graph always comes out spelled the same way. Following the order the + // groups were named in would instead follow where the nodes came from, and + // a document already in graph form names its nodes after the entries it + // holds, leaving `10` ahead of `2` auto merged{sourcemeta::core::JSON::make_object()}; - for (const auto &group : position) { - auto body{relabel(bodies.at(representative.at(group.first)))}; + for (std::size_t index = 0; index < walked.size(); index++) { + auto body{relabel(bodies.at(representative.at(walked.at(index))))}; unique(body); - merged.assign(std::to_string(group.second), std::move(body)); + merged.assign(std::to_string(index), std::move(body)); } const auto next{position.at(classes.at(collapsed_entry))}; @@ -1110,6 +1088,7 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, #include "rules/flatten_nested_allof.h" #include "rules/flatten_nested_anyof.h" #include "rules/flatten_nested_extends.h" +#include "rules/hyper_schema_keywords_drop.h" #include "rules/if_then_else_implicit.h" #include "rules/if_without_then_else.h" #include "rules/ignored_metaschema.h" @@ -1123,6 +1102,7 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, #include "rules/maximum_can_equal_integer_fold.h" #include "rules/maximum_can_equal_true_drop.h" #include "rules/maximum_real_for_integer.h" +#include "rules/metadata_keywords_drop.h" #include "rules/min_contains_without_contains.h" #include "rules/min_items_given_min_contains.h" #include "rules/min_length_implicit.h" @@ -1162,6 +1142,7 @@ auto lift_subschemas(sourcemeta::core::JSON &schema, #include "rules/type_with_applicator_to_extends.h" #include "rules/unevaluated_items_to_items.h" #include "rules/unevaluated_properties_to_additional_properties.h" +#include "rules/unknown_keywords_drop.h" #include "rules/unknown_keywords_prefix.h" #include "rules/unknown_local_ref.h" #include "rules/unknown_type_names.h" @@ -1192,6 +1173,8 @@ auto canonicalize(sourcemeta::core::JSON &schema, rules.push_back(make_rule()); rules.push_back(make_rule()); rules.push_back(make_rule()); + rules.push_back(make_rule()); + rules.push_back(make_rule()); rules.push_back(make_rule()); rules.push_back(make_rule()); rules.push_back(make_rule()); @@ -1258,6 +1241,7 @@ auto canonicalize(sourcemeta::core::JSON &schema, rules.push_back(make_rule()); rules.push_back(make_rule()); rules.push_back(make_rule()); + rules.push_back(make_rule()); rules.push_back(make_rule()); rules.push_back(make_rule()); rules.push_back(make_rule()); diff --git a/vendor/blaze/src/canonicalizer/helpers.h b/vendor/blaze/src/canonicalizer/helpers.h index aebd2821a..416fb1eed 100644 --- a/vendor/blaze/src/canonicalizer/helpers.h +++ b/vendor/blaze/src/canonicalizer/helpers.h @@ -13,11 +13,126 @@ inline auto is_in_place_applicator(const SchemaKeywordType type) -> bool { type == SchemaKeywordType::ApplicatorValueInPlaceNegate; } -// Walk up from a schema location, continuing as long as the traversal -// predicate returns true for each keyword type encountered. Returns a -// reference to the pointer of the ancestor where the match callback returned -// true, or nullopt if no match was found or the traversal predicate stopped -// the walk. +// TODO: Move upstream, so that the compiler can share this predicate rather +// than repeating the same check +// Draft 3 and Draft 4 read "integer" as a number written without a fractional +// part, so `3.0` is not one there, and `type: "integer"` says something that +// an `enum` of integral values cannot. Draft 6 onwards widened it to any +// number whose fractional part is zero, and the dialects before Draft 3 read +// it that way too. Kept in step with `integral_reals_are_integers` in the +// compiler, which decides the same question when evaluating, other than the +// OpenAPI 3.0 Schema Object that the compiler's copy also names. No rule here +// reaches that dialect, as every one of them asks for a JSON Schema +// vocabulary, so adding it would be a line no test could hold. A rule that +// does come to reach it needs it here too +inline auto integral_reals_are_integers(const SchemaVocabularies &vocabularies) + -> bool { + return !vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4_HYPER}); +} + +// Whether an instance of a type the schema admits can equal this value. +// Equality reads a number by its value, so an integral number matches however +// it was written, which makes this a different question from whether the value +// is itself of an admitted type: an option of `1.0` is what an instance of `1` +// equals, and that instance is an integer in every dialect +inline auto type_can_equal(const sourcemeta::core::JSON::TypeSet &types, + const sourcemeta::core::JSON &value) -> bool { + using Type = sourcemeta::core::JSON::Type; + if (!value.is_number()) { + return types.test(std::to_underlying(value.type())); + } + + return (value.is_integral() && + types.test(std::to_underlying(Type::Integer))) || + types.test(std::to_underlying(Type::Real)) || + types.test(std::to_underlying(Type::Decimal)); +} + +// Whether every instance that can equal this value is of a type the schema +// admits, which is what leaves `type` with nothing to say that the value does +// not. Draft 3 and Draft 4 do not read a number written with a fractional part +// as an integer, so `type: "integer"` still turns away the `1.0` and the `1e0` +// that an option of `1` lets through +inline auto type_always_equals(const sourcemeta::core::JSON::TypeSet &types, + const sourcemeta::core::JSON &value, + const SchemaVocabularies &vocabularies) -> bool { + using Type = sourcemeta::core::JSON::Type; + if (!value.is_number()) { + return types.test(std::to_underlying(value.type())); + } + + // A number written with a fractional or an exponent part is stored as a real + // or as a decimal, so a type that names every number covers each spelling, + // and only an integral value also has an integer one to cover + if (types.test(std::to_underlying(Type::Real)) && + types.test(std::to_underlying(Type::Decimal))) { + return !value.is_integral() || + types.test(std::to_underlying(Type::Integer)); + } + + return value.is_integral() && types.test(std::to_underlying(Type::Integer)) && + integral_reals_are_integers(vocabularies); +} + +// Whether the `type` of a schema says nothing its `enum` options do not, which +// is what lets the type go. Draft 3 reads a bare `any` and an empty subschema +// as holding for every instance, so neither says anything, while a subschema +// this cannot read may say something the options do not +inline auto type_is_implied_by_enum(const sourcemeta::core::JSON &type, + const sourcemeta::core::JSON &options, + const SchemaVocabularies &vocabularies) + -> bool { + if (vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER})) { + if (type.is_string() && type.to_string() == "any") { + return true; + } + + if (type.is_array()) { + bool has_tautology{false}; + bool has_unknown_subschema{false}; + for (const auto &entry : type.as_array()) { + if (entry.is_string() && entry.to_string() == "any") { + has_tautology = true; + break; + } + + if (entry.is_object()) { + if (entry.empty()) { + has_tautology = true; + break; + } + + has_unknown_subschema = true; + } + } + + if (has_tautology) { + return true; + } + + if (has_unknown_subschema) { + return false; + } + } + } + + const auto types{parse_schema_type(type)}; + if (!types.any()) { + return false; + } + + return std::ranges::all_of( + options.as_array(), [&types, &vocabularies](const auto &option) -> bool { + return type_always_equals(types, option, vocabularies); + }); +} + // Whether a `type` value only consists of simple type names that can be // parsed into a complete set of instance types. Draft 0 to Draft 3 unions // may contain subschemas or `any`, in which case the parsed set is an @@ -119,6 +234,11 @@ inline auto into_unsatisfiable(sourcemeta::core::JSON &schema, } } +// Walk up from a schema location, continuing as long as the traversal +// predicate returns true for each keyword type encountered. Returns a +// reference to the pointer of the ancestor where the match callback returned +// true, or nullopt if no match was found or the traversal predicate stopped +// the walk. template auto walk_up(const sourcemeta::core::JSON &root, const SchemaFrame &frame, const SchemaFrame::Location &location, const SchemaWalker &walker, @@ -170,6 +290,105 @@ auto walk_up_in_place_applicators(const sourcemeta::core::JSON &root, is_in_place_applicator, matches); } +// Walk up the in-place applicator chain of a location once, reporting whether +// any ancestor matches and collecting every scope the walk passes through on +// the way. A reference landing on any of those scopes is one that reaches this +// location without moving through the instance, which is what makes them worth +// collecting. The two answers come from the same walk because the chain and +// the predicate that bounds it are the same either way +template +auto walk_up_in_place_applicators_collecting_scopes( + const sourcemeta::core::JSON &root, const SchemaFrame &frame, + const SchemaFrame::Location &location, const SchemaWalker &walker, + const SchemaResolver &resolver, const MatchCallback &matches, + std::set &scopes) -> bool { + scopes.insert(location.pointer); + auto current_pointer{location.pointer}; + auto current_parent{location.parent}; + + while (current_parent.has_value()) { + const auto &parent_pointer{current_parent.value()}; + const auto relative_pointer{current_pointer.resolve_from(parent_pointer)}; + assert(!relative_pointer.empty() && relative_pointer.at(0).is_property()); + const auto parent{frame.traverse(parent_pointer)}; + assert(parent.has_value()); + const auto &parent_vocabularies{ + frame.vocabularies(parent.value().get(), resolver)}; + if (!is_in_place_applicator( + walker(relative_pointer.at(0).to_property(), parent_vocabularies) + .type)) { + return false; + } + + if (matches(sourcemeta::core::get(root, parent_pointer), + parent_vocabularies)) { + return true; + } + + scopes.insert(parent_pointer); + current_pointer = parent_pointer; + current_parent = parent.value().get().parent; + } + + return false; +} + +// The lexical walk stops at a subschema nothing encloses, which is what a +// definition under `$defs` looks like. Whichever scope references that +// definition still sees the annotations it produces, so a keyword that reads +// them, such as `unevaluatedItems`, can sit on the far side of the reference. +// This repeats the walk from every scope that reaches this location through a +// reference, so such a keyword is found wherever it hides +template +auto walk_up_in_place_applicators_across_references( + const sourcemeta::core::JSON &root, const SchemaFrame &frame, + const SchemaFrame::Location &location, const SchemaWalker &walker, + const SchemaResolver &resolver, const MatchCallback &matches) -> bool { + std::vector> pending{ + std::cref(location)}; + std::set visited{location.pointer}; + + while (!pending.empty()) { + const auto ¤t{pending.back().get()}; + pending.pop_back(); + + // The caller already checked the location it asked about, but a scope we + // arrived at through a reference still has to be checked on its own + if (current.pointer != location.pointer && + matches(sourcemeta::core::get(root, current.pointer), + frame.vocabularies(current, resolver))) { + return true; + } + + std::set scopes; + if (walk_up_in_place_applicators_collecting_scopes( + root, frame, current, walker, resolver, matches, scopes)) { + return true; + } + + frame.for_each_reference( + [&](const SchemaReferenceType, + const sourcemeta::core::WeakPointer &origin, + const SchemaFrame::Reference &reference) -> void { + const auto destination{frame.traverse(reference.destination)}; + if (!destination.has_value() || + !scopes.contains(destination.value().get().pointer)) { + return; + } + + const auto source{frame.traverse(origin.initial())}; + if (!source.has_value() || + !visited.insert(source.value().get().pointer).second) { + return; + } + + pending.emplace_back(std::cref(source.value().get())); + }); + } + + return false; +} + #define ONLY_CONTINUE_IF(condition) \ if (!(condition)) { \ return false; \ diff --git a/vendor/blaze/src/canonicalizer/rules/additional_items_implicit.h b/vendor/blaze/src/canonicalizer/rules/additional_items_implicit.h index f4fd2dad8..0c62c928d 100644 --- a/vendor/blaze/src/canonicalizer/rules/additional_items_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/additional_items_implicit.h @@ -14,6 +14,7 @@ class AdditionalItemsImplicit final : public SchemaTransformRule { const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7}) && @@ -24,8 +25,9 @@ class AdditionalItemsImplicit final : public SchemaTransformRule { const auto *items{schema.try_at("items")}; ONLY_CONTINUE_IF(items && items->is_array() && !schema.defines("additionalItems")); - this->is_draft3_ = - vocabularies.contains(SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3); + this->is_draft3_ = vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}); return true; } diff --git a/vendor/blaze/src/canonicalizer/rules/dependencies_to_extends_disallow.h b/vendor/blaze/src/canonicalizer/rules/dependencies_to_extends_disallow.h index 8f08ecb59..feab48824 100644 --- a/vendor/blaze/src/canonicalizer/rules/dependencies_to_extends_disallow.h +++ b/vendor/blaze/src/canonicalizer/rules/dependencies_to_extends_disallow.h @@ -13,7 +13,9 @@ class DependenciesToExtendsDisallow final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( - vocabularies.contains(SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3) && + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && schema.is_object()); const auto *dependencies{schema.try_at("dependencies")}; diff --git a/vendor/blaze/src/canonicalizer/rules/disallow_to_array_of_schemas.h b/vendor/blaze/src/canonicalizer/rules/disallow_to_array_of_schemas.h index 479037c78..e3bbaae41 100644 --- a/vendor/blaze/src/canonicalizer/rules/disallow_to_array_of_schemas.h +++ b/vendor/blaze/src/canonicalizer/rules/disallow_to_array_of_schemas.h @@ -12,15 +12,18 @@ class DisallowToArrayOfSchemas final : public SchemaTransformRule { const sourcemeta::core::SchemaFrame::Location &, const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { - ONLY_CONTINUE_IF(vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3}) && - schema.is_object() && schema.defines("disallow")); + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object() && schema.defines("disallow")); - this->convert_to_schemas_ = - vocabularies.contains(SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3); + this->convert_to_schemas_ = vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}); const auto &disallow{schema.at("disallow")}; if (disallow.is_string()) { diff --git a/vendor/blaze/src/canonicalizer/rules/divisible_by_implicit.h b/vendor/blaze/src/canonicalizer/rules/divisible_by_implicit.h index f7705e93a..72a4aacad 100644 --- a/vendor/blaze/src/canonicalizer/rules/divisible_by_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/divisible_by_implicit.h @@ -11,10 +11,12 @@ class DivisibleByImplicit final : public SchemaTransformRule { const sourcemeta::core::SchemaFrame::Location &, const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { - ONLY_CONTINUE_IF(vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3}) && - schema.is_object() && !schema.defines("divisibleBy")); + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object() && !schema.defines("divisibleBy")); const auto *type{schema.try_at("type")}; ONLY_CONTINUE_IF(type && type->is_string() && diff --git a/vendor/blaze/src/canonicalizer/rules/draft3_type_any.h b/vendor/blaze/src/canonicalizer/rules/draft3_type_any.h index 581a555d8..96fe86977 100644 --- a/vendor/blaze/src/canonicalizer/rules/draft3_type_any.h +++ b/vendor/blaze/src/canonicalizer/rules/draft3_type_any.h @@ -12,7 +12,9 @@ class Draft3TypeAny final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( - vocabularies.contains(SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3) && + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && schema.is_object()); const auto *type{schema.try_at("type")}; diff --git a/vendor/blaze/src/canonicalizer/rules/empty_dependencies_drop.h b/vendor/blaze/src/canonicalizer/rules/empty_dependencies_drop.h index 45a262ec8..875b22083 100644 --- a/vendor/blaze/src/canonicalizer/rules/empty_dependencies_drop.h +++ b/vendor/blaze/src/canonicalizer/rules/empty_dependencies_drop.h @@ -13,6 +13,7 @@ class EmptyDependenciesDrop final : public SchemaTransformRule { const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7}) && diff --git a/vendor/blaze/src/canonicalizer/rules/enum_drop_redundant_validation.h b/vendor/blaze/src/canonicalizer/rules/enum_drop_redundant_validation.h index 75615cec4..14d721f73 100644 --- a/vendor/blaze/src/canonicalizer/rules/enum_drop_redundant_validation.h +++ b/vendor/blaze/src/canonicalizer/rules/enum_drop_redundant_validation.h @@ -23,11 +23,18 @@ class EnumDropRedundantValidation final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, SchemaVocabularies::Known::JSON_SCHEMA_2019_09_VALIDATION, SchemaVocabularies::Known::JSON_SCHEMA_2020_12_VALIDATION}) && - schema.is_object() && !schema.defines("type")); + schema.is_object()); const auto *enum_value{schema.try_at("enum")}; ONLY_CONTINUE_IF(enum_value && enum_value->is_array()); + // `enum_with_type` takes away a `type` the options already imply, so one + // that is still here says something they do not, and belongs in a branch + // of its own rather than beside them + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type == nullptr || !type_is_implied_by_enum( + *type, *enum_value, vocabularies)); + this->keywords_.clear(); this->wrap_keywords_.clear(); this->is_pre_draft4_ = vocabularies.contains_any( diff --git a/vendor/blaze/src/canonicalizer/rules/enum_filter_by_type.h b/vendor/blaze/src/canonicalizer/rules/enum_filter_by_type.h index c5d34f0b9..138178b08 100644 --- a/vendor/blaze/src/canonicalizer/rules/enum_filter_by_type.h +++ b/vendor/blaze/src/canonicalizer/rules/enum_filter_by_type.h @@ -17,6 +17,7 @@ class EnumFilterByType final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, @@ -32,23 +33,12 @@ class EnumFilterByType final : public SchemaTransformRule { const auto declared_types{parse_schema_type(*type)}; ONLY_CONTINUE_IF(declared_types.any()); - const bool integer_matches_integral{ - vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, - SchemaVocabularies::Known::JSON_SCHEMA_2019_09_VALIDATION, - SchemaVocabularies::Known::JSON_SCHEMA_2020_12_VALIDATION}) && - declared_types.test( - std::to_underlying(sourcemeta::core::JSON::Type::Integer))}; this->matching_indices_.clear(); bool has_mismatch{false}; std::size_t index{0}; for (const auto &value : enum_value->as_array()) { - const bool matches{ - declared_types.test(std::to_underlying(value.type())) || - (integer_matches_integral && value.is_integral())}; - if (matches) { + if (type_can_equal(declared_types, value)) { this->matching_indices_.push_back(index); } else { has_mismatch = true; diff --git a/vendor/blaze/src/canonicalizer/rules/enum_split_by_type.h b/vendor/blaze/src/canonicalizer/rules/enum_split_by_type.h index 1d32f627a..2570419a0 100644 --- a/vendor/blaze/src/canonicalizer/rules/enum_split_by_type.h +++ b/vendor/blaze/src/canonicalizer/rules/enum_split_by_type.h @@ -28,7 +28,8 @@ class EnumSplitByType final : public SchemaTransformRule { {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3})}; + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER})}; ONLY_CONTINUE_IF((any_of_dialect || type_union_dialect) && schema.is_object()); diff --git a/vendor/blaze/src/canonicalizer/rules/enum_with_type.h b/vendor/blaze/src/canonicalizer/rules/enum_with_type.h index 60ba163e9..bf1db2604 100644 --- a/vendor/blaze/src/canonicalizer/rules/enum_with_type.h +++ b/vendor/blaze/src/canonicalizer/rules/enum_with_type.h @@ -27,54 +27,7 @@ class EnumWithType final : public SchemaTransformRule { const auto *enum_value{schema.try_at("enum")}; ONLY_CONTINUE_IF(enum_value && enum_value->is_array()); - if (vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER})) { - if (type->is_string() && type->to_string() == "any") { - return true; - } - - if (type->is_array()) { - bool has_tautology{false}; - bool has_unknown_subschema{false}; - for (const auto &entry : type->as_array()) { - if (entry.is_string() && entry.to_string() == "any") { - has_tautology = true; - break; - } - if (entry.is_object()) { - if (entry.empty()) { - has_tautology = true; - break; - } - has_unknown_subschema = true; - } - } - - if (has_tautology) { - return true; - } - - if (has_unknown_subschema) { - return false; - } - } - } - - const auto current_types{parse_schema_type(*type)}; - ONLY_CONTINUE_IF(current_types.any()); - const bool integer_matches_integral{ - vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7}) && - current_types.test( - std::to_underlying(sourcemeta::core::JSON::Type::Integer))}; - ONLY_CONTINUE_IF(std::ranges::all_of( - enum_value->as_array(), - [¤t_types, integer_matches_integral](const auto &item) -> auto { - return current_types.test(std::to_underlying(item.type())) || - (integer_matches_integral && item.is_integral()); - })); + ONLY_CONTINUE_IF(type_is_implied_by_enum(*type, *enum_value, vocabularies)); return true; } diff --git a/vendor/blaze/src/canonicalizer/rules/equal_numeric_bounds_to_enum.h b/vendor/blaze/src/canonicalizer/rules/equal_numeric_bounds_to_enum.h index b377741a6..78d5a161a 100644 --- a/vendor/blaze/src/canonicalizer/rules/equal_numeric_bounds_to_enum.h +++ b/vendor/blaze/src/canonicalizer/rules/equal_numeric_bounds_to_enum.h @@ -21,10 +21,17 @@ class EqualNumericBoundsToEnum final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0}) && schema.is_object()); + // An integer only where the dialect reads one as any number whose + // fractional part is zero. Draft 3 and Draft 4 read it as a number written + // without a fractional part, so there `type: "integer"` rejects `3.0` + // where `enum: [ 3 ]` accepts it, the two being equal as numbers. Keeping + // the type beside the enum is not an option either, as the canonical form + // has no entry that carries both const auto *type{schema.try_at("type")}; - ONLY_CONTINUE_IF( - type && type->is_string() && - (type->to_string() == "integer" || type->to_string() == "number")); + ONLY_CONTINUE_IF(type && type->is_string() && + (type->to_string() == "number" || + (type->to_string() == "integer" && + integral_reals_are_integers(vocabularies)))); const auto *minimum{schema.try_at("minimum")}; ONLY_CONTINUE_IF(minimum && minimum->is_number()); const auto *maximum{schema.try_at("maximum")}; diff --git a/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_boolean_integer_fold.h b/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_boolean_integer_fold.h index b19f814d0..869cd0efb 100644 --- a/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_boolean_integer_fold.h +++ b/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_boolean_integer_fold.h @@ -14,6 +14,7 @@ class ExclusiveMaximumBooleanIntegerFold final : public SchemaTransformRule { const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && schema.is_object()); diff --git a/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_boolean_integer_fold.h b/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_boolean_integer_fold.h index f66428a48..82db3e155 100644 --- a/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_boolean_integer_fold.h +++ b/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_boolean_integer_fold.h @@ -14,6 +14,7 @@ class ExclusiveMinimumBooleanIntegerFold final : public SchemaTransformRule { const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && schema.is_object()); diff --git a/vendor/blaze/src/canonicalizer/rules/extends_to_array.h b/vendor/blaze/src/canonicalizer/rules/extends_to_array.h index c0813696e..949604c00 100644 --- a/vendor/blaze/src/canonicalizer/rules/extends_to_array.h +++ b/vendor/blaze/src/canonicalizer/rules/extends_to_array.h @@ -11,12 +11,14 @@ class ExtendsToArray final : public SchemaTransformRule { const sourcemeta::core::SchemaFrame::Location &, const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { - ONLY_CONTINUE_IF(vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3}) && - schema.is_object()); + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object()); const auto *extends{schema.try_at("extends")}; ONLY_CONTINUE_IF(extends && !extends->is_array()); diff --git a/vendor/blaze/src/canonicalizer/rules/hyper_schema_keywords_drop.h b/vendor/blaze/src/canonicalizer/rules/hyper_schema_keywords_drop.h new file mode 100644 index 000000000..ef148659e --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/hyper_schema_keywords_drop.h @@ -0,0 +1,59 @@ +class HyperSchemaKeywordsDrop final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + HyperSchemaKeywordsDrop() + : SchemaTransformRule{"hyper_schema_keywords_drop"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::SchemaVocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const -> bool override { + // The canonical form says what makes an instance valid, and none of these + // takes part in that: `links` describes link relations, `readonly` and + // `mediaType` describe the instance to a reader, and the rest describe how + // a document is addressed. Two documents that differ in none but these + // accept the same instances, so the canonical form is the one without + // them. + // + // These are the keywords the Draft 3 hyper-schema meta-schema adds to the + // Draft 3 one, named here rather than asked of the walker: a document on a + // pre-vocabulary dialect puts every keyword it knows in the one vocabulary + // that dialect stands for, `$schema` and `properties` along with `links` + // + // Only Draft 3 is taken this far. The canonical form of every other + // dialect still describes a nested document, which leaves these keywords + // somewhere to sit, so dropping them there would be a change of its own + static const std::set HYPER_SCHEMA_KEYWORDS{ + "contentEncoding", "fragmentResolution", "links", "mediaType", + "pathStart", "readonly", "root"}; + + ONLY_CONTINUE_IF( + vocabularies.contains( + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER) && + schema.is_object()); + + std::vector keywords; + for (const auto &entry : schema.as_object()) { + if (HYPER_SCHEMA_KEYWORDS.contains(entry.first)) { + keywords.push_back(entry.first); + } + } + + ONLY_CONTINUE_IF(!keywords.empty()); + this->keywords_ = std::move(keywords); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + for (const auto &keyword : this->keywords_) { + schema.erase(keyword); + } + } + +private: + mutable std::vector keywords_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/implicit_contains_keywords.h b/vendor/blaze/src/canonicalizer/rules/implicit_contains_keywords.h index 8a7079e3a..263919fea 100644 --- a/vendor/blaze/src/canonicalizer/rules/implicit_contains_keywords.h +++ b/vendor/blaze/src/canonicalizer/rules/implicit_contains_keywords.h @@ -25,17 +25,15 @@ class ImplicitContainsKeywords final : public SchemaTransformRule { } else { ONLY_CONTINUE_IF(!schema.defines("minContains") && !schema.defines("maxContains")); - ONLY_CONTINUE_IF( - !walk_up_in_place_applicators( - root, frame, location, walker, resolver, - [](const sourcemeta::core::JSON &ancestor, - const SchemaVocabularies &ancestor_vocabularies) -> bool { - return ancestor.defines("unevaluatedItems") && - ancestor_vocabularies.contains( - SchemaVocabularies::Known:: - JSON_SCHEMA_2020_12_UNEVALUATED); - }) - .has_value()); + ONLY_CONTINUE_IF(!walk_up_in_place_applicators_across_references( + root, frame, location, walker, resolver, + [](const sourcemeta::core::JSON &ancestor, + const SchemaVocabularies &ancestor_vocabularies) -> bool { + return ancestor.defines("unevaluatedItems") && + ancestor_vocabularies.contains( + SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_UNEVALUATED); + })); } return true; diff --git a/vendor/blaze/src/canonicalizer/rules/implicit_object_keywords.h b/vendor/blaze/src/canonicalizer/rules/implicit_object_keywords.h index b34cdce1d..4119f3643 100644 --- a/vendor/blaze/src/canonicalizer/rules/implicit_object_keywords.h +++ b/vendor/blaze/src/canonicalizer/rules/implicit_object_keywords.h @@ -99,6 +99,7 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { !schema.defines("patternProperties") && vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, @@ -137,6 +138,7 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0})); @@ -146,6 +148,7 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7})}; @@ -156,7 +159,8 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3}); + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}); this->has_work_ = this->add_pattern_properties_ || this->add_property_names_ || this->add_min_properties_ || @@ -172,6 +176,7 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, @@ -191,6 +196,7 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4})}; this->add_unique_items_ = diff --git a/vendor/blaze/src/canonicalizer/rules/items_implicit.h b/vendor/blaze/src/canonicalizer/rules/items_implicit.h index bc061cad2..e582d772d 100644 --- a/vendor/blaze/src/canonicalizer/rules/items_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/items_implicit.h @@ -31,19 +31,17 @@ class ItemsImplicit final : public SchemaTransformRule { vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_2020_12_UNEVALUATED, SchemaVocabularies::Known::JSON_SCHEMA_2019_09_APPLICATOR}))); - ONLY_CONTINUE_IF( - !walk_up_in_place_applicators( - root, frame, location, walker, resolver, - [](const sourcemeta::core::JSON &ancestor, - const SchemaVocabularies &ancestor_vocabularies) -> bool { - return ancestor.defines("unevaluatedItems") && - ancestor_vocabularies.contains_any( - {SchemaVocabularies::Known:: - JSON_SCHEMA_2020_12_UNEVALUATED, - SchemaVocabularies::Known:: - JSON_SCHEMA_2019_09_APPLICATOR}); - }) - .has_value()); + ONLY_CONTINUE_IF(!walk_up_in_place_applicators_across_references( + root, frame, location, walker, resolver, + [](const sourcemeta::core::JSON &ancestor, + const SchemaVocabularies &ancestor_vocabularies) -> bool { + return ancestor.defines("unevaluatedItems") && + ancestor_vocabularies.contains_any( + {SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_UNEVALUATED, + SchemaVocabularies::Known:: + JSON_SCHEMA_2019_09_APPLICATOR}); + })); return true; } diff --git a/vendor/blaze/src/canonicalizer/rules/metadata_keywords_drop.h b/vendor/blaze/src/canonicalizer/rules/metadata_keywords_drop.h new file mode 100644 index 000000000..95d0d6d93 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/metadata_keywords_drop.h @@ -0,0 +1,57 @@ +class MetadataKeywordsDrop final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + MetadataKeywordsDrop() : SchemaTransformRule{"metadata_keywords_drop"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::SchemaVocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const -> bool override { + // The canonical form says what makes an instance valid, and none of these + // takes part in that: they name a schema, describe it, and say what to + // put where an instance says nothing. Two documents that differ in none + // but these accept the same instances, so the canonical form is the one + // without them. + // + // These are the metadata keywords of the Draft 3 meta-schema, named here + // rather than asked of the walker: a document on a pre-vocabulary dialect + // puts every keyword it knows in the one vocabulary that dialect stands + // for, `title` and `type` alike + // + // Only Draft 3 is taken this far. The canonical form of every other + // dialect still describes a nested document, which leaves these keywords + // somewhere to sit, so dropping them there would be a change of its own + static const std::set METADATA_KEYWORDS{ + "default", "description", "title"}; + + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object()); + + std::vector keywords; + for (const auto &entry : schema.as_object()) { + if (METADATA_KEYWORDS.contains(entry.first)) { + keywords.push_back(entry.first); + } + } + + ONLY_CONTINUE_IF(!keywords.empty()); + this->keywords_ = std::move(keywords); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + for (const auto &keyword : this->keywords_) { + schema.erase(keyword); + } + } + +private: + mutable std::vector keywords_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/min_length_implicit.h b/vendor/blaze/src/canonicalizer/rules/min_length_implicit.h index 597962de0..03d24591f 100644 --- a/vendor/blaze/src/canonicalizer/rules/min_length_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/min_length_implicit.h @@ -19,6 +19,7 @@ class MinLengthImplicit final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0}) && diff --git a/vendor/blaze/src/canonicalizer/rules/required_property_implicit.h b/vendor/blaze/src/canonicalizer/rules/required_property_implicit.h index 41110f918..8a303c632 100644 --- a/vendor/blaze/src/canonicalizer/rules/required_property_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/required_property_implicit.h @@ -13,7 +13,9 @@ class RequiredPropertyImplicit final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( - vocabularies.contains(SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3) && + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && schema.is_object()); const auto *type{schema.try_at("type")}; diff --git a/vendor/blaze/src/canonicalizer/rules/type_boolean_as_enum.h b/vendor/blaze/src/canonicalizer/rules/type_boolean_as_enum.h index 2c2624c75..ed383590e 100644 --- a/vendor/blaze/src/canonicalizer/rules/type_boolean_as_enum.h +++ b/vendor/blaze/src/canonicalizer/rules/type_boolean_as_enum.h @@ -19,6 +19,7 @@ class TypeBooleanAsEnum final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0}) && diff --git a/vendor/blaze/src/canonicalizer/rules/type_inherit_in_place.h b/vendor/blaze/src/canonicalizer/rules/type_inherit_in_place.h index cfc60e184..b3d8ee64c 100644 --- a/vendor/blaze/src/canonicalizer/rules/type_inherit_in_place.h +++ b/vendor/blaze/src/canonicalizer/rules/type_inherit_in_place.h @@ -18,6 +18,7 @@ class TypeInheritInPlace final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0})); diff --git a/vendor/blaze/src/canonicalizer/rules/type_null_as_enum.h b/vendor/blaze/src/canonicalizer/rules/type_null_as_enum.h index 3b2aa68b3..74249b716 100644 --- a/vendor/blaze/src/canonicalizer/rules/type_null_as_enum.h +++ b/vendor/blaze/src/canonicalizer/rules/type_null_as_enum.h @@ -19,6 +19,7 @@ class TypeNullAsEnum final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0}) && diff --git a/vendor/blaze/src/canonicalizer/rules/type_union_implicit.h b/vendor/blaze/src/canonicalizer/rules/type_union_implicit.h index 2a67e0867..49a38aa3e 100644 --- a/vendor/blaze/src/canonicalizer/rules/type_union_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/type_union_implicit.h @@ -11,12 +11,14 @@ class TypeUnionImplicit final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &walker, const sourcemeta::core::SchemaResolver &resolver) const -> bool override { ONLY_CONTINUE_IF(schema.is_object() && !schema.empty()); - ONLY_CONTINUE_IF(!vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3}) || - !schema.defines("disallow")); + ONLY_CONTINUE_IF( + !vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) || + !schema.defines("disallow")); ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_2020_12_VALIDATION, SchemaVocabularies::Known::JSON_SCHEMA_2019_09_VALIDATION, @@ -24,6 +26,7 @@ class TypeUnionImplicit final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0})); diff --git a/vendor/blaze/src/canonicalizer/rules/type_union_to_schemas.h b/vendor/blaze/src/canonicalizer/rules/type_union_to_schemas.h index eb5af68e5..2b99e7474 100644 --- a/vendor/blaze/src/canonicalizer/rules/type_union_to_schemas.h +++ b/vendor/blaze/src/canonicalizer/rules/type_union_to_schemas.h @@ -11,12 +11,14 @@ class TypeUnionToSchemas final : public SchemaTransformRule { const sourcemeta::core::SchemaFrame::Location &, const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { - ONLY_CONTINUE_IF(vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3}) && - schema.is_object()); + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object()); const auto *type{schema.try_at("type")}; ONLY_CONTINUE_IF(type && type->is_array()); diff --git a/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_allof.h b/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_allof.h index 5a2c2c79b..009a8acef 100644 --- a/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_allof.h +++ b/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_allof.h @@ -154,6 +154,22 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { return true; } + // Merging only collapses the sibling applicators into the `allOf` that + // is already there, and the condition has to come out false as soon as + // the transform returns. So merging is the right move only when those + // siblings are the whole reason we are here: typed keywords with no + // `type` to sit under, and references that need a branch of their own, + // all survive a merge and have to go through the extraction path instead + const bool mergeable_siblings{std::ranges::any_of( + APPLICATORS_WITHOUT_ALLOF, [&schema](const auto *applicator) -> bool { + return schema.defines(applicator); + })}; + if (!mergeable_siblings || has_orphaned_typed_keywords || + modern_ref_needs_wrapping || this->has_modern_ref_ || + this->has_dynamic_ref_ || this->has_recursive_ref_) { + return true; + } + const auto all_refs_fixed{!frame.any_reference_from( location.pointer, [&](const sourcemeta::core::SchemaReferenceType, diff --git a/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_extends.h b/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_extends.h index b5b8b2f0c..900271444 100644 --- a/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_extends.h +++ b/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_extends.h @@ -12,12 +12,14 @@ class TypeWithApplicatorToExtends final : public SchemaTransformRule { const sourcemeta::core::SchemaFrame::Location &, const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { - ONLY_CONTINUE_IF(vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3}) && - schema.is_object()); + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_0, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object()); const auto *extends_value{schema.try_at("extends")}; const bool has_extends{(extends_value != nullptr) && diff --git a/vendor/blaze/src/canonicalizer/rules/unknown_keywords_drop.h b/vendor/blaze/src/canonicalizer/rules/unknown_keywords_drop.h new file mode 100644 index 000000000..cba90e8cf --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/unknown_keywords_drop.h @@ -0,0 +1,50 @@ +class UnknownKeywordsDrop final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + UnknownKeywordsDrop() : SchemaTransformRule{"unknown_keywords_drop"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::SchemaVocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &) const -> bool override { + // A keyword the dialect does not define says nothing about whether an + // instance is valid, so the canonical form is the one without it. Every + // other dialect keeps such a keyword under an `x-` prefix instead, which + // `unknown_keywords_prefix` does, as their canonical forms describe a + // nested document that has somewhere to put it + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object()); + + std::vector keywords; + for (const auto &entry : schema.as_object()) { + // If there is any i.e. optional vocabulary we don't recognise, then this + // seemingly unknown keyword might belong to one of those, and thus it + // might not be safe to flag it + if (walker(entry.first, vocabularies).type == + SchemaKeywordType::Unknown && + !vocabularies.has_unknown()) { + keywords.push_back(entry.first); + } + } + + ONLY_CONTINUE_IF(!keywords.empty()); + this->keywords_ = std::move(keywords); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + for (const auto &keyword : this->keywords_) { + schema.erase(keyword); + } + } + +private: + mutable std::vector keywords_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/unknown_keywords_prefix.h b/vendor/blaze/src/canonicalizer/rules/unknown_keywords_prefix.h index f50bcc90b..b737b4bd1 100644 --- a/vendor/blaze/src/canonicalizer/rules/unknown_keywords_prefix.h +++ b/vendor/blaze/src/canonicalizer/rules/unknown_keywords_prefix.h @@ -10,7 +10,14 @@ class UnknownKeywordsPrefix final : public SchemaTransformRule { const SchemaFrame::Location &, const SchemaWalker &walker, const SchemaResolver &) const -> bool override { - ONLY_CONTINUE_IF(schema.is_object()); + // Draft 3 deletes an unknown keyword outright instead, which + // `unknown_keywords_drop` does, as its canonical form has nowhere to put + // one + ONLY_CONTINUE_IF( + !vocabularies.contains_any( + {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER}) && + schema.is_object()); std::vector locations; for (const auto &entry : schema.as_object()) { if (entry.first.starts_with("x-")) { diff --git a/vendor/blaze/src/canonicalizer/rules/unsatisfiable_empty_enum.h b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_empty_enum.h index e0089bec3..7b2a716bd 100644 --- a/vendor/blaze/src/canonicalizer/rules/unsatisfiable_empty_enum.h +++ b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_empty_enum.h @@ -17,6 +17,7 @@ class UnsatisfiableEmptyEnum final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, diff --git a/vendor/blaze/src/canonicalizer/rules/unsatisfiable_exclusive_equal_bounds.h b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_exclusive_equal_bounds.h index f459b2b6e..515aeed4e 100644 --- a/vendor/blaze/src/canonicalizer/rules/unsatisfiable_exclusive_equal_bounds.h +++ b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_exclusive_equal_bounds.h @@ -14,6 +14,7 @@ class UnsatisfiableExclusiveEqualBounds final : public SchemaTransformRule { const sourcemeta::core::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4}) && schema.is_object()); diff --git a/vendor/blaze/src/canonicalizer/rules/unsatisfiable_type_and_enum.h b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_type_and_enum.h index 916ef0f52..e3d788677 100644 --- a/vendor/blaze/src/canonicalizer/rules/unsatisfiable_type_and_enum.h +++ b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_type_and_enum.h @@ -18,6 +18,7 @@ class UnsatisfiableTypeAndEnum final : public SchemaTransformRule { SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_1, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_2, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3, + SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_3_HYPER, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, @@ -33,19 +34,9 @@ class UnsatisfiableTypeAndEnum final : public SchemaTransformRule { const auto declared_types{parse_schema_type(*type)}; ONLY_CONTINUE_IF(declared_types.any()); - const bool integer_matches_integral{ - vocabularies.contains_any( - {SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_6, - SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_7, - SchemaVocabularies::Known::JSON_SCHEMA_2019_09_VALIDATION, - SchemaVocabularies::Known::JSON_SCHEMA_2020_12_VALIDATION}) && - declared_types.test( - std::to_underlying(sourcemeta::core::JSON::Type::Integer))}; ONLY_CONTINUE_IF(std::ranges::none_of( - enum_value->as_array(), - [&declared_types, integer_matches_integral](const auto &value) -> auto { - return declared_types.test(std::to_underlying(value.type())) || - (integer_matches_integral && value.is_integral()); + enum_value->as_array(), [&declared_types](const auto &value) -> auto { + return type_can_equal(declared_types, value); })); this->unsatisfiable_ = unsatisfiable_schema(vocabularies); return true; diff --git a/vendor/blaze/src/canonicalizer/schema_helpers.h b/vendor/blaze/src/canonicalizer/schema_helpers.h index e94125166..dab11fa4d 100644 --- a/vendor/blaze/src/canonicalizer/schema_helpers.h +++ b/vendor/blaze/src/canonicalizer/schema_helpers.h @@ -30,6 +30,7 @@ inline auto parse_schema_type_string(const sourcemeta::core::JSON::String &type, } else if (type == "number") { result.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); result.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); + result.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); } else if (type == "integer") { result.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); } else if (type == "string") { diff --git a/vendor/blaze/src/compiler/CMakeLists.txt b/vendor/blaze/src/compiler/CMakeLists.txt index 987106efe..3167c2ac0 100644 --- a/vendor/blaze/src/compiler/CMakeLists.txt +++ b/vendor/blaze/src/compiler/CMakeLists.txt @@ -1,10 +1,11 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME compiler FOLDER "Blaze/Compiler" - PRIVATE_HEADERS error.h unevaluated.h + PRIVATE_HEADERS error.h SOURCES compile.cc compile_json.cc compile_helpers.h keyword_iterator.h postprocess.h - default_compiler.cc unevaluated.cc + unevaluated.h + default_compiler.cc default_compiler_2020_12.h default_compiler_2019_09.h default_compiler_draft7.h diff --git a/vendor/blaze/src/compiler/compile.cc b/vendor/blaze/src/compiler/compile.cc index fabb4bf77..ca9aa0dee 100644 --- a/vendor/blaze/src/compiler/compile.cc +++ b/vendor/blaze/src/compiler/compile.cc @@ -18,6 +18,7 @@ #include "compile_helpers.h" #include "keyword_iterator.h" #include "postprocess.h" +#include "unevaluated.h" namespace { @@ -31,17 +32,27 @@ auto is_metaschema_reference(const sourcemeta::core::WeakPointer &origin) // Draft 6 introduced boolean schemas. Draft 4 and earlier have none, and the // only places they accept a boolean are `additionalProperties` and -// `additionalItems`, whose own definitions spell that out +// `additionalItems`, whose own definitions spell that out. The OpenAPI 3.0 +// Schema Object has none either, and offers a boolean at +// `additionalProperties` alone auto booleans_are_schemas( const sourcemeta::core::SchemaVocabularies &vocabularies) -> bool { using Known = sourcemeta::core::SchemaVocabularies::Known; return !vocabularies.contains_any( {Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, - Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER}); + Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER, + Known::OPENAPI_3_0_BASE}); } +// Draft 7 and earlier let `$ref` override whatever sits beside it, so such a +// sibling is not a keyword of the schema at all and nothing about it, not even +// its shape, is this compiler's business // Draft 4 and earlier spell these as flags on a sibling bound rather than as -// bounds of their own, and their meta-schemas ask for that sibling to be there +// bounds of their own, and their meta-schemas ask for that sibling to be +// there. The OpenAPI 3.0 Schema Object is deliberately absent: it spells them +// as flags the way Draft 4 does, but its meta-schema declares no dependency +// between a flag and the bound it modifies, so one standing on its own is +// well-formed there and merely has nothing to modify auto exclusive_bounds_need_a_sibling( const sourcemeta::core::SchemaVocabularies &vocabularies) -> bool { using Known = sourcemeta::core::SchemaVocabularies::Known; @@ -57,7 +68,17 @@ auto is_schema(const sourcemeta::core::JSON &value, const bool allow_boolean) // The reason the keyword's value is not the shape the meta-schema asks for, or // `nullptr` when it is. The location the error carries names the keyword, so -// these read as statements about it +// these read as statements about it. +// +// What a dialect's meta-schema states is what settles this, rather than what +// its prose asks for, so that a schema the meta-schema accepts is one that +// compiles. A dialect whose prose carries a rule its meta-schema leaves +// unencoded therefore goes unenforced here: OpenAPI 3.0 asks that `items` be +// present whenever `type` is `"array"`, and that a Discriminator Object stand +// beside a composite keyword, while its meta-schema expresses neither, and it +// omits the lower bound that would keep `allOf`, `anyOf` and `oneOf` from +// standing empty. Catching those belongs to a linter, which is free to be +// stricter than the document the dialect publishes auto keyword_shape_error( const sourcemeta::core::JSON::String &keyword, const sourcemeta::core::SchemaKeywordType type, const bool known, @@ -72,6 +93,11 @@ auto keyword_shape_error( "This keyword was expected to be set to a boolean"}; static constexpr auto EXPECTED_NUMBER{ "This keyword was expected to be set to a number"}; + static constexpr auto EXPECTED_OBJECT{ + "This keyword was expected to be set to an object"}; + static constexpr auto EXPECTED_DIALECT_OBJECT{ + "This keyword was expected to be set to an object of the shape this " + "dialect defines"}; static constexpr auto EXPECTED_NON_NEGATIVE_INTEGER{ "This keyword was expected to be set to a non-negative integer"}; static constexpr auto EXPECTED_SCHEMA{ @@ -96,12 +122,86 @@ auto keyword_shape_error( return value.is_string() ? nullptr : EXPECTED_STRING; } if (keyword == "uniqueItems" || keyword == "deprecated" || - keyword == "readOnly" || keyword == "writeOnly") { + keyword == "readOnly" || keyword == "writeOnly" || + keyword == "nullable") { return value.is_boolean() ? nullptr : EXPECTED_BOOLEAN; } if (keyword == "examples") { return value.is_array() ? nullptr : EXPECTED_ARRAY; } + // The OpenAPI dialects define these three as objects of their own. What + // follows is what their meta-schemas state, other than the `format` + // annotations two of their members carry, which assert nothing by default + if (keyword == "discriminator" || keyword == "xml" || + keyword == "externalDocs") { + if (!value.is_object()) { + return EXPECTED_OBJECT; + } + + // Each OpenAPI version defines these objects for itself, and the later + // ones carry members this does not know about, so only the dialect this + // was written against is held to the shape below + if (!vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known::OPENAPI_3_0_BASE)) { + return nullptr; + } + + if (keyword == "discriminator") { + const auto *property_name{value.try_at("propertyName")}; + if (property_name == nullptr || !property_name->is_string()) { + return EXPECTED_DIALECT_OBJECT; + } + + const auto *mapping{value.try_at("mapping")}; + if (mapping != nullptr && + (!mapping->is_object() || + std::ranges::any_of(mapping->as_object(), + [](const auto &entry) -> bool { + return !entry.second.is_string(); + }))) { + return EXPECTED_DIALECT_OBJECT; + } + + // Unlike the other two, this one its meta-schema leaves open + return nullptr; + } + + if (keyword == "externalDocs") { + const auto *url{value.try_at("url")}; + if (url == nullptr || !url->is_string()) { + return EXPECTED_DIALECT_OBJECT; + } + } + + for (const auto &entry : value.as_object()) { + if (entry.first.starts_with("x-")) { + continue; + } + + const auto expected_string{ + keyword == "externalDocs" + ? (entry.first == "description" || entry.first == "url") + : (entry.first == "name" || entry.first == "namespace" || + entry.first == "prefix")}; + const auto expected_boolean{ + keyword == "xml" && + (entry.first == "attribute" || entry.first == "wrapped")}; + if (expected_string) { + if (!entry.second.is_string()) { + return EXPECTED_DIALECT_OBJECT; + } + } else if (expected_boolean) { + if (!entry.second.is_boolean()) { + return EXPECTED_DIALECT_OBJECT; + } + } else { + // Both of their meta-schemas close them to the members they name + return EXPECTED_DIALECT_OBJECT; + } + } + + return nullptr; + } if (keyword == "maxContains" || keyword == "minContains") { // These only exist from 2019-09 onwards, where a number whose fractional // part is zero counts as an integer @@ -117,7 +217,8 @@ auto keyword_shape_error( JSON_SCHEMA_DRAFT_3_HYPER, sourcemeta::core::SchemaVocabularies::Known::JSON_SCHEMA_DRAFT_4, sourcemeta::core::SchemaVocabularies::Known:: - JSON_SCHEMA_DRAFT_4_HYPER}) + JSON_SCHEMA_DRAFT_4_HYPER, + sourcemeta::core::SchemaVocabularies::Known::OPENAPI_3_0_BASE}) ? (value.is_boolean() ? nullptr : EXPECTED_BOOLEAN) : (value.is_number() ? nullptr : EXPECTED_NUMBER)); } @@ -292,11 +393,16 @@ auto compile_subschema(const sourcemeta::blaze::Context &context, const auto *shape_error{keyword_shape_error( keyword, metadata.type, official, schema_context.schema.at(keyword), allow_boolean, schema_context.vocabularies)}; + static const sourcemeta::core::JSON::String KEYWORD_REF{"$ref"}; + const auto overridden_by_ref{ + keyword != KEYWORD_REF && schema_context.schema.defines(KEYWORD_REF) && + ref_object_ignores_siblings(schema_context.vocabularies)}; + // Draft 3 spells these as flags on a sibling bound rather than as bounds of // their own, and its meta-schema asks for that sibling to be there static const sourcemeta::core::JSON::String KEYWORD_MINIMUM{"minimum"}; static const sourcemeta::core::JSON::String KEYWORD_MAXIMUM{"maximum"}; - if (official && + if (!overridden_by_ref && official && exclusive_bounds_need_a_sibling(schema_context.vocabularies) && ((keyword == "exclusiveMinimum" && !schema_context.schema.defines(KEYWORD_MINIMUM)) || @@ -311,7 +417,7 @@ auto compile_subschema(const sourcemeta::blaze::Context &context, "This keyword was expected to accompany the bound it applies to"); } - if (shape_error != nullptr) [[unlikely]] { + if (!overridden_by_ref && shape_error != nullptr) [[unlikely]] { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location( diff --git a/vendor/blaze/src/compiler/compile_helpers.h b/vendor/blaze/src/compiler/compile_helpers.h index dcc615053..ff9eb4ed7 100644 --- a/vendor/blaze/src/compiler/compile_helpers.h +++ b/vendor/blaze/src/compiler/compile_helpers.h @@ -9,6 +9,7 @@ #include // assert #include // std::cref #include // std::distance +#include // std::numeric_limits #include // std::optional #include // std::regex, std::regex_match, std::smatch #include // std::out_of_range @@ -254,6 +255,28 @@ inline auto is_numeric_integer_type_check(const Instruction &instruction) sourcemeta::core::JSON::Type::Integer; } +// A multiple of one admits exactly the integers, so it bounds a number the way +// an integer type check does, without saying anything about the type +inline auto is_numeric_integral_multiple_check(const Instruction &instruction) + -> bool { + if (instruction.type != InstructionIndex::AssertionDivisible) { + return false; + } + + const auto &divisor{std::get(instruction.value)}; + if (!divisor.is_integral()) { + return false; + } + + // A real or decimal may spell an integer too large to represent, and such a + // divisor is never one, so a failed conversion just means no fusion + try { + return divisor.as_integer() == 1; + } catch (const std::out_of_range &) { + return false; + } +} + inline auto is_numeric_bound_check(const Instruction &instruction) -> bool { return instruction.type == InstructionIndex::AssertionGreaterEqual || instruction.type == InstructionIndex::AssertionGreater || @@ -277,12 +300,22 @@ inline auto merge_integer_bound(const Instruction &instruction, if (instruction.type == InstructionIndex::AssertionGreaterEqual) { minimum = minimum.has_value() ? std::max(minimum.value(), value) : value; } else if (instruction.type == InstructionIndex::AssertionGreater) { + // Tightening the bound past the representable range would wrap around, so + // leave such a bound to the standalone instruction that can still hold it + if (value == std::numeric_limits::max()) { + return false; + } + const auto adjusted{value + 1}; minimum = minimum.has_value() ? std::max(minimum.value(), adjusted) : adjusted; } else if (instruction.type == InstructionIndex::AssertionLessEqual) { maximum = maximum.has_value() ? std::min(maximum.value(), value) : value; } else if (instruction.type == InstructionIndex::AssertionLess) { + if (value == std::numeric_limits::min()) { + return false; + } + const auto adjusted{value - 1}; maximum = maximum.has_value() ? std::min(maximum.value(), adjusted) : adjusted; @@ -293,6 +326,47 @@ inline auto merge_integer_bound(const Instruction &instruction, return true; } +// Whether the bound fits the integer the instructions carry. One that does not +// sits beyond the size of anything that can be held in memory, so the keyword +// compilers say such a bound outright rather than convert a value that does +// not fit. The comparison reads the number by its value, so it holds whichever +// way the literal came to be stored +inline auto bound_is_representable(const sourcemeta::core::JSON &value) + -> bool { + static const sourcemeta::core::JSON LIMIT{ + std::numeric_limits::max()}; + return value <= LIMIT; +} + +// Draft 3 to 7 let `$ref` replace every sibling keyword, so nothing those +// siblings say ever takes effect, and the OpenAPI 3.0 Schema Object answers +// with Draft 4 here. Core's schema walker frames nothing under one of those +// drafts either, which is what keeps such a sibling from compiling at all +inline auto ref_overrides_sibling_keywords(const SchemaContext &schema_context) + -> bool { + using Known = sourcemeta::core::SchemaVocabularies::Known; + return schema_context.schema.is_object() && + schema_context.schema.defines("$ref") && + schema_context.vocabularies.contains_any( + {Known::JSON_SCHEMA_DRAFT_7, Known::JSON_SCHEMA_DRAFT_7_HYPER, + Known::JSON_SCHEMA_DRAFT_6, Known::JSON_SCHEMA_DRAFT_6_HYPER, + Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER, + Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, + Known::OPENAPI_3_0_BASE}); +} + +// The OpenAPI 3.0 Schema Object spells a reference as a Reference Object, whose +// every other member the specification says to ignore. The shape of such a +// member is therefore not this dialect's to judge at all, and its own +// meta-schema agrees: it accepts whatever stands beside a `$ref`. Every other +// dialect here lets `$ref` override its siblings without excusing them from +// being well-formed, which their meta-schemas do go on to check +inline auto ref_object_ignores_siblings( + const sourcemeta::core::SchemaVocabularies &vocabularies) -> bool { + return vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known::OPENAPI_3_0_BASE); +} + // Note that the size keywords that let the type level fuse their bound accept // any integral number, including reals with no fractional part like `2.0`, so // this must recognise those too or the fused bound would be silently dropped @@ -577,7 +651,8 @@ inline auto required_properties(const SchemaContext &schema_context) schema_context.vocabularies.contains( Known::JSON_SCHEMA_2019_09_VALIDATION) || schema_context.vocabularies.contains( - Known::JSON_SCHEMA_2020_12_VALIDATION)}; + Known::JSON_SCHEMA_2020_12_VALIDATION) || + schema_context.vocabularies.contains(Known::OPENAPI_3_0_BASE)}; ValueStringSet result; diff --git a/vendor/blaze/src/compiler/default_compiler.cc b/vendor/blaze/src/compiler/default_compiler.cc index 500327245..92779a8e3 100644 --- a/vendor/blaze/src/compiler/default_compiler.cc +++ b/vendor/blaze/src/compiler/default_compiler.cc @@ -8,16 +8,76 @@ #include "default_compiler_draft7.h" #include "default_compiler_openapi.h" +#include // std::ranges::any_of #include // assert #include // std::string #include // std::unordered_set +namespace { + +// Whether any element of the array is a schema +auto any_element_is_a_schema(const sourcemeta::core::JSON &value, + const bool allow_boolean) -> bool { + return value.is_array() && + std::ranges::any_of(value.as_array(), + [allow_boolean](const auto &element) -> bool { + return is_schema(element, allow_boolean); + }); +} + +// Whether a schema sits where a keyword of this kind holds one, and so would +// have to compile. A single schema is enough to settle it, as several of these +// keywords let a schema stand beside a value that is not one: Draft 3 `type` +// lists type names among its subschemas, and `dependencies` lists property +// names among them, so asking that every member or element be a schema would +// hand a schema to a compiler that cannot be given one +auto holds_schema(const sourcemeta::core::SchemaKeywordType type, + const sourcemeta::core::JSON &value, const bool allow_boolean) + -> bool { + switch (type) { + case sourcemeta::core::SchemaKeywordType::Unknown: + case sourcemeta::core::SchemaKeywordType::Assertion: + case sourcemeta::core::SchemaKeywordType::Annotation: + case sourcemeta::core::SchemaKeywordType::Reference: + case sourcemeta::core::SchemaKeywordType::Other: + case sourcemeta::core::SchemaKeywordType::Comment: + return false; + // The schemas are the members + case sourcemeta::core::SchemaKeywordType::LocationMembers: + case sourcemeta::core::SchemaKeywordType:: + ApplicatorMembersTraversePropertyStatic: + case sourcemeta::core::SchemaKeywordType:: + ApplicatorMembersTraversePropertyRegex: + case sourcemeta::core::SchemaKeywordType::ApplicatorMembersInPlaceSome: + return value.is_object() && + std::ranges::any_of( + value.as_object(), [allow_boolean](const auto &entry) -> bool { + return is_schema(entry.second, allow_boolean); + }); + // The schemas are the elements, and the value itself is never one + case sourcemeta::core::SchemaKeywordType::ApplicatorElementsTraverseItem: + case sourcemeta::core::SchemaKeywordType::ApplicatorElementsInPlace: + case sourcemeta::core::SchemaKeywordType::ApplicatorElementsInPlaceSome: + case sourcemeta::core::SchemaKeywordType:: + ApplicatorElementsInPlaceSomeNegate: + return any_element_is_a_schema(value, allow_boolean); + // Either the value is the schema, or its elements are + case sourcemeta::core::SchemaKeywordType:: + ApplicatorValueOrElementsTraverseAnyItemOrItem: + case sourcemeta::core::SchemaKeywordType::ApplicatorValueOrElementsInPlace: + return is_schema(value, allow_boolean) || + any_element_is_a_schema(value, allow_boolean); + // The value is the schema + default: + return is_schema(value, allow_boolean); + } +} + // NOLINTNEXTLINE(google-readability-function-size,hicpp-function-size,readability-function-size) -auto sourcemeta::blaze::default_schema_compiler( - const sourcemeta::blaze::Context &context, - const sourcemeta::blaze::SchemaContext &schema_context, - const sourcemeta::blaze::DynamicContext &dynamic_context, - const sourcemeta::blaze::Instructions ¤t) +auto dispatch_keyword(const sourcemeta::blaze::Context &context, + const sourcemeta::blaze::SchemaContext &schema_context, + const sourcemeta::blaze::DynamicContext &dynamic_context, + const sourcemeta::blaze::Instructions ¤t) -> sourcemeta::blaze::Instructions { assert(!dynamic_context.keyword.empty()); @@ -46,6 +106,7 @@ auto sourcemeta::blaze::default_schema_compiler( Known::JSON_SCHEMA_DRAFT_4_HYPER, Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, + Known::OPENAPI_3_0_BASE, Known::OPENAPI_3_1_BASE, Known::OPENAPI_3_2_BASE, Known::SOURCEMETA_EXTENSION_V1}; @@ -70,13 +131,6 @@ auto sourcemeta::blaze::default_schema_compiler( current); \ } -#define STOP_IF_SIBLING_KEYWORD(vocabulary, _keyword) \ - if (schema_context.vocabularies.contains(vocabulary) && \ - schema_context.schema.is_object() && \ - schema_context.schema.defines(_keyword)) { \ - return {}; \ - } - // ******************************************** // 2020-12 // ******************************************** @@ -293,8 +347,6 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_7, Known::JSON_SCHEMA_DRAFT_7_HYPER, "$ref", compiler_draft3_core_ref); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_7, "$ref"); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_7_HYPER, "$ref"); // Any COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_7, Known::JSON_SCHEMA_DRAFT_7_HYPER, @@ -382,8 +434,6 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_6, Known::JSON_SCHEMA_DRAFT_6_HYPER, "$ref", compiler_draft3_core_ref); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_6, "$ref"); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_6_HYPER, "$ref"); // Any COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_6, Known::JSON_SCHEMA_DRAFT_6_HYPER, @@ -468,8 +518,6 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER, "$ref", compiler_draft3_core_ref); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_4, "$ref"); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_4_HYPER, "$ref"); // Applicators COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER, @@ -541,8 +589,6 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, "$ref", compiler_draft3_core_ref); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_3, "$ref"); - STOP_IF_SIBLING_KEYWORD(Known::JSON_SCHEMA_DRAFT_3_HYPER, "$ref"); // Applicators COMPILE_ANY(Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, @@ -609,9 +655,101 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::OPENAPI_3_1_BASE, Known::OPENAPI_3_2_BASE, "example", compiler_openapi_noop); + // ******************************************** + // OPENAPI 3.0 + // ******************************************** + + // Every arm below guards on this vocabulary alone, never sharing a + // `COMPILE_ANY` with a JSON Schema one. The OpenAPI 3.0 Schema Object is a + // closed dialect whose keyword set is neither a subset nor a superset of + // Draft 4, so a shared arm would hand it a keyword it must not have. The + // rejection at the end of this function relies on this table naming every + // keyword of the dialect and nothing else + + COMPILE(Known::OPENAPI_3_0_BASE, "$ref", compiler_draft3_core_ref); + + // Applicators + COMPILE(Known::OPENAPI_3_0_BASE, "allOf", compiler_draft4_applicator_allof); + COMPILE(Known::OPENAPI_3_0_BASE, "anyOf", compiler_draft4_applicator_anyof); + COMPILE(Known::OPENAPI_3_0_BASE, "oneOf", compiler_draft4_applicator_oneof); + COMPILE(Known::OPENAPI_3_0_BASE, "not", compiler_draft4_applicator_not); + COMPILE(Known::OPENAPI_3_0_BASE, "properties", + compiler_draft3_applicator_properties); + COMPILE(Known::OPENAPI_3_0_BASE, "additionalProperties", + compiler_draft3_applicator_additionalproperties); + COMPILE(Known::OPENAPI_3_0_BASE, "items", compiler_draft3_applicator_items); + + // Any + COMPILE(Known::OPENAPI_3_0_BASE, "type", compiler_draft3_validation_type); + COMPILE(Known::OPENAPI_3_0_BASE, "enum", compiler_draft3_validation_enum); + + // Object + COMPILE(Known::OPENAPI_3_0_BASE, "required", + compiler_draft4_validation_required); + COMPILE(Known::OPENAPI_3_0_BASE, "maxProperties", + compiler_draft4_validation_maxproperties); + COMPILE(Known::OPENAPI_3_0_BASE, "minProperties", + compiler_draft4_validation_minproperties); + + // Array + COMPILE(Known::OPENAPI_3_0_BASE, "uniqueItems", + compiler_draft3_validation_uniqueitems); + COMPILE(Known::OPENAPI_3_0_BASE, "maxItems", + compiler_draft3_validation_maxitems); + COMPILE(Known::OPENAPI_3_0_BASE, "minItems", + compiler_draft3_validation_minitems); + + // String + COMPILE(Known::OPENAPI_3_0_BASE, "pattern", + compiler_draft3_validation_pattern); + COMPILE(Known::OPENAPI_3_0_BASE, "maxLength", + compiler_draft3_validation_maxlength); + COMPILE(Known::OPENAPI_3_0_BASE, "minLength", + compiler_draft3_validation_minlength); + COMPILE(Known::OPENAPI_3_0_BASE, "format", compiler_draft3_validation_format); + + // Number + COMPILE(Known::OPENAPI_3_0_BASE, "maximum", + compiler_draft3_validation_maximum); + COMPILE(Known::OPENAPI_3_0_BASE, "minimum", + compiler_draft3_validation_minimum); + COMPILE(Known::OPENAPI_3_0_BASE, "multipleOf", + compiler_draft3_validation_divisibleby); + + // `maximum` and `minimum` read these as siblings, as they do on the Draft 4 + // rung, so the keywords themselves emit nothing. They need an arm all the + // same, or the rejection below would refuse them + COMPILE(Known::OPENAPI_3_0_BASE, "exclusiveMaximum", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "exclusiveMinimum", compiler_openapi_noop); + + // `type` reads `nullable` as a sibling, and the rest assert nothing at all + COMPILE(Known::OPENAPI_3_0_BASE, "nullable", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "discriminator", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "xml", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "externalDocs", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "example", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "readOnly", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "writeOnly", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "deprecated", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "title", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "description", compiler_openapi_noop); + COMPILE(Known::OPENAPI_3_0_BASE, "default", compiler_openapi_noop); + #undef COMPILE #undef COMPILE_ANY -#undef STOP_IF_SIBLING_KEYWORD + + // The OpenAPI 3.0 Schema Object is closed: its meta-schema sets + // `additionalProperties` to `false` and admits no member other than the + // keywords it names and those prefixed with `x-`. A member outside that set + // is no keyword we may ignore, so compilation refuses it rather than passing + // it by, and the table above is therefore a complete enumeration of the + // dialect rather than a convenience + if (schema_context.vocabularies.contains(Known::OPENAPI_3_0_BASE) && + !dynamic_context.keyword.starts_with("x-")) { + throw sourcemeta::blaze::CompilerError( + schema_context.base, absolute_schema_location(context, schema_context), + PROHIBITED_KEYWORD_FOR_DIALECT); + } if ((schema_context.vocabularies.contains(Known::JSON_SCHEMA_2019_09_CORE) || schema_context.vocabularies.contains(Known::JSON_SCHEMA_2020_12_CORE)) && @@ -639,3 +777,45 @@ auto sourcemeta::blaze::default_schema_compiler( return {}; } + +} // namespace + +auto sourcemeta::blaze::default_schema_compiler( + const sourcemeta::blaze::Context &context, + const sourcemeta::blaze::SchemaContext &schema_context, + const sourcemeta::blaze::DynamicContext &dynamic_context, + const sourcemeta::blaze::Instructions ¤t) + -> sourcemeta::blaze::Instructions { + if (ref_overrides_sibling_keywords(schema_context)) { + const auto &metadata{ + context.walker(dynamic_context.keyword, schema_context.vocabularies)}; + if (metadata.type != sourcemeta::core::SchemaKeywordType::Reference) { + // Whether the shape an overridden keyword takes is one its dialect + // allows at all is still for the keyword compiler to judge, so such a + // keyword compiles for that judgement alone and what it makes is thrown + // away. One holding a schema is where that stops, as core's schema + // walker frames nothing under an overridden keyword, and an unframed + // schema is not one this compiler can be handed. The dialect that reads + // a reference as an object of its own stops earlier still, as there the + // sibling is not merely overridden but ignored. + // + // Draft 3 has no boolean schemas, but its own definitions of these two + // keywords accept a boolean in place of one + const auto allow_boolean{ + booleans_are_schemas(schema_context.vocabularies) || + dynamic_context.keyword == "additionalProperties" || + dynamic_context.keyword == "additionalItems"}; + if (!ref_object_ignores_siblings(schema_context.vocabularies) && + !holds_schema(metadata.type, + schema_context.schema.at(dynamic_context.keyword), + allow_boolean)) { + [[maybe_unused]] const auto discarded{dispatch_keyword( + context, schema_context, dynamic_context, current)}; + } + + return {}; + } + } + + return dispatch_keyword(context, schema_context, dynamic_context, current); +} diff --git a/vendor/blaze/src/compiler/default_compiler_2019_09.h b/vendor/blaze/src/compiler/default_compiler_2019_09.h index a466478fa..14cb7e81c 100644 --- a/vendor/blaze/src/compiler/default_compiler_2019_09.h +++ b/vendor/blaze/src/compiler/default_compiler_2019_09.h @@ -3,6 +3,8 @@ #include +#include // std::to_underlying + #include "compile_helpers.h" #include "default_compiler_draft4.h" @@ -149,28 +151,41 @@ auto compiler_2019_09_applicator_contains_with_options( return {}; } + // These bounds accept any integral number, including one spelled with a + // fractional or exponent part like `1.0` or `1e0`, as this dialect reads + // every number with a zero fractional part as an integer. A bound too large + // to hold is left out, as the general check below cannot carry it std::size_t minimum{1}; - if (schema_context.schema.defines("minContains")) { - if (schema_context.schema.at("minContains").is_integer() && - schema_context.schema.at("minContains").is_positive()) { - minimum = static_cast( - schema_context.schema.at("minContains").to_integer()); - } else if (schema_context.schema.at("minContains").is_real() && - schema_context.schema.at("minContains").is_positive()) { + bool minimum_is_unreachable{false}; + if (schema_context.schema.defines("minContains") && + schema_context.schema.at("minContains").is_integral() && + schema_context.schema.at("minContains").is_positive()) { + if (bound_is_representable(schema_context.schema.at("minContains"))) { minimum = static_cast( schema_context.schema.at("minContains").as_integer()); + } else { + minimum_is_unreachable = true; } } std::optional maximum; - if (schema_context.schema.defines("maxContains")) { - if (schema_context.schema.at("maxContains").is_integer() && - schema_context.schema.at("maxContains").is_positive()) { - maximum = schema_context.schema.at("maxContains").to_integer(); - } else if (schema_context.schema.at("maxContains").is_real() && - schema_context.schema.at("maxContains").is_positive()) { - maximum = schema_context.schema.at("maxContains").as_integer(); - } + if (schema_context.schema.defines("maxContains") && + schema_context.schema.at("maxContains").is_integral() && + schema_context.schema.at("maxContains").is_positive() && + bound_is_representable(schema_context.schema.at("maxContains"))) { + maximum = static_cast( + schema_context.schema.at("maxContains").as_integer()); + } + + // A lower bound too large to hold is one no array has enough items to meet, + // so every array fails and non-arrays are left alone, which is what this + // keyword asks for. Carrying such a bound into the general check below would + // instead report a count the schema never stated + if (minimum_is_unreachable) { + ValueTypes types; + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Array)); + return {make(sourcemeta::blaze::InstructionIndex::AssertionNotTypeStrictAny, + context, schema_context, dynamic_context, types)}; } // An unsatisfiable range is deliberately left for the general check below, diff --git a/vendor/blaze/src/compiler/default_compiler_draft3.h b/vendor/blaze/src/compiler/default_compiler_draft3.h index c5e942bc9..2e9a2411a 100644 --- a/vendor/blaze/src/compiler/default_compiler_draft3.h +++ b/vendor/blaze/src/compiler/default_compiler_draft3.h @@ -8,7 +8,9 @@ #include // std::sort, std::ranges::any_of, std::ranges::all_of, std::find_if, std::ranges::none_of #include // assert +#include // std::int32_t, std::int64_t #include // std::initializer_list +#include // std::numeric_limits #include // std::set #include // std::move, std::to_underlying @@ -111,9 +113,83 @@ static constexpr auto EXPECTED_NON_NEGATIVE{ static constexpr auto EXPECTED_TYPE_NAMES{ "This keyword was expected to be set to a known type name, or to a " "non-empty array of unique ones"}; +static constexpr auto UNSUPPORTED_VALUE_FOR_DIALECT{ + "The value this keyword is set to is not supported by this dialect"}; +static constexpr auto PROHIBITED_KEYWORD_FOR_DIALECT{ + "This keyword is prohibited by this dialect"}; +// A subschema that `narrows_type_away` drops is never compiled, so the refusal +// at the end of the dispatch table never reaches its members. Walk them here, +// as a member this dialect does not define is an error rather than one more +// keyword to pass by. The dialect is closed, so the applicators below are the +// complete set of places a further subschema can sit +static inline auto assert_members_are_permitted( + const sourcemeta::core::JSON &schema, + const sourcemeta::blaze::Context &context, + const sourcemeta::blaze::SchemaContext &schema_context, + const std::uint64_t depth = 0) -> void { + if (depth >= context.tweaks.max_depth) [[unlikely]] { + throw sourcemeta::blaze::CompilerDepthLimitError{context.tweaks.max_depth}; + } + + // The OpenAPI 3.0 Schema Object is the only dialect here that treats a + // member it does not define as an error, so it is the only one whose pruned + // subschemas have anything to answer for + if (!schema_context.vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known::OPENAPI_3_0_BASE)) { + return; + } + + if (!schema.is_object()) { + return; + } + + // A Reference Object ignores whatever sits beside `$ref`, so those members + // answer to no keyword set + if (schema.defines("$ref")) { + return; + } + + for (const auto &entry : schema.as_object()) { + if (entry.first.starts_with("x-")) { + continue; + } + + if (context.walker(entry.first, schema_context.vocabularies).type == + sourcemeta::core::SchemaKeywordType::Unknown) { + throw sourcemeta::blaze::CompilerError( + schema_context.base, + absolute_schema_location(context, schema_context), + PROHIBITED_KEYWORD_FOR_DIALECT); + } + + if (entry.first == "not" || entry.first == "items" || + entry.first == "additionalProperties") { + assert_members_are_permitted(entry.second, context, schema_context, + depth + 1); + } else if (entry.first == "allOf" || entry.first == "anyOf" || + entry.first == "oneOf") { + if (entry.second.is_array()) { + for (const auto &element : entry.second.as_array()) { + assert_members_are_permitted(element, context, schema_context, + depth + 1); + } + } + } else if (entry.first == "properties") { + if (entry.second.is_object()) { + for (const auto &property : entry.second.as_object()) { + assert_members_are_permitted(property.second, context, schema_context, + depth + 1); + } + } + } + } +} + static constexpr auto EXPECTED_UNIQUE_VALUES{ "This keyword was expected to be set to a non-empty array of unique " "values"}; +static constexpr auto EXPECTED_NON_EMPTY_VALUES{ + "This keyword was expected to be set to a non-empty array of values"}; static constexpr auto EXPECTED_SCHEMA_OBJECT{ "Every value of this keyword was expected to be a valid schema"}; static constexpr auto EXPECTED_DEPENDENCIES{ @@ -122,24 +198,80 @@ static constexpr auto EXPECTED_DEPENDENCIES{ // Draft 3 and Draft 4 read "integer" as a number written without a fractional // part, so a bound like `2.0` is not one there. Draft 6 onwards widened it to -// any number whose fractional part is zero +// any number whose fractional part is zero. The OpenAPI 3.0 Schema Object +// answers with Draft 4, as its meta-schema is a Draft 4 document. +// +// Note that OpenAPI 3.0.4 says the opposite in prose, that "both `1` and +// `1.0` are equivalent, and are both considered to be integers", reversing +// what 3.0.3 said. We keep the Draft 4 reading for every dated OpenAPI 3.0 +// dialect regardless, because the Schema Object extends the Draft 4 base +// dialect and so is the base dialect's question to answer, not a patch +// release's to redefine auto integral_reals_are_integers( const sourcemeta::core::SchemaVocabularies &vocabularies) -> bool { using Known = sourcemeta::core::SchemaVocabularies::Known; return !vocabularies.contains_any( {Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, - Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER}); + Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER, + Known::OPENAPI_3_0_BASE}); +} + +// A number too large to hold as a 64-bit integer is kept as a decimal, where +// whether it is an integer stays a question about how the literal was written +// rather than about how it came to be stored +auto is_dialect_integer( + const sourcemeta::core::JSON &value, + const sourcemeta::core::SchemaVocabularies &vocabularies) -> bool { + if (!value.is_integral()) { + return false; + } + + return integral_reals_are_integers(vocabularies) || value.is_integer() || + (value.is_decimal() && value.to_decimal().is_integer()); } // Draft 6 introduced boolean schemas. Draft 4 and earlier have none, and the // only places they accept a boolean are `additionalProperties` and -// `additionalItems`, whose own definitions spell that out +// `additionalItems`, whose own definitions spell that out. The OpenAPI 3.0 +// Schema Object has none either, and offers a boolean at +// `additionalProperties` alone auto booleans_are_schemas( const sourcemeta::core::SchemaVocabularies &vocabularies) -> bool { using Known = sourcemeta::core::SchemaVocabularies::Known; return !vocabularies.contains_any( {Known::JSON_SCHEMA_DRAFT_3, Known::JSON_SCHEMA_DRAFT_3_HYPER, - Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER}); + Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER, + Known::OPENAPI_3_0_BASE}); +} + +// OpenAPI 3.0 bans `type: "null"` and offers `nullable` in its place, which +// widens a sibling `type` and nothing else. It takes effect only where `type` +// is present, so on its own it constrains nothing, and it does not relax any +// other keyword, so a null instance still has to satisfy every sibling. +// The vocabulary has to be in play, or a member by that name in a dialect that +// gives it no meaning would be read as author data one moment and as a keyword +// the next +auto is_nullable(const sourcemeta::core::SchemaVocabularies &vocabularies, + const sourcemeta::core::JSON &schema) -> bool { + using Known = sourcemeta::core::SchemaVocabularies::Known; + return vocabularies.contains(Known::OPENAPI_3_0_BASE) && schema.is_object() && + schema.defines("nullable") && schema.at("nullable").is_boolean() && + schema.at("nullable").to_boolean(); +} + +// Whether a sibling `type` names this type and may be taken as a guarantee +// about the instance. A scalar `type` normally is one, which lets the compiler +// specialise on it, either by folding another keyword's bound into the type +// assertion or by emitting an instruction that assumes the instance's shape. +// `nullable` widens it to also admit null, so there it guarantees nothing and +// every such specialisation has to stand down +auto assume_type(const sourcemeta::core::SchemaVocabularies &vocabularies, + const sourcemeta::core::JSON &schema, + const std::string_view name) -> bool { + return schema.is_object() && schema.defines("type") && + schema.at("type").is_string() && + schema.at("type").to_string() == name && + !is_nullable(vocabularies, schema); } auto is_schema(const sourcemeta::core::JSON &value, const bool allow_boolean) @@ -159,11 +291,22 @@ auto all_are_schemas(const sourcemeta::core::JSON &value, // that does not satisfy that is not a constraint at all, so we ignore it // rather than compile part of it or turn it into a failure auto is_schema_array(const sourcemeta::core::JSON &value, - const bool allow_boolean) -> bool { - return value.is_array() && !value.empty() && + const bool allow_boolean, const bool allow_empty = false) + -> bool { + return value.is_array() && (allow_empty || !value.empty()) && all_are_schemas(value, allow_boolean); } +// Draft 4 onwards give `allOf`, `anyOf` and `oneOf` a `minItems` of one, so an +// empty array is malformed there. The OpenAPI 3.0 Schema Object constrains +// only the member type, leaving an empty one well-formed, and an empty +// conjunction or disjunction has a settled meaning of its own +auto in_place_applicators_may_be_empty( + const sourcemeta::core::SchemaVocabularies &vocabularies) -> bool { + using Known = sourcemeta::core::SchemaVocabularies::Known; + return vocabularies.contains(Known::OPENAPI_3_0_BASE); +} + // Draft 3 describes `type` and `disallow` as a type name or an array of unique // entries, each of which is a type name or a subschema auto is_draft3_type_union(const sourcemeta::core::JSON &value) -> bool { @@ -218,6 +361,14 @@ compile_properties(const sourcemeta::blaze::Context &context, const sourcemeta::blaze::DynamicContext &dynamic_context, const sourcemeta::blaze::Instructions &) -> std::vector> { + // The keywords that fuse with `properties` reach it from the side, past the + // point where an overridden keyword is dropped. A `properties` that `$ref` + // overrides enforces nothing, and nothing under it is framed, so there is + // neither anything to compile here nor anything to fuse with + if (ref_overrides_sibling_keywords(schema_context)) { + return {}; + } + std::vector> properties; for (const auto &entry : schema_context.schema.at("properties").as_object()) { @@ -322,10 +473,8 @@ auto compile_required_assertions(const Context &context, return {}; } - const auto assume_object{schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == - "object"}; + const auto assume_object{assume_type(schema_context.vocabularies, + schema_context.schema, "object")}; if (properties_set.empty()) { return {}; @@ -463,7 +612,10 @@ auto properties_as_loop(const Context &context, schema_context.vocabularies.contains( Known::JSON_SCHEMA_2019_09_VALIDATION) || schema_context.vocabularies.contains( - Known::JSON_SCHEMA_2020_12_VALIDATION); + Known::JSON_SCHEMA_2020_12_VALIDATION) || + schema_context.vocabularies.contains(Known::OPENAPI_3_0_BASE); + // `const` is deliberately absent from the OpenAPI 3.0 dialect below, which + // has `enum` but no `const` of its own const auto imports_const = schema_context.vocabularies.contains(Known::JSON_SCHEMA_DRAFT_6) || schema_context.vocabularies.contains(Known::JSON_SCHEMA_DRAFT_7) || @@ -661,6 +813,17 @@ auto extract_integer_bounds(const Instructions &children) return {minimum, maximum}; } +auto integer_lower_bound_extra_index(const Instructions &children) + -> std::size_t { + for (const auto &child : children) { + if (child.type == InstructionIndex::AssertionGreaterEqual) { + return child.extra_index; + } + } + + return 0; +} + auto compiler_draft3_applicator_properties_with_options( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions ¤t, @@ -680,6 +843,11 @@ auto compiler_draft3_applicator_properties_with_options( } if (narrows_type_away(schema_context.schema, {"object"})) { + for (const auto &entry : + schema_context.schema.at(dynamic_context.keyword).as_object()) { + assert_members_are_permitted(entry.second, context, schema_context); + } + return {}; } @@ -739,10 +907,8 @@ auto compiler_draft3_applicator_properties_with_options( ? dynamic_context : relative_dynamic_context()}; - const auto assume_object{schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == - "object"}; + const auto assume_object{assume_type(schema_context.vocabularies, + schema_context.schema, "object")}; ValueStringSet required{required_properties(schema_context)}; @@ -941,10 +1107,13 @@ auto compiler_draft3_applicator_properties_with_options( ? InstructionIndex::AssertionTypeIntegerBoundedStrict : InstructionIndex::AssertionTypeIntegerBounded; auto instance_location = substeps.front().relative_instance_location; + const auto minimum_extra_index{ + integer_lower_bound_extra_index(substeps)}; substeps.clear(); auto fused = make(index, context, schema_context, relative_dynamic_context(), bounds); fused.relative_instance_location = std::move(instance_location); + fused.extra_index = minimum_extra_index; substeps.push_back(std::move(fused)); } else if (is_integer_type_lower_bound_pattern(substeps)) { const auto minimum = extract_integer_lower_bound(substeps); @@ -953,11 +1122,14 @@ auto compiler_draft3_applicator_properties_with_options( ? InstructionIndex::AssertionTypeIntegerLowerBoundStrict : InstructionIndex::AssertionTypeIntegerLowerBound; auto instance_location = substeps.front().relative_instance_location; + const auto minimum_extra_index{ + integer_lower_bound_extra_index(substeps)}; substeps.clear(); auto fused = make(index, context, schema_context, relative_dynamic_context(), ValueIntegerBounds{minimum, 0}); fused.relative_instance_location = std::move(instance_location); + fused.extra_index = minimum_extra_index; substeps.push_back(std::move(fused)); } else if (substeps.size() == 2) { bool has_items_bounded{false}; @@ -1287,10 +1459,8 @@ auto compiler_draft3_applicator_patternproperties( inline auto properties_enforce_closed_object(const Context &context, const SchemaContext &schema_context) -> bool { - const bool assume_object{schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == - "object"}; + const bool assume_object{assume_type(schema_context.vocabularies, + schema_context.schema, "object")}; if (!assume_object || !schema_context.schema.defines("properties") || !schema_context.schema.at("properties").is_object()) { return false; @@ -1334,6 +1504,9 @@ auto compiler_draft3_applicator_additionalproperties_with_options( const DynamicContext &dynamic_context, const bool annotate, const bool track_evaluation) -> Instructions { if (narrows_type_away(schema_context.schema, {"object"})) { + assert_members_are_permitted( + schema_context.schema.at(dynamic_context.keyword), context, + schema_context); return {}; } @@ -1686,6 +1859,9 @@ auto compiler_draft3_applicator_items_with_options( } if (narrows_type_away(schema_context.schema, {"array"})) { + assert_members_are_permitted( + schema_context.schema.at(dynamic_context.keyword), context, + schema_context); return {}; } @@ -1934,6 +2110,15 @@ auto compiler_draft3_validation_enum(const Context &context, EXPECTED_UNIQUE_VALUES); } + // The OpenAPI 3.0 Schema Object asks for at least one value, and is alone in + // permitting duplicates among them + if (schema_context.vocabularies.contains(Known::OPENAPI_3_0_BASE) && + schema_context.schema.at(dynamic_context.keyword).empty()) { + throw sourcemeta::blaze::CompilerError( + schema_context.base, absolute_schema_location(context, schema_context), + EXPECTED_NON_EMPTY_VALUES); + } + if (schema_context.schema.at(dynamic_context.keyword).empty()) { return {make(sourcemeta::blaze::InstructionIndex::AssertionFail, context, schema_context, dynamic_context, ValueNone{})}; @@ -1954,14 +2139,20 @@ auto compiler_draft3_validation_enum(const Context &context, sourcemeta::core::PropertyHashJSON hasher; for (const auto &option : schema_context.schema.at(dynamic_context.keyword).as_array()) { + // Neither 2019-09 onwards nor the OpenAPI 3.0 Schema Object asks for the + // options to be unique, and the comparison below weighs these hashes + // against the deduplicated set of options, so a repeated option must not + // be counted twice + if (!options.insert(option).second) { + continue; + } + if (option.is_string()) { const auto hash{hasher(option.to_string())}; if (hasher.is_perfect(hash)) { perfect_string_hashes.emplace_back(option.to_string(), hash); } } - - options.insert(option); } // Only apply this optimisation on fast validation, as it @@ -2000,9 +2191,8 @@ auto compiler_draft3_validation_maxlength(const Context &context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral() || - (!integral_reals_are_integers(schema_context.vocabularies) && - !schema_context.schema.at(dynamic_context.keyword).is_integer())) { + if (!is_dialect_integer(schema_context.schema.at(dynamic_context.keyword), + schema_context.vocabularies)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_INTEGER); @@ -2039,9 +2229,15 @@ auto compiler_draft3_validation_maxlength(const Context &context, // property name, so there we must still emit them ourselves if (context.mode == Mode::FastValidation && !schema_context.is_property_name && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "string") { + assume_type(schema_context.vocabularies, schema_context.schema, + "string")) { + return {}; + } + + // A bound too large to hold sits beyond the length of anything that can + // exist, so every string meets it and there is nothing left to assert + if (!bound_is_representable( + schema_context.schema.at(dynamic_context.keyword))) { return {}; } @@ -2059,9 +2255,8 @@ auto compiler_draft3_validation_minlength(const Context &context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral() || - (!integral_reals_are_integers(schema_context.vocabularies) && - !schema_context.schema.at(dynamic_context.keyword).is_integer())) { + if (!is_dialect_integer(schema_context.schema.at(dynamic_context.keyword), + schema_context.vocabularies)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_INTEGER); @@ -2077,14 +2272,29 @@ auto compiler_draft3_validation_minlength(const Context &context, return {}; } + // A bound too large to hold is beyond the length of anything that can + // exist, so no string can meet it, and nothing else is affected. A property + // name is always a string, which leaves the type check nothing to spare + if (!bound_is_representable( + schema_context.schema.at(dynamic_context.keyword))) { + if (schema_context.is_property_name) { + return {make(sourcemeta::blaze::InstructionIndex::AssertionFail, context, + schema_context, dynamic_context, ValueNone{})}; + } + + ValueTypes types; + types.set(std::to_underlying(sourcemeta::core::JSON::Type::String)); + return {make(sourcemeta::blaze::InstructionIndex::AssertionNotTypeStrictAny, + context, schema_context, dynamic_context, types)}; + } + // We'll handle it at the type level as an optimization. Note that the type // compiler bails out before it reads these bounds when it is compiling a // property name, so there we must still emit them ourselves if (context.mode == Mode::FastValidation && !schema_context.is_property_name && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "string") { + assume_type(schema_context.vocabularies, schema_context.schema, + "string")) { return {}; } @@ -2103,9 +2313,8 @@ auto compiler_draft3_validation_maxitems(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral() || - (!integral_reals_are_integers(schema_context.vocabularies) && - !schema_context.schema.at(dynamic_context.keyword).is_integer())) { + if (!is_dialect_integer(schema_context.schema.at(dynamic_context.keyword), + schema_context.vocabularies)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_INTEGER); @@ -2123,9 +2332,15 @@ auto compiler_draft3_validation_maxitems(const Context &context, // We'll handle it at the type level as an optimization if (context.mode == Mode::FastValidation && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "array") { + assume_type(schema_context.vocabularies, schema_context.schema, + "array")) { + return {}; + } + + // A bound too large to hold sits beyond the size of anything that can + // exist, so every array meets it and there is nothing left to assert + if (!bound_is_representable( + schema_context.schema.at(dynamic_context.keyword))) { return {}; } @@ -2142,9 +2357,8 @@ auto compiler_draft3_validation_minitems(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral() || - (!integral_reals_are_integers(schema_context.vocabularies) && - !schema_context.schema.at(dynamic_context.keyword).is_integer())) { + if (!is_dialect_integer(schema_context.schema.at(dynamic_context.keyword), + schema_context.vocabularies)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_INTEGER); @@ -2160,11 +2374,25 @@ auto compiler_draft3_validation_minitems(const Context &context, return {}; } + // A bound too large to hold is beyond the size of anything that can + // exist, so no array can meet it, and nothing else is affected. A property + // name is always a string, which no array bound applies to + if (!bound_is_representable( + schema_context.schema.at(dynamic_context.keyword))) { + if (schema_context.is_property_name) { + return {}; + } + + ValueTypes types; + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Array)); + return {make(sourcemeta::blaze::InstructionIndex::AssertionNotTypeStrictAny, + context, schema_context, dynamic_context, types)}; + } + // We'll handle it at the type level as an optimization if (context.mode == Mode::FastValidation && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "array") { + assume_type(schema_context.vocabularies, schema_context.schema, + "array")) { return {}; } @@ -2243,6 +2471,33 @@ auto compiler_draft3_validation_minimum(const Context &context, schema_context.schema.at(dynamic_context.keyword)})}; } +// Whether the name was one this function knows, so that callers can tell an +// unrecognised name apart from one that set no bit +auto assign_type_bit(sourcemeta::blaze::ValueTypes &types, + const std::string_view name) -> bool { + if (name == "null") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Null)); + } else if (name == "boolean") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Boolean)); + } else if (name == "object") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Object)); + } else if (name == "array") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Array)); + } else if (name == "number") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); + } else if (name == "integer") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); + } else if (name == "string") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::String)); + } else { + return false; + } + + return true; +} + auto compiler_draft3_validation_type(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, @@ -2254,6 +2509,32 @@ auto compiler_draft3_validation_type(const Context &context, schema_context.vocabularies.contains(Known::JSON_SCHEMA_DRAFT_3) || schema_context.vocabularies.contains(Known::JSON_SCHEMA_DRAFT_3_HYPER)}; + // This dialect types `type` as a string drawn from six names. It has no + // array form and no null type, offering `nullable` in place of the latter. + // `assign_type_bit` answers for the seven JSON Schema names, so the null one + // has to be ruled out separately + if (schema_context.vocabularies.contains(Known::OPENAPI_3_0_BASE)) { + ValueTypes recognised{}; + if (!value.is_string() || value.to_string() == "null" || + !assign_type_bit(recognised, value.to_string())) { + throw sourcemeta::blaze::CompilerError( + schema_context.base, + absolute_schema_location(context, schema_context), + UNSUPPORTED_VALUE_FOR_DIALECT); + } + + // A scalar `type` widened by `nullable` is the array form in all but + // spelling, so it takes the same instruction and skips the scalar fast + // paths below, none of which can express a bound alongside a second + // permitted type + if (is_nullable(schema_context.vocabularies, schema_context.schema)) { + ValueTypes types{recognised}; + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Null)); + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrictAny, + context, schema_context, dynamic_context, types)}; + } + } + if (is_draft3) { if (value.is_string() && value.to_string() == "any") { return draft3_any_type_instructions(context, schema_context, @@ -2501,16 +2782,12 @@ auto compiler_draft3_validation_type(const Context &context, context, schema_context, dynamic_context, types)}; } if (type == "integer") { - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("enum") && - schema_context.schema.at("enum").is_array() && - std::ranges::all_of(schema_context.schema.at("enum").as_array(), - [](const auto &candidate) -> auto { - return candidate.is_integer(); - })) { - return {}; - } - + // Draft 4 and earlier have no shortcut for an all-integer `enum` here, + // and neither does the OpenAPI 3.0 Schema Object, unlike every other + // type name. Equality reads a number by its value rather than by how it + // is written, so the options admit an instance carrying a fractional + // part, which none of these dialects counts as an integer, and so this + // assertion always has work left to do return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, context, schema_context, dynamic_context, sourcemeta::core::JSON::Type::Integer)}; @@ -2633,24 +2910,7 @@ auto compiler_draft3_validation_type(const Context &context, continue; } - const auto &type_string{element.to_string()}; - if (type_string == "null") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Null)); - } else if (type_string == "boolean") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Boolean)); - } else if (type_string == "object") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Object)); - } else if (type_string == "array") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Array)); - } else if (type_string == "number") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); - } else if (type_string == "integer") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); - } else if (type_string == "string") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::String)); - } else if (!is_draft3) { + if (!assign_type_bit(types, element.to_string()) && !is_draft3) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), @@ -2964,6 +3224,22 @@ auto compiler_draft3_validation_divisibleby( schema_context.schema.at(dynamic_context.keyword)})}; } +// The formats that the OpenAPI 3.1 and 3.2 specifications define for numbers, +// which the rest of this compiler treats apart from the string ones +auto is_openapi_numeric_format(const SchemaContext &schema_context, + const sourcemeta::core::JSON &format) -> bool { + using Known = sourcemeta::core::SchemaVocabularies::Known; + if (!format.is_string() || + (!schema_context.vocabularies.contains(Known::OPENAPI_3_1_BASE) && + !schema_context.vocabularies.contains(Known::OPENAPI_3_2_BASE))) { + return false; + } + + const auto &name{format.to_string()}; + return name == "int32" || name == "int64" || name == "float" || + name == "double"; +} + auto compiler_draft3_validation_format(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, @@ -2986,7 +3262,6 @@ auto compiler_draft3_validation_format(const Context &context, schema_context.schema.at("x-format-assertion").to_boolean()}; const auto assert_active{context.tweaks.format_assertion || force_assertion}; - if ((is_2019_09_format && assert_active) || is_2020_12_format_assertion || (is_2020_12_format_annotation && assert_active)) { const auto &format{schema_context.schema.at(dynamic_context.keyword)}; @@ -2995,6 +3270,47 @@ auto compiler_draft3_validation_format(const Context &context, } const auto &name{format.to_string()}; + + // These are the only formats the OpenAPI 3.1 and 3.2 specifications + // define themselves, and supporting any other entry of the Format + // Registry they defer to is strictly optional + // TODO: Consider asserting the numeric and string formats that the + // OpenAPI Format Registry defines + if (is_openapi_numeric_format(schema_context, format)) { + Instructions instructions; + if (name == "int32") { + instructions.push_back(make( + sourcemeta::blaze::InstructionIndex::AssertionNumberIntegerBounded, + context, schema_context, dynamic_context, + ValueIntegerBounds{std::numeric_limits::min(), + std::numeric_limits::max()})); + } else if (name == "int64") { + instructions.push_back(make( + sourcemeta::blaze::InstructionIndex::AssertionNumberIntegerBounded, + context, schema_context, dynamic_context, + ValueIntegerBounds{std::numeric_limits::min(), + std::numeric_limits::max()})); + } else { + instructions.push_back( + make(sourcemeta::blaze::InstructionIndex::AssertionNumberType, + context, schema_context, dynamic_context, + name == "float" ? ValueNumberType::Float + : ValueNumberType::Double)); + } + + // Unlike the string formats, the annotation cannot be grouped by the + // instance type, as `ControlGroupWhenType` does not take a number + if (annotations_enabled(context, dynamic_context.keyword)) { + instructions.push_back( + make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, + schema_context, dynamic_context, + sourcemeta::core::JSON{ + schema_context.schema.at(dynamic_context.keyword)})); + } + + return instructions; + } + ValueStringType type; if (name == "date-time") { type = ValueStringType::DateTime; @@ -3068,6 +3384,15 @@ auto compiler_draft3_validation_format(const Context &context, sourcemeta::core::JSON{ schema_context.schema.at(dynamic_context.keyword)})}; + // A format that applies to numbers cannot have its annotation grouped by + // the instance type, so emit it the way the assertion path does rather + // than collect it for strings alone + if (is_openapi_numeric_format( + schema_context, + schema_context.schema.at(dynamic_context.keyword))) { + return children; + } + return {make(sourcemeta::blaze::InstructionIndex::ControlGroupWhenType, context, schema_context, relative_dynamic_context(), ValueType::String, std::move(children))}; @@ -3098,7 +3423,52 @@ auto compiler_draft3_validation_format(const Context &context, schema_context.vocabularies.contains(Known::JSON_SCHEMA_DRAFT_7) || schema_context.vocabularies.contains(Known::JSON_SCHEMA_DRAFT_7_HYPER)}; - if (is_draft7) { + if (schema_context.vocabularies.contains(Known::OPENAPI_3_0_BASE)) { + // OpenAPI 3.0 defines these two for numbers, so they cannot resolve to a + // string logical type like the rest of this chain does + if (name == "int32" || name == "int64") { + return {make( + sourcemeta::blaze::InstructionIndex::AssertionNumberIntegerBounded, + context, schema_context, dynamic_context, + name == "int32" + ? ValueIntegerBounds{std::numeric_limits::min(), + std::numeric_limits::max()} + : ValueIntegerBounds{std::numeric_limits::min(), + std::numeric_limits::max()})}; + } + + if (name == "float" || name == "double") { + return {make(sourcemeta::blaze::InstructionIndex::AssertionNumberType, + context, schema_context, dynamic_context, + name == "float" ? ValueNumberType::Float + : ValueNumberType::Double)}; + } + + if (name == "date-time") { + type = ValueStringType::DateTime; + } else if (name == "date") { + type = ValueStringType::Date; + } else if (name == "byte") { + type = ValueStringType::Base64; + } else if (name == "email") { + // The Schema Object is a subset of JSON Schema Draft Wright-00, whose + // own formats apply alongside the ones the specification tabulates + type = ValueStringType::Email; + } else if (name == "hostname") { + type = ValueStringType::Hostname; + } else if (name == "ipv4") { + type = ValueStringType::IPv4; + } else if (name == "ipv6") { + type = ValueStringType::IPv6; + } else if (name == "uri") { + type = ValueStringType::URI; + } else { + // `binary` stands for unencoded binary data, which a JSON string cannot + // carry, and `password` is a hint to obscure the value, so neither of + // the two remaining formats constrains the instance + return {}; + } + } else if (is_draft7) { if (name == "date-time") { type = ValueStringType::DateTime; } else if (name == "date") { diff --git a/vendor/blaze/src/compiler/default_compiler_draft4.h b/vendor/blaze/src/compiler/default_compiler_draft4.h index c69beae56..879169a12 100644 --- a/vendor/blaze/src/compiler/default_compiler_draft4.h +++ b/vendor/blaze/src/compiler/default_compiler_draft4.h @@ -20,10 +20,12 @@ auto compiler_draft4_validation_required(const Context &context, const DynamicContext &dynamic_context, const Instructions ¤t) -> Instructions { - // Draft 4 alone asks that `required` name at least one property + // Draft 4 asks that `required` name at least one property, and the OpenAPI + // 3.0 Schema Object, whose meta-schema is a Draft 4 document, asks the same using Known = sourcemeta::core::SchemaVocabularies::Known; const auto allows_empty{!schema_context.vocabularies.contains_any( - {Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER})}; + {Known::JSON_SCHEMA_DRAFT_4, Known::JSON_SCHEMA_DRAFT_4_HYPER, + Known::OPENAPI_3_0_BASE})}; if (!is_string_array(schema_context.schema.at(dynamic_context.keyword)) || (!allows_empty && schema_context.schema.at(dynamic_context.keyword).empty())) { @@ -42,8 +44,10 @@ auto compiler_draft4_applicator_allof(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!is_schema_array(schema_context.schema.at(dynamic_context.keyword), - booleans_are_schemas(schema_context.vocabularies))) { + if (!is_schema_array( + schema_context.schema.at(dynamic_context.keyword), + booleans_are_schemas(schema_context.vocabularies), + in_place_applicators_may_be_empty(schema_context.vocabularies))) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_SCHEMA_ARRAY); @@ -90,13 +94,23 @@ auto compiler_draft4_applicator_anyof(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!is_schema_array(schema_context.schema.at(dynamic_context.keyword), - booleans_are_schemas(schema_context.vocabularies))) { + if (!is_schema_array( + schema_context.schema.at(dynamic_context.keyword), + booleans_are_schemas(schema_context.vocabularies), + in_place_applicators_may_be_empty(schema_context.vocabularies))) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_SCHEMA_ARRAY); } + // A disjunction over no branches can never be satisfied. Only a dialect that + // permits the empty array gets here, as the shape check above refuses it + // everywhere else + if (schema_context.schema.at(dynamic_context.keyword).empty()) { + return {make(sourcemeta::blaze::InstructionIndex::AssertionFail, context, + schema_context, dynamic_context, ValueNone{})}; + } + Instructions disjunctors; for (std::uint64_t index = 0; index < schema_context.schema.at(dynamic_context.keyword).size(); @@ -166,13 +180,23 @@ auto compiler_draft4_applicator_oneof(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!is_schema_array(schema_context.schema.at(dynamic_context.keyword), - booleans_are_schemas(schema_context.vocabularies))) { + if (!is_schema_array( + schema_context.schema.at(dynamic_context.keyword), + booleans_are_schemas(schema_context.vocabularies), + in_place_applicators_may_be_empty(schema_context.vocabularies))) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_SCHEMA_ARRAY); } + // A disjunction over no branches can never be satisfied. Only a dialect that + // permits the empty array gets here, as the shape check above refuses it + // everywhere else + if (schema_context.schema.at(dynamic_context.keyword).empty()) { + return {make(sourcemeta::blaze::InstructionIndex::AssertionFail, context, + schema_context, dynamic_context, ValueNone{})}; + } + Instructions disjunctors; for (std::uint64_t index = 0; index < schema_context.schema.at(dynamic_context.keyword).size(); @@ -231,9 +255,8 @@ auto compiler_draft4_validation_maxproperties( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral() || - (!integral_reals_are_integers(schema_context.vocabularies) && - !schema_context.schema.at(dynamic_context.keyword).is_integer())) { + if (!is_dialect_integer(schema_context.schema.at(dynamic_context.keyword), + schema_context.vocabularies)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_INTEGER); @@ -253,9 +276,15 @@ auto compiler_draft4_validation_maxproperties( // We'll handle it at the type level as an optimization if (context.mode == Mode::FastValidation && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "object") { + assume_type(schema_context.vocabularies, schema_context.schema, + "object")) { + return {}; + } + + // A bound too large to hold sits beyond the size of anything that can + // exist, so every object meets it and there is nothing left to assert + if (!bound_is_representable( + schema_context.schema.at(dynamic_context.keyword))) { return {}; } @@ -272,9 +301,8 @@ auto compiler_draft4_validation_minproperties( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral() || - (!integral_reals_are_integers(schema_context.vocabularies) && - !schema_context.schema.at(dynamic_context.keyword).is_integer())) { + if (!is_dialect_integer(schema_context.schema.at(dynamic_context.keyword), + schema_context.vocabularies)) { throw sourcemeta::blaze::CompilerError( schema_context.base, absolute_schema_location(context, schema_context), EXPECTED_INTEGER); @@ -292,11 +320,25 @@ auto compiler_draft4_validation_minproperties( return {}; } + // A bound too large to hold is beyond the size of anything that can + // exist, so no object can meet it, and nothing else is affected. A property + // name is always a string, which no object bound applies to + if (!bound_is_representable( + schema_context.schema.at(dynamic_context.keyword))) { + if (schema_context.is_property_name) { + return {}; + } + + ValueTypes types; + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Object)); + return {make(sourcemeta::blaze::InstructionIndex::AssertionNotTypeStrictAny, + context, schema_context, dynamic_context, types)}; + } + // We'll handle it at the type level as an optimization if (context.mode == Mode::FastValidation && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "object") { + assume_type(schema_context.vocabularies, schema_context.schema, + "object")) { return {}; } diff --git a/vendor/blaze/src/compiler/default_compiler_draft6.h b/vendor/blaze/src/compiler/default_compiler_draft6.h index 5b1a1e7d3..d4482eefa 100644 --- a/vendor/blaze/src/compiler/default_compiler_draft6.h +++ b/vendor/blaze/src/compiler/default_compiler_draft6.h @@ -151,20 +151,14 @@ auto compiler_draft6_validation_type(const Context &context, // A preceding array loop at this location already rejects a non-array, // so the type assertion is redundant. The array size bounds are only - // redundant when there are none to enforce, as the loops other than the - // sized one do not check the item count. Only a loop that truly rejects - // a non-array qualifies: some item loops instead pass a non-array - // untouched, so they are deliberately excluded below + // redundant when there are none to enforce, as the loop may be + // carrying bounds fused from a different schema. Only a loop that + // truly rejects a non-array qualifies: every other item loop holds + // vacuously on a non-array, as `items` asserts nothing on one if (context.mode == Mode::FastValidation && minimum == 0 && !maximum.has_value() && !current.empty() && - (current.back().type == - sourcemeta::blaze::InstructionIndex:: - LoopItemsPropertiesExactlyTypeStrictHash || - current.back().type == - sourcemeta::blaze::InstructionIndex:: - LoopItemsPropertiesExactlyTypeStrictHash3 || - current.back().type == sourcemeta::blaze::InstructionIndex:: - LoopItemsIntegerBoundedSized) && + current.back().type == sourcemeta::blaze::InstructionIndex:: + LoopItemsIntegerBoundedSized && current.back().relative_instance_location == to_pointer(dynamic_context.base_instance_location)) { return {}; diff --git a/vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler.h b/vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler.h index 4dd7271b0..acdc3ea99 100644 --- a/vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler.h +++ b/vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler.h @@ -6,7 +6,6 @@ #endif #include -#include #include @@ -21,6 +20,7 @@ #include // std::numeric_limits #include // std::map #include // std::optional, std::nullopt +#include // std::set #include // std::string #include // std::string_view #include // std::tuple @@ -34,6 +34,23 @@ namespace sourcemeta::blaze { +/// @ingroup compiler +struct SchemaUnevaluatedEntry { + /// The absolute pointers of the static keyword dependencies + std::set static_dependencies; + /// The absolute pointers of the dynamic keyword dependencies, along with the + /// references that lead to them + std::set dynamic_dependencies; + /// Whether the entry cannot be fully resolved, which means + /// there might be unknown dynamic dependencies + bool unresolved{false}; +}; + +/// @ingroup compiler +/// The flattened set of unevaluated cases in the schema by absolute URI +using SchemaUnevaluatedEntries = + std::map; + /// @ingroup compiler /// The schema compiler context is the current subschema information you have at /// your disposal to implement a keyword diff --git a/vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler_unevaluated.h b/vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler_unevaluated.h deleted file mode 100644 index daab62965..000000000 --- a/vendor/blaze/src/compiler/include/sourcemeta/blaze/compiler_unevaluated.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef SOURCEMETA_BLAZE_COMPILER_UNEVALUATED_H -#define SOURCEMETA_BLAZE_COMPILER_UNEVALUATED_H - -#ifndef SOURCEMETA_BLAZE_COMPILER_EXPORT -#include -#endif - -#include -#include -#include - -#include // std::map -#include // std::set - -// TODO: Eventually this file should dissapear and move this analysis as part of -// framing - -namespace sourcemeta::blaze { - -/// @ingroup compiler -struct SchemaUnevaluatedEntry { - /// The absolute pointers of the static keyword dependencies - std::set static_dependencies; - /// The absolute pointers of the dynamic keyword dependencies, along with the - /// references that lead to them - std::set dynamic_dependencies; - /// Whether the entry cannot be fully resolved, which means - /// there might be unknown dynamic dependencies - bool unresolved{false}; -}; - -/// @ingroup compiler -/// The flattened set of unevaluated cases in the schema by absolute URI -using SchemaUnevaluatedEntries = - std::map; - -/// @ingroup compiler -/// -/// This function performs a static analysis pass on `unevaluatedProperties` and -/// `unevaluatedItems` occurences throughout the entire schema (if any). -auto SOURCEMETA_BLAZE_COMPILER_EXPORT -unevaluated(const sourcemeta::core::JSON &schema, - const sourcemeta::core::SchemaFrame &frame, - const sourcemeta::core::SchemaWalker &walker, - const sourcemeta::core::SchemaResolver &resolver) - -> SchemaUnevaluatedEntries; - -} // namespace sourcemeta::blaze - -#endif diff --git a/vendor/blaze/src/compiler/postprocess.h b/vendor/blaze/src/compiler/postprocess.h index b5a1e0188..58b8cab0c 100644 --- a/vendor/blaze/src/compiler/postprocess.h +++ b/vendor/blaze/src/compiler/postprocess.h @@ -207,7 +207,9 @@ inline auto fuse_numeric_bounds(Instructions &instructions, for (std::size_t type_index = 0; type_index < instructions.size(); ++type_index) { const auto &type_instruction{instructions[type_index]}; - if (!is_numeric_integer_type_check(type_instruction)) { + const auto anchors_on_type{is_numeric_integer_type_check(type_instruction)}; + if (!anchors_on_type && + !is_numeric_integral_multiple_check(type_instruction)) { continue; } @@ -220,6 +222,7 @@ inline auto fuse_numeric_bounds(Instructions &instructions, std::vector bound_indices; std::optional minimum; std::optional maximum; + std::optional minimum_index; for (std::size_t index = 0; index < instructions.size(); ++index) { const auto &candidate{instructions[index]}; @@ -232,10 +235,15 @@ inline auto fuse_numeric_bounds(Instructions &instructions, continue; } + const auto previous_minimum{minimum}; if (!merge_integer_bound(candidate, minimum, maximum)) { continue; } + if (minimum != previous_minimum) { + minimum_index = index; + } + bound_indices.push_back(index); } @@ -249,7 +257,16 @@ inline auto fuse_numeric_bounds(Instructions &instructions, InstructionIndex fused_type; Value fused_value; - if (minimum.has_value() && maximum.has_value()) { + if (!anchors_on_type) { + // Without a type check to absorb there is no lower-bound-only form to + // fall back on, as the instruction has to carry both ends of the range + if (!minimum.has_value() || !maximum.has_value()) { + continue; + } + + fused_type = InstructionIndex::AssertionNumberIntegerBounded; + fused_value = ValueIntegerBounds{minimum.value(), maximum.value()}; + } else if (minimum.has_value() && maximum.has_value()) { fused_type = is_strict ? InstructionIndex::AssertionTypeIntegerBoundedStrict : InstructionIndex::AssertionTypeIntegerBounded; @@ -265,11 +282,14 @@ inline auto fuse_numeric_bounds(Instructions &instructions, const auto first_index{ std::min(type_index, *std::ranges::min_element(bound_indices))}; + const auto &minimum_metadata{ + extra[instructions[minimum_index.value()].extra_index]}; const auto extra_index{extra.size()}; - extra.push_back({.relative_schema_location = parent->first, - .keyword_location = parent->second, - .schema_resource = type_metadata.schema_resource, - .vocabulary = type_metadata.vocabulary}); + extra.push_back( + {.relative_schema_location = minimum_metadata.relative_schema_location, + .keyword_location = minimum_metadata.keyword_location, + .schema_resource = type_metadata.schema_resource, + .vocabulary = type_metadata.vocabulary}); Instructions result; result.reserve(instructions.size() - bound_indices.size()); diff --git a/vendor/blaze/src/compiler/schema_helpers.h b/vendor/blaze/src/compiler/schema_helpers.h index ba69c4fb8..4b3dff553 100644 --- a/vendor/blaze/src/compiler/schema_helpers.h +++ b/vendor/blaze/src/compiler/schema_helpers.h @@ -28,6 +28,7 @@ inline auto parse_schema_type_string(const sourcemeta::core::JSON::String &type, } else if (type == "number") { result.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); result.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); + result.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); } else if (type == "integer") { result.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); } else if (type == "string") { diff --git a/vendor/blaze/src/compiler/unevaluated.cc b/vendor/blaze/src/compiler/unevaluated.cc deleted file mode 100644 index 77c418d1f..000000000 --- a/vendor/blaze/src/compiler/unevaluated.cc +++ /dev/null @@ -1,311 +0,0 @@ -#include -#include - -#include "compile_helpers.h" - -#include // std::set -#include // std::pair - -namespace { -using namespace sourcemeta::core; -using namespace sourcemeta::blaze; -using Known = SchemaVocabularies::Known; - -// NOLINTBEGIN(cert-err58-cpp,bugprone-throwing-static-initialization) -const std::string UNEVALUATED_PROPERTIES{"unevaluatedProperties"}; -const std::string UNEVALUATED_ITEMS{"unevaluatedItems"}; -// NOLINTEND(cert-err58-cpp,bugprone-throwing-static-initialization) - -// Whether a location is already being visited has to survive the nested calls -// that walk through it, and stop applying the moment they unwind -class PathGuard { -public: - PathGuard(std::set> &visited, - const WeakPointer &pointer, const bool is_static) - : visited_{visited}, entry_{pointer, is_static}, - first_visit_{visited.insert(this->entry_).second} {} - - ~PathGuard() { - if (this->first_visit_) { - this->visited_.erase(this->entry_); - } - } - - [[nodiscard]] auto first_visit() const -> bool { return this->first_visit_; } - - PathGuard(const PathGuard &) = delete; - auto operator=(const PathGuard &) -> PathGuard & = delete; - PathGuard(PathGuard &&) = delete; - auto operator=(PathGuard &&) -> PathGuard & = delete; - -private: - std::set> &visited_; - const std::pair entry_; - const bool first_visit_; -}; - -auto find_adjacent_dependencies( - const JSON::String ¤t, const JSON &schema, const SchemaFrame &frame, - const SchemaWalker &walker, const SchemaResolver &resolver, - const std::set &keywords, const SchemaFrame::Location &root, - const SchemaFrame::Location &entry, const bool is_static, - std::set> &visited, - sourcemeta::blaze::SchemaUnevaluatedEntry &result) -> void { - // A schema may reference itself, directly or through a chain of in-place - // applicators. Following such a cycle forever exhausts the stack, so we cut - // it as soon as we meet a location already on the path we came in through. - // The mark only lives as long as that path, as two references to a common - // target are not a cycle, and each of them still has to be accounted for - const PathGuard guard{visited, entry.pointer, is_static}; - if (!guard.first_visit()) { - return; - } - - const auto &subschema{get(schema, entry.pointer)}; - if (!subschema.is_object()) { - return; - } - - const auto &subschema_vocabularies{frame.vocabularies(entry, resolver)}; - - for (const auto &property : subschema.as_object()) { - if (property.first == current && entry.pointer == root.pointer) { - continue; - } - if (keywords.contains(property.first)) { - // In 2019-09, `additionalItems` takes no effect without `items` - if (subschema_vocabularies.contains( - Known::JSON_SCHEMA_2019_09_APPLICATOR) && - property.first == "additionalItems" && !subschema.defines("items")) { - continue; - } - - auto pointer{entry.pointer.concat(make_weak_pointer(property.first))}; - if (is_static) { - result.static_dependencies.emplace(std::move(pointer)); - } else { - result.dynamic_dependencies.emplace(std::move(pointer)); - } - - continue; - } - - switch (walker(property.first, subschema_vocabularies).type) { - // References - case SchemaKeywordType::Reference: { - const auto reference{ - frame.dereference(entry, make_weak_pointer(property.first))}; - if (reference.first == SchemaReferenceType::Static && - reference.second.has_value()) { - // Recurse into a dedicated entry so that whether this reference's - // target contributes any dynamic dependency can be read directly, - // rather than inferred from whether it grew the shared deduplicated - // set, which misses every reference after the first to a common - // target - SchemaUnevaluatedEntry nested; - find_adjacent_dependencies( - current, schema, frame, walker, resolver, keywords, root, - reference.second.value().get(), is_static, visited, nested); - - // Whatever the target contributes gets recorded at the location of - // the target itself, which tells the applicators this reference sits - // under nothing about it. Record the reference as a dependency of - // its own too, so that they can still tell that reaching through it - // leads to one, and therefore that they cannot short-circuit. Only - // the dynamic dependencies are consulted that way, whereas the - // static ones name the keyword locations that evaluate, which a - // reference is not one of - if (!is_static && !nested.dynamic_dependencies.empty()) { - result.dynamic_dependencies.emplace( - entry.pointer.concat(make_weak_pointer(property.first))); - } - - result.unresolved = result.unresolved || nested.unresolved; - result.static_dependencies.merge(nested.static_dependencies); - result.dynamic_dependencies.merge(nested.dynamic_dependencies); - } else if (reference.first == SchemaReferenceType::Dynamic) { - result.unresolved = true; - } - - break; - } - - // Static - case SchemaKeywordType::ApplicatorElementsInPlace: - // TODO(C++23): Use std::views::enumerate when available in libc++ - for (std::size_t index = 0; index < property.second.size(); index++) { - find_adjacent_dependencies( - current, schema, frame, walker, resolver, keywords, root, - frame.traverse(entry, make_weak_pointer(property.first, index)), - is_static, visited, result); - } - - break; - - // Dynamic - case SchemaKeywordType::ApplicatorElementsInPlaceSome: - if (property.second.is_array()) { - for (std::size_t index = 0; index < property.second.size(); index++) { - find_adjacent_dependencies( - current, schema, frame, walker, resolver, keywords, root, - frame.traverse(entry, make_weak_pointer(property.first, index)), - false, visited, result); - } - } - - break; - case SchemaKeywordType::ApplicatorValueTraverseAnyItem: - [[fallthrough]]; - case SchemaKeywordType::ApplicatorValueTraverseParent: - [[fallthrough]]; - case SchemaKeywordType::ApplicatorValueInPlaceMaybe: - if ((property.second.is_object() || property.second.is_boolean())) { - find_adjacent_dependencies( - current, schema, frame, walker, resolver, keywords, root, - frame.traverse(entry, make_weak_pointer(property.first)), false, - visited, result); - } - - break; - case SchemaKeywordType::ApplicatorValueOrElementsInPlace: - if (property.second.is_array()) { - for (std::size_t index = 0; index < property.second.size(); index++) { - find_adjacent_dependencies( - current, schema, frame, walker, resolver, keywords, root, - frame.traverse(entry, make_weak_pointer(property.first, index)), - false, visited, result); - } - } else if ((property.second.is_object() || - property.second.is_boolean())) { - find_adjacent_dependencies( - current, schema, frame, walker, resolver, keywords, root, - frame.traverse(entry, make_weak_pointer(property.first)), false, - visited, result); - } - - break; - case SchemaKeywordType::ApplicatorMembersInPlaceSome: - if (property.second.is_object()) { - for (const auto &pair : property.second.as_object()) { - find_adjacent_dependencies( - current, schema, frame, walker, resolver, keywords, root, - frame.traverse(entry, - make_weak_pointer(property.first, pair.first)), - false, visited, result); - } - } - - break; - - // Anything else does not contribute to the dependency list - default: - break; - } - } -} - -auto register_under_all_bases(SchemaUnevaluatedEntries &result, - const SchemaFrame &frame, - const SchemaFrame::Location &location, - const JSON::String &keyword, - const SchemaUnevaluatedEntry &value) -> void { - result.emplace(frame.uri(location, make_weak_pointer(keyword)), value); - frame.for_each_location([&](const SchemaReferenceType, const std::string_view, - const SchemaFrame::Location &alternate) -> void { - if (alternate.pointer != location.pointer || - alternate.base == location.base) { - return; - } - - if (alternate.type != SchemaFrame::LocationType::Subschema && - alternate.type != SchemaFrame::LocationType::Resource && - alternate.type != SchemaFrame::LocationType::Anchor) { - return; - } - - result.emplace(frame.uri(alternate, make_weak_pointer(keyword)), value); - }); -} - -} // namespace - -namespace sourcemeta::blaze { - -// TODO: Refactor this entire function using `SchemaFrame`'s new `Instances` -// mode. We can loop over every subschema that defines `unevaluatedProperties` -// or `unevaluatedItems`, find all other subschemas with the same unresolved -// instance location (static dependency) or conditional equivalent unresolved -// instance location (dynamic dependency) and see if those ones define any of -// the dependent keywords. -auto unevaluated(const JSON &schema, const SchemaFrame &frame, - const SchemaWalker &walker, const SchemaResolver &resolver) - -> SchemaUnevaluatedEntries { - SchemaUnevaluatedEntries result; - - frame.for_each_subschema([&](const SchemaFrame::Location &location) -> void { - const auto &subschema{get(schema, location.pointer)}; - assert((subschema.is_object() || subschema.is_boolean())); - if (!subschema.is_object()) { - return; - } - - const bool has_unevaluated_properties{ - subschema.defines("unevaluatedProperties")}; - const bool has_unevaluated_items{subschema.defines("unevaluatedItems")}; - if (!has_unevaluated_properties && !has_unevaluated_items) { - return; - } - - const auto &subschema_vocabularies{frame.vocabularies(location, resolver)}; - - // The same pointer may be reachable through alternate identifiers whose - // dynamic anchors carry a different base, so we register the entry under - // each of them - if (has_unevaluated_properties) { - if ((subschema_vocabularies.contains( - Known::JSON_SCHEMA_2020_12_UNEVALUATED) && - subschema_vocabularies.contains( - Known::JSON_SCHEMA_2020_12_APPLICATOR)) || - subschema_vocabularies.contains( - Known::JSON_SCHEMA_2019_09_APPLICATOR)) { - std::set> visited; - SchemaUnevaluatedEntry unevaluated; - find_adjacent_dependencies( - "unevaluatedProperties", schema, frame, walker, resolver, - {"properties", "patternProperties", "additionalProperties", - "unevaluatedProperties"}, - location, location, true, visited, unevaluated); - register_under_all_bases(result, frame, location, - UNEVALUATED_PROPERTIES, unevaluated); - } - } - - if (has_unevaluated_items) { - std::set> visited; - SchemaUnevaluatedEntry unevaluated; - if (subschema_vocabularies.contains( - Known::JSON_SCHEMA_2020_12_UNEVALUATED) && - subschema_vocabularies.contains( - Known::JSON_SCHEMA_2020_12_APPLICATOR)) { - find_adjacent_dependencies( - "unevaluatedItems", schema, frame, walker, resolver, - {"prefixItems", "items", "contains", "unevaluatedItems"}, location, - location, true, visited, unevaluated); - register_under_all_bases(result, frame, location, UNEVALUATED_ITEMS, - unevaluated); - } else if (subschema_vocabularies.contains( - Known::JSON_SCHEMA_2019_09_APPLICATOR)) { - find_adjacent_dependencies( - "unevaluatedItems", schema, frame, walker, resolver, - {"items", "additionalItems", "unevaluatedItems"}, location, - location, true, visited, unevaluated); - register_under_all_bases(result, frame, location, UNEVALUATED_ITEMS, - unevaluated); - } - } - }); - - return result; -} - -} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/compiler/unevaluated.h b/vendor/blaze/src/compiler/unevaluated.h new file mode 100644 index 000000000..2eecf332c --- /dev/null +++ b/vendor/blaze/src/compiler/unevaluated.h @@ -0,0 +1,351 @@ +#ifndef SOURCEMETA_BLAZE_COMPILER_UNEVALUATED_H_ +#define SOURCEMETA_BLAZE_COMPILER_UNEVALUATED_H_ + +#include + +#include +#include + +#include // assert +#include // std::size_t +#include // std::set +#include // std::string_view +#include // std::move, std::pair + +#include "compile_helpers.h" + +namespace sourcemeta::blaze { + +// NOLINTBEGIN(cert-err58-cpp,bugprone-throwing-static-initialization) +static const sourcemeta::core::JSON::String UNEVALUATED_PROPERTIES{ + "unevaluatedProperties"}; +static const sourcemeta::core::JSON::String UNEVALUATED_ITEMS{ + "unevaluatedItems"}; +// NOLINTEND(cert-err58-cpp,bugprone-throwing-static-initialization) + +// Whether a location is already being visited has to survive the nested calls +// that walk through it, and stop applying the moment they unwind +class PathGuard { +public: + PathGuard(std::set> &visited, + const sourcemeta::core::WeakPointer &pointer, const bool is_static) + : visited_{visited}, entry_{pointer, is_static}, + first_visit_{visited.insert(this->entry_).second} {} + + ~PathGuard() { + if (this->first_visit_) { + this->visited_.erase(this->entry_); + } + } + + [[nodiscard]] auto first_visit() const -> bool { return this->first_visit_; } + + PathGuard(const PathGuard &) = delete; + auto operator=(const PathGuard &) -> PathGuard & = delete; + PathGuard(PathGuard &&) = delete; + auto operator=(PathGuard &&) -> PathGuard & = delete; + +private: + std::set> &visited_; + const std::pair entry_; + const bool first_visit_; +}; + +inline auto find_adjacent_dependencies( + const sourcemeta::core::JSON::String ¤t, + const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const std::set &keywords, + const sourcemeta::core::SchemaFrame::Location &root, + const sourcemeta::core::SchemaFrame::Location &entry, const bool is_static, + std::set> &visited, + SchemaUnevaluatedEntry &result) -> void { + // A schema may reference itself, directly or through a chain of in-place + // applicators. Following such a cycle forever exhausts the stack, so we cut + // it as soon as we meet a location already on the path we came in through. + // The mark only lives as long as that path, as two references to a common + // target are not a cycle, and each of them still has to be accounted for + const PathGuard guard{visited, entry.pointer, is_static}; + if (!guard.first_visit()) { + return; + } + + const auto &subschema{sourcemeta::core::get(schema, entry.pointer)}; + if (!subschema.is_object()) { + return; + } + + const auto &subschema_vocabularies{frame.vocabularies(entry, resolver)}; + + for (const auto &property : subschema.as_object()) { + if (property.first == current && entry.pointer == root.pointer) { + continue; + } + if (keywords.contains(property.first)) { + // In 2019-09, `additionalItems` takes no effect without `items` + if (subschema_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2019_09_APPLICATOR) && + property.first == "additionalItems" && !subschema.defines("items")) { + continue; + } + + auto pointer{entry.pointer.concat(make_weak_pointer(property.first))}; + if (is_static) { + result.static_dependencies.emplace(std::move(pointer)); + } else { + result.dynamic_dependencies.emplace(std::move(pointer)); + } + + continue; + } + + switch (walker(property.first, subschema_vocabularies).type) { + // References + case sourcemeta::core::SchemaKeywordType::Reference: { + const auto reference{ + frame.dereference(entry, make_weak_pointer(property.first))}; + if (reference.first == sourcemeta::core::SchemaReferenceType::Static && + reference.second.has_value()) { + // Recurse into a dedicated entry so that whether this reference's + // target contributes any dynamic dependency can be read directly, + // rather than inferred from whether it grew the shared deduplicated + // set, which misses every reference after the first to a common + // target + SchemaUnevaluatedEntry nested; + find_adjacent_dependencies( + current, schema, frame, walker, resolver, keywords, root, + reference.second.value().get(), is_static, visited, nested); + + // Whatever the target contributes gets recorded at the location of + // the target itself, which tells the applicators this reference sits + // under nothing about it. Record the reference as a dependency of + // its own too, so that they can still tell that reaching through it + // leads to one, and therefore that they cannot short-circuit. Only + // the dynamic dependencies are consulted that way, whereas the + // static ones name the keyword locations that evaluate, which a + // reference is not one of + if (!is_static && !nested.dynamic_dependencies.empty()) { + result.dynamic_dependencies.emplace( + entry.pointer.concat(make_weak_pointer(property.first))); + } + + result.unresolved = result.unresolved || nested.unresolved; + result.static_dependencies.merge(nested.static_dependencies); + result.dynamic_dependencies.merge(nested.dynamic_dependencies); + } else if (reference.first == + sourcemeta::core::SchemaReferenceType::Dynamic) { + result.unresolved = true; + } + + break; + } + + // Static + case sourcemeta::core::SchemaKeywordType::ApplicatorElementsInPlace: + // TODO(C++23): Use std::views::enumerate when available in libc++ + for (std::size_t index = 0; index < property.second.size(); index++) { + find_adjacent_dependencies( + current, schema, frame, walker, resolver, keywords, root, + frame.traverse(entry, make_weak_pointer(property.first, index)) + .value() + .get(), + is_static, visited, result); + } + + break; + + // Dynamic + case sourcemeta::core::SchemaKeywordType::ApplicatorElementsInPlaceSome: + if (property.second.is_array()) { + for (std::size_t index = 0; index < property.second.size(); index++) { + find_adjacent_dependencies( + current, schema, frame, walker, resolver, keywords, root, + frame.traverse(entry, make_weak_pointer(property.first, index)) + .value() + .get(), + false, visited, result); + } + } + + break; + case sourcemeta::core::SchemaKeywordType::ApplicatorValueTraverseAnyItem: + [[fallthrough]]; + case sourcemeta::core::SchemaKeywordType::ApplicatorValueTraverseParent: + [[fallthrough]]; + case sourcemeta::core::SchemaKeywordType::ApplicatorValueInPlaceMaybe: + if ((property.second.is_object() || property.second.is_boolean())) { + find_adjacent_dependencies( + current, schema, frame, walker, resolver, keywords, root, + frame.traverse(entry, make_weak_pointer(property.first)) + .value() + .get(), + false, visited, result); + } + + break; + case sourcemeta::core::SchemaKeywordType:: + ApplicatorValueOrElementsInPlace: + if (property.second.is_array()) { + for (std::size_t index = 0; index < property.second.size(); index++) { + find_adjacent_dependencies( + current, schema, frame, walker, resolver, keywords, root, + frame.traverse(entry, make_weak_pointer(property.first, index)) + .value() + .get(), + false, visited, result); + } + } else if ((property.second.is_object() || + property.second.is_boolean())) { + find_adjacent_dependencies( + current, schema, frame, walker, resolver, keywords, root, + frame.traverse(entry, make_weak_pointer(property.first)) + .value() + .get(), + false, visited, result); + } + + break; + case sourcemeta::core::SchemaKeywordType::ApplicatorMembersInPlaceSome: + if (property.second.is_object()) { + for (const auto &pair : property.second.as_object()) { + find_adjacent_dependencies( + current, schema, frame, walker, resolver, keywords, root, + frame + .traverse(entry, + make_weak_pointer(property.first, pair.first)) + .value() + .get(), + false, visited, result); + } + } + + break; + + // Anything else does not contribute to the dependency list + default: + break; + } + } +} + +inline auto register_under_all_bases( + SchemaUnevaluatedEntries &result, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::JSON::String &keyword, + const SchemaUnevaluatedEntry &value) -> void { + result.emplace(frame.uri(location, make_weak_pointer(keyword)), value); + frame.for_each_location( + [&](const sourcemeta::core::SchemaReferenceType, const std::string_view, + const sourcemeta::core::SchemaFrame::Location &alternate) -> void { + if (alternate.pointer != location.pointer || + alternate.base == location.base) { + return; + } + + if (alternate.type != + sourcemeta::core::SchemaFrame::LocationType::Subschema && + alternate.type != + sourcemeta::core::SchemaFrame::LocationType::Resource && + alternate.type != + sourcemeta::core::SchemaFrame::LocationType::Anchor) { + return; + } + + result.emplace(frame.uri(alternate, make_weak_pointer(keyword)), value); + }); +} + +// TODO: Refactor this entire function using `SchemaFrame`'s new `Instances` +// mode. We can loop over every subschema that defines `unevaluatedProperties` +// or `unevaluatedItems`, find all other subschemas with the same unresolved +// instance location (static dependency) or conditional equivalent unresolved +// instance location (dynamic dependency) and see if those ones define any of +// the dependent keywords. +inline auto unevaluated(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver) + -> SchemaUnevaluatedEntries { + SchemaUnevaluatedEntries result; + + frame.for_each_subschema( + [&](const sourcemeta::core::SchemaFrame::Location &location) -> void { + const auto &subschema{sourcemeta::core::get(schema, location.pointer)}; + assert((subschema.is_object() || subschema.is_boolean())); + if (!subschema.is_object()) { + return; + } + + const bool has_unevaluated_properties{ + subschema.defines("unevaluatedProperties")}; + const bool has_unevaluated_items{subschema.defines("unevaluatedItems")}; + if (!has_unevaluated_properties && !has_unevaluated_items) { + return; + } + + const auto &subschema_vocabularies{ + frame.vocabularies(location, resolver)}; + + // The same pointer may be reachable through alternate identifiers whose + // dynamic anchors carry a different base, so we register the entry + // under each of them + if (has_unevaluated_properties) { + if ((subschema_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_UNEVALUATED) && + subschema_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_APPLICATOR)) || + subschema_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2019_09_APPLICATOR)) { + std::set> visited; + SchemaUnevaluatedEntry unevaluated; + find_adjacent_dependencies( + "unevaluatedProperties", schema, frame, walker, resolver, + {"properties", "patternProperties", "additionalProperties", + "unevaluatedProperties"}, + location, location, true, visited, unevaluated); + register_under_all_bases(result, frame, location, + UNEVALUATED_PROPERTIES, unevaluated); + } + } + + if (has_unevaluated_items) { + std::set> visited; + SchemaUnevaluatedEntry unevaluated; + if (subschema_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_UNEVALUATED) && + subschema_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2020_12_APPLICATOR)) { + find_adjacent_dependencies( + "unevaluatedItems", schema, frame, walker, resolver, + {"prefixItems", "items", "contains", "unevaluatedItems"}, + location, location, true, visited, unevaluated); + register_under_all_bases(result, frame, location, UNEVALUATED_ITEMS, + unevaluated); + } else if (subschema_vocabularies.contains( + sourcemeta::core::SchemaVocabularies::Known:: + JSON_SCHEMA_2019_09_APPLICATOR)) { + find_adjacent_dependencies( + "unevaluatedItems", schema, frame, walker, resolver, + {"items", "additionalItems", "unevaluatedItems"}, location, + location, true, visited, unevaluated); + register_under_all_bases(result, frame, location, UNEVALUATED_ITEMS, + unevaluated); + } + } + }); + + return result; +} + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/convert/convert.cc b/vendor/blaze/src/convert/convert.cc index 347204e5e..7d4c353b8 100644 --- a/vendor/blaze/src/convert/convert.cc +++ b/vendor/blaze/src/convert/convert.cc @@ -37,9 +37,9 @@ namespace { using Rule = std::tuple, bool>; /// Construct a rule entry for the given rule type -template T> -[[nodiscard]] auto make_rule() -> Rule { - return {std::make_unique(), +template T, typename... Args> +[[nodiscard]] auto make_rule(Args &&...arguments) -> Rule { + return {std::make_unique(std::forward(arguments)...), std::is_same_v}; } @@ -516,6 +516,7 @@ auto apply(const std::vector &rules, sourcemeta::core::JSON &schema, #include "rules/upgrade_draft_4_to_draft_6.h" #include "rules/upgrade_draft_6_to_draft_7.h" #include "rules/upgrade_draft_7_to_draft_2019_09.h" +#include "rules/upgrade_openapi_3_0_to_draft_4.h" #include "rules/upgrade_openapi_3_1_to_openapi_3_2.h" #undef ONLY_CONTINUE_IF @@ -544,6 +545,40 @@ auto convert(sourcemeta::core::JSON &schema, std::vector rules; rules.reserve(20); + + // Again here, and not only among the spellings above: a marker that said + // something when the pass began can stop saying anything once a rung has + // moved the resource holding it to the dialect the marker names, and a + // conversion that left it behind would not be done with the document + rules.push_back(make_rule()); + + // A document on the OpenAPI 3.0 dialect forks here rather than standing on a + // rung. Every target below reaches it through Draft 4, which is the base + // dialect its Schema Object extends, so this runs ahead of the ladder and + // the rungs carry on from there + rules.push_back(make_rule(target)); + + // Whether this conversion starts from OpenAPI 3.0. Usually only the caller + // can say, because the Schema Object admits no `$schema` member of its own, + // so a document written in that dialect is named from the outside. The + // exception is the override marker, which names a dialect for a document that + // cannot declare one, and which `declared_dialect` reads ahead of `$schema` + // exactly as core frames it. + // + // So the question is what the document says it is, and only where it says + // nothing does the default the caller passed decide. Asking merely whether a + // `$schema` member is present answers both halves wrongly: a document naming + // this dialect through the marker is read as not being on it, and one naming + // another dialect that way is read as being on it. + // + // What this buys is the keywords the fork carries across as themselves. Each + // is author data to the rung that promotes it, and shadowing it there would + // undo the fork, so the rules that would are told to leave it be + const auto declared{declared_dialect(schema)}; + const auto from_openapi_3_0{ + names_openapi_3_0_dialect(declared) || + (declared.empty() && names_openapi_3_0_dialect(default_dialect))}; + rules.push_back(make_rule()); rules.push_back(make_rule()); @@ -563,7 +598,10 @@ auto convert(sourcemeta::core::JSON &schema, target == ConvertTarget::Draft202012 || target == ConvertTarget::OpenAPI31 || target == ConvertTarget::OpenAPI32) { - rules.push_back(make_rule()); + rules.push_back(make_rule( + from_openapi_3_0 + ? std::vector{"readOnly", "writeOnly"} + : std::vector{})); rules.push_back(make_rule()); } @@ -571,7 +609,9 @@ auto convert(sourcemeta::core::JSON &schema, target == ConvertTarget::Draft202012 || target == ConvertTarget::OpenAPI31 || target == ConvertTarget::OpenAPI32) { - rules.push_back(make_rule()); + rules.push_back(make_rule( + from_openapi_3_0 ? std::vector{"deprecated"} + : std::vector{})); rules.push_back(make_rule()); rules.push_back(make_rule()); rules.push_back(make_rule()); @@ -586,7 +626,11 @@ auto convert(sourcemeta::core::JSON &schema, if (target == ConvertTarget::OpenAPI31 || target == ConvertTarget::OpenAPI32) { - rules.push_back(make_rule()); + rules.push_back(make_rule( + from_openapi_3_0 + ? std::vector{"example", "discriminator", + "externalDocs", "xml"} + : std::vector{})); rules.push_back(make_rule()); } diff --git a/vendor/blaze/src/convert/helpers.h b/vendor/blaze/src/convert/helpers.h index abf8cbfbd..994839dbb 100644 --- a/vendor/blaze/src/convert/helpers.h +++ b/vendor/blaze/src/convert/helpers.h @@ -74,20 +74,6 @@ inline auto assign_before_first_key(sourcemeta::core::JSON &schema, schema.try_assign_before(key, value, first); } -inline auto bump_dialect(sourcemeta::core::JSON &schema, const Site &site, - const std::string_view dialect) -> void { - if (!at_dialect_declaration(schema, site)) { - return; - } - - if (schema.defines("$schema")) { - schema.assign("$schema", sourcemeta::core::JSON{dialect}); - return; - } - - assign_before_first_key(schema, "$schema", sourcemeta::core::JSON{dialect}); -} - inline auto current_dialect_or_override(const sourcemeta::core::JSON &schema) -> std::string_view { return declared_dialect(schema); @@ -146,6 +132,119 @@ inline auto is_metaschema_target(const sourcemeta::core::JSON &schema, constexpr std::string_view OPENAPI_3_1_DIALECT{ "https://spec.openapis.org/oas/3.1/dialect/base"}; +// The OpenAPI 3.0 Schema Object dialect, which the conversion reads as a +// source and never writes. It is not a rung: its Schema Object extends the +// Draft 4 base dialect while OpenAPI 3.1's extends 2020-12, so it sits below +// Draft 4 by base dialect and below 3.1 by product version, and those two +// orderings disagree. A document on it forks instead, reaching an OpenAPI +// target through 3.1 and a JSON Schema target through Draft 4. +// +// Both dated spellings are listed and neither is canonical. Unlike OpenAPI +// 3.1's three URIs, these name two documents that genuinely differ, so there +// is nothing here for `normalized_official_dialect` to fold. They differ only +// in the parts of an OpenAPI document that a Schema Object never reaches, +// which is why one set of rules serves both +// NOLINTNEXTLINE(cert-err58-cpp,bugprone-throwing-static-initialization) +constexpr std::array OPENAPI_3_0_DIALECTS{ + {"tag:spec.openapis.org,2024-10-18:oas/3.0/dialect", + "tag:spec.openapis.org,2021-09-28:oas/3.0/dialect"}}; + +// Whether the dialect is one of those, asked of the spelling as written. A tag +// URI carries no fragment and no scheme the spelling rules fold, so what the +// document says is already the only spelling there is +inline auto names_openapi_3_0_dialect(const std::string_view dialect) -> bool { + return std::ranges::find(OPENAPI_3_0_DIALECTS, dialect) != + OPENAPI_3_0_DIALECTS.cend(); +} + +// Fold an OpenAPI 3.0 `nullable` into the `type` beside it, which is the one +// member of that dialect carrying an assertion rather than an annotation. Both +// targets of the fork accept a type array, so the same rewrite serves each. +// +// The keyword takes effect only where `type` is explicitly defined within the +// same Schema Object, which the specification states outright. One standing +// alone constrained nothing, and is left exactly where it is so that the +// shadowing carries it across as the author data it was. Erasing it would drop +// something the author wrote, which is not what becomes of any other member +// this dialect has, and turning it into `{"type": ["null"]}` would invent a +// constraint it never applied. +// +// A `nullable` that a `$ref` suppressed is left untouched too, whatever it +// says. Such a keyword never applied, and the caller shadows it along with +// every other sibling instead +inline auto fold_nullable_into_type(sourcemeta::core::JSON &schema) -> void { + const auto *nullable{schema.try_at("nullable")}; + if (nullable == nullptr || !nullable->is_boolean() || + !nullable->to_boolean() || schema.defines("$ref") || + !schema.defines("type")) { + return; + } + + static const sourcemeta::core::JSON NULL_TYPE{"null"}; + auto &type{schema.at("type")}; + if (type.is_string()) { + auto names{sourcemeta::core::JSON::make_array()}; + names.push_back(type); + names.push_back_if_unique(NULL_TYPE); + type = std::move(names); + } else if (type.is_array()) { + // An array here is not a shape OpenAPI 3.0 permits, as its `type` takes a + // single name. Carrying one anyway costs a line and keeps the rewrite from + // depending on a check that runs elsewhere + type.push_back_if_unique(NULL_TYPE); + } else { + return; + } + + schema.erase("nullable"); +} + +// Draft 4 reads `enum` as a set and requires its members to be unique, while +// the OpenAPI 3.0 Schema Object states `uniqueItems: false` for it outright. A +// repeated option says nothing a single one does not, so a document carrying +// one would otherwise reach a dialect that cannot read it back +inline auto deduplicate_enum(sourcemeta::core::JSON &schema) -> void { + auto *options{schema.try_at("enum")}; + if (options == nullptr || !options->is_array() || options->unique()) { + return; + } + + auto distinct{sourcemeta::core::JSON::make_array()}; + for (const auto &option : options->as_array()) { + distinct.push_back_if_unique(option); + } + + *options = std::move(distinct); +} + +// An empty `anyOf` or `oneOf` is satisfied by no instance whatever, which +// Draft 4 has no boolean schema to say and spells as a `not` of the schema +// every instance satisfies. That goes into `allOf`, whose meaning is already +// the conjunction this needs, so a `not` the author wrote is left exactly as it +// stands. Draft 4 requires at least one branch in each of the three, while the +// OpenAPI 3.0 Schema Object places no bound on them at all +inline auto fold_empty_applicators(sourcemeta::core::JSON &schema) -> void { + static const std::array UNSATISFIABLE_WHEN_EMPTY{ + {"anyOf", "oneOf"}}; + + for (const auto &keyword : UNSATISFIABLE_WHEN_EMPTY) { + const sourcemeta::core::JSON::String name{keyword}; + const auto *branches{schema.try_at(name)}; + if (branches == nullptr || !branches->is_array() || !branches->empty()) { + continue; + } + + schema.erase(name); + if (!schema.defines("allOf") || !schema.at("allOf").is_array()) { + schema.assign("allOf", sourcemeta::core::JSON::make_array()); + } + + auto nothing{sourcemeta::core::JSON::make_object()}; + nothing.assign("not", sourcemeta::core::JSON::make_object()); + schema.at("allOf").push_back(std::move(nothing)); + } +} + // The OpenAPI 3.2 dialect, which declares the same four keywords as 3.1 with // strictly more permissive constraints, so the rung above only has a dialect // to move. OpenAPI 3.2 publishes one dated URI with no undated alias, so there @@ -313,11 +412,13 @@ inline auto names_official_metaschema(const std::string_view uri) -> bool { }); } -// A dialect the ladder does not name is one the conversion has no rules for, -// whether it belongs to a draft older than the ladder starts at or to a -// meta-schema of the caller's own +// Whether the conversion has rules for moving a schema off this dialect. A +// dialect it does not name is one it has no rules for, whether that is a draft +// older than the ladder starts at or a meta-schema of the caller's own. Every +// rung qualifies, and so does OpenAPI 3.0, which forks off the ladder rather +// than standing on it inline auto names_ladder_dialect(const std::string_view dialect) -> bool { - return dialect_position(dialect) > 0; + return dialect_position(dialect) > 0 || names_openapi_3_0_dialect(dialect); } // Core reads this keyword as a dialect too, so it may well be a keyword the @@ -328,6 +429,40 @@ inline auto is_own_dialect_override(const sourcemeta::core::JSON &value) return value.is_string() && dialect_position(value.to_string()) > 0; } +inline auto bump_dialect(sourcemeta::core::JSON &schema, const Site &site, + const std::string_view dialect) -> void { + // Core reads the override marker as the dialect of the position carrying it, + // ahead of any `$schema` beside it and wherever that position is + if (schema.is_object()) { + auto *override_value{schema.try_at(DIALECT_OVERRIDE_KEYWORD)}; + if (override_value != nullptr && override_value->is_string() && + names_ladder_dialect(override_value->to_string())) { + // Where nothing else is read, the dialect moves by writing the marker. A + // `$schema` written here instead would not be the thing read, and the + // position would keep the dialect it had while its keywords moved on + if (!at_dialect_declaration(schema, site)) { + *override_value = sourcemeta::core::JSON{dialect}; + return; + } + + // Here a `$schema` is read, and the marker would go on overriding it, so + // the marker gives way to the declaration that now says the same thing + schema.erase(DIALECT_OVERRIDE_KEYWORD); + } + } + + if (!at_dialect_declaration(schema, site)) { + return; + } + + if (schema.defines("$schema")) { + schema.assign("$schema", sourcemeta::core::JSON{dialect}); + return; + } + + assign_before_first_key(schema, "$schema", sourcemeta::core::JSON{dialect}); +} + // Whether an identifier and a dialect name the same thing once both are // resolved against what the caller said the document is called inline auto names_the_same_uri(const sourcemeta::core::JSON &schema, diff --git a/vendor/blaze/src/convert/rules/dialect_override_becomes_dollar_schema.h b/vendor/blaze/src/convert/rules/dialect_override_becomes_dollar_schema.h index f171069d3..2766c425d 100644 --- a/vendor/blaze/src/convert/rules/dialect_override_becomes_dollar_schema.h +++ b/vendor/blaze/src/convert/rules/dialect_override_becomes_dollar_schema.h @@ -2,7 +2,15 @@ /// any `$schema` beside it, so a document carrying one has whatever dialect it /// names whether or not the author meant that. Settling it into a real /// `$schema` is what makes the rest of the conversion, and the output, depend -/// on nothing but what JSON Schema itself defines +/// on nothing but what JSON Schema itself defines. +/// +/// That is only possible where a `$schema` is the thing framing reads. A marker +/// deeper than that names a dialect for one position, which JSON Schema has no +/// way to say, so it can only be settled when it says nothing: when it names +/// the dialect that position would have had anyway. One that names another +/// dialect stays exactly where it is, and `bump_dialect` moves it along with +/// the schema it governs, because dropping it would silently reinterpret every +/// keyword underneath it class DialectOverrideBecomesDollarSchema final : public SchemaTransformRule { public: DialectOverrideBecomesDollarSchema() @@ -11,7 +19,7 @@ class DialectOverrideBecomesDollarSchema final : public SchemaTransformRule { [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, const sourcemeta::core::JSON &, const sourcemeta::core::SchemaVocabularies &, - const Site &, + const Site &site, const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> bool override { @@ -23,7 +31,37 @@ class DialectOverrideBecomesDollarSchema final : public SchemaTransformRule { // would read as a dialect here. Any other value is the author's own data // that happens to share the name, and is left exactly as written const auto *value{schema.try_at(DIALECT_OVERRIDE_KEYWORD)}; - return value != nullptr && is_own_dialect_override(*value); + return value != nullptr && is_own_dialect_override(*value) && + (at_dialect_declaration(schema, site) || this->says_nothing(site)); + } + + auto begin_pass() const -> void override { this->redundant_.clear(); } + + auto plan(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::SchemaVocabularies &, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const Site &site, const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const -> void override { + this->redundant_.erase(site.pointer); + if (!schema.is_object() || !location.parent.has_value()) { + return; + } + + const auto *value{schema.try_at(DIALECT_OVERRIDE_KEYWORD)}; + if (value == nullptr || !value->is_string()) { + return; + } + + // What the position would read as without the marker is what the resource + // holding it reads as, which only the frame can say + const auto enclosing{frame.traverse(location.parent.value())}; + if (enclosing.has_value() && + without_empty_fragment(enclosing.value().get().dialect) == + without_empty_fragment(value->to_string())) { + this->redundant_.insert(site.pointer); + } } auto transform(sourcemeta::core::JSON &schema, const Site &site) const @@ -32,9 +70,9 @@ class DialectOverrideBecomesDollarSchema final : public SchemaTransformRule { schema.erase(DIALECT_OVERRIDE_KEYWORD); // Only where framing would read a `$schema` does one have to be written to - // keep the dialect the document had. Anywhere else the keyword said - // nothing that a `$schema` could say, and the subschema takes the dialect - // of the resource holding it + // keep the dialect the document had. Anywhere else the marker said nothing + // a `$schema` could say, and said nothing at all, so the subschema takes + // the dialect of the resource holding it either way if (!dialect.is_string() || schema.defines("$schema") || !at_dialect_declaration(schema, site)) { return; @@ -42,4 +80,13 @@ class DialectOverrideBecomesDollarSchema final : public SchemaTransformRule { assign_before_first_key(schema, "$schema", std::move(dialect)); } + +private: + // Whether the marker at this position names the dialect the position would + // read as without it + [[nodiscard]] auto says_nothing(const Site &site) const -> bool { + return this->redundant_.contains(site.pointer); + } + + mutable std::set redundant_; }; diff --git a/vendor/blaze/src/convert/rules/prefix_promoted_draft_2019_09_keywords.h b/vendor/blaze/src/convert/rules/prefix_promoted_draft_2019_09_keywords.h index c2a2293b2..12253f6e3 100644 --- a/vendor/blaze/src/convert/rules/prefix_promoted_draft_2019_09_keywords.h +++ b/vendor/blaze/src/convert/rules/prefix_promoted_draft_2019_09_keywords.h @@ -1,7 +1,9 @@ class PrefixPromoted201909Keywords final : public SchemaTransformRule { public: - PrefixPromoted201909Keywords() - : SchemaTransformRule{"prefix_promoted_2019_09_keywords"} {}; + PrefixPromoted201909Keywords( + std::vector &&keywords_left_alone = {}) + : SchemaTransformRule{"prefix_promoted_2019_09_keywords"}, + keywords_left_alone_{std::move(keywords_left_alone)} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -17,7 +19,7 @@ class PrefixPromoted201909Keywords final : public SchemaTransformRule { promoted_keyword_is_author_data(schema, PROMOTING_DIALECT)); for (const auto &keyword : KEYWORDS) { - if (schema.defines(keyword)) { + if (schema.defines(keyword) && !this->is_left_alone(schema, keyword)) { return true; } } @@ -30,7 +32,8 @@ class PrefixPromoted201909Keywords final : public SchemaTransformRule { this->renames_.clear(); for (const auto &keyword : KEYWORDS) { const std::string keyword_name{keyword}; - if (!schema.defines(keyword_name)) { + if (!schema.defines(keyword_name) || + this->is_left_alone(schema, keyword)) { continue; } @@ -66,5 +69,28 @@ class PrefixPromoted201909Keywords final : public SchemaTransformRule { "unevaluatedProperties", "maxContains", "minContains", "contentSchema", "deprecated"}}; + // A keyword the caller asked to carry across as the keyword it already was, + // rather than as the author data it would otherwise be read as. + // + // The caller names the dialect of a document, not of each position in it, and + // a document may embed a resource written in another dialect whose members + // are exactly the author data this rule exists to protect. What tells the two + // apart is the value: the source dialect constrained the keyword it named, so + // one carrying a shape that dialect never allowed was never that keyword + [[nodiscard]] auto is_left_alone(const sourcemeta::core::JSON &schema, + const std::string_view keyword) const + -> bool { + if (std::ranges::find(this->keywords_left_alone_, keyword) == + this->keywords_left_alone_.cend()) { + return false; + } + + // Only a boolean is the annotation this names. A value of any other shape + // was never that keyword, whatever it is called, and carrying it across as + // one would leave an output the target's own meta-schema rejects + return schema.at(sourcemeta::core::JSON::String{keyword}).is_boolean(); + } + + const std::vector keywords_left_alone_; mutable std::unordered_map renames_; }; diff --git a/vendor/blaze/src/convert/rules/prefix_promoted_draft_7_keywords.h b/vendor/blaze/src/convert/rules/prefix_promoted_draft_7_keywords.h index a9570c7d9..9fccf545b 100644 --- a/vendor/blaze/src/convert/rules/prefix_promoted_draft_7_keywords.h +++ b/vendor/blaze/src/convert/rules/prefix_promoted_draft_7_keywords.h @@ -1,7 +1,9 @@ class PrefixPromotedDraft7Keywords final : public SchemaTransformRule { public: - PrefixPromotedDraft7Keywords() - : SchemaTransformRule{"prefix_promoted_draft_7_keywords"} {}; + PrefixPromotedDraft7Keywords( + std::vector &&keywords_left_alone = {}) + : SchemaTransformRule{"prefix_promoted_draft_7_keywords"}, + keywords_left_alone_{std::move(keywords_left_alone)} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -17,7 +19,7 @@ class PrefixPromotedDraft7Keywords final : public SchemaTransformRule { promoted_keyword_is_author_data(schema, PROMOTING_DIALECT)); for (const auto &keyword : KEYWORDS) { - if (schema.defines(keyword)) { + if (schema.defines(keyword) && !this->is_left_alone(schema, keyword)) { return true; } } @@ -30,7 +32,8 @@ class PrefixPromotedDraft7Keywords final : public SchemaTransformRule { this->renames_.clear(); for (const auto &keyword : KEYWORDS) { const std::string keyword_name{keyword}; - if (!schema.defines(keyword_name)) { + if (!schema.defines(keyword_name) || + this->is_left_alone(schema, keyword)) { continue; } @@ -64,5 +67,28 @@ class PrefixPromotedDraft7Keywords final : public SchemaTransformRule { {"$comment", "if", "then", "else", "readOnly", "writeOnly", "contentMediaType", "contentEncoding"}}; + // A keyword the caller asked to carry across as the keyword it already was, + // rather than as the author data it would otherwise be read as. + // + // The caller names the dialect of a document, not of each position in it, and + // a document may embed a resource written in another dialect whose members + // are exactly the author data this rule exists to protect. What tells the two + // apart is the value: the source dialect constrained the keyword it named, so + // one carrying a shape that dialect never allowed was never that keyword + [[nodiscard]] auto is_left_alone(const sourcemeta::core::JSON &schema, + const std::string_view keyword) const + -> bool { + if (std::ranges::find(this->keywords_left_alone_, keyword) == + this->keywords_left_alone_.cend()) { + return false; + } + + // Only a boolean is the annotation these two name. A value of any other + // shape was never that keyword, whatever it is called, and carrying it + // across as one would leave an output the target's own meta-schema rejects + return schema.at(sourcemeta::core::JSON::String{keyword}).is_boolean(); + } + + const std::vector keywords_left_alone_; mutable std::unordered_map renames_; }; diff --git a/vendor/blaze/src/convert/rules/prefix_promoted_openapi_3_1_keywords.h b/vendor/blaze/src/convert/rules/prefix_promoted_openapi_3_1_keywords.h index 90079a168..d39b66e1b 100644 --- a/vendor/blaze/src/convert/rules/prefix_promoted_openapi_3_1_keywords.h +++ b/vendor/blaze/src/convert/rules/prefix_promoted_openapi_3_1_keywords.h @@ -1,7 +1,9 @@ class PrefixPromotedOpenAPI31Keywords final : public SchemaTransformRule { public: - PrefixPromotedOpenAPI31Keywords() - : SchemaTransformRule{"prefix_promoted_openapi_3_1_keywords"} {}; + PrefixPromotedOpenAPI31Keywords( + std::vector &&keywords_left_alone = {}) + : SchemaTransformRule{"prefix_promoted_openapi_3_1_keywords"}, + keywords_left_alone_{std::move(keywords_left_alone)} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -24,8 +26,14 @@ class PrefixPromotedOpenAPI31Keywords final : public SchemaTransformRule { ONLY_CONTINUE_IF( promoted_keyword_is_author_data(schema, OPENAPI_3_1_DIALECT)); - return schema.defines_any( - {"example", "discriminator", "externalDocs", "xml"}); + for (const auto &keyword : KEYWORDS) { + if (schema.defines(sourcemeta::core::JSON::String{keyword}) && + !this->is_left_alone(schema, keyword)) { + return true; + } + } + + return false; } auto transform(sourcemeta::core::JSON &schema, const Site &) const @@ -33,7 +41,8 @@ class PrefixPromotedOpenAPI31Keywords final : public SchemaTransformRule { this->renames_.clear(); for (const auto &keyword : KEYWORDS) { const std::string keyword_name{keyword}; - if (!schema.defines(keyword_name)) { + if (!schema.defines(keyword_name) || + this->is_left_alone(schema, keyword)) { continue; } @@ -63,5 +72,30 @@ class PrefixPromotedOpenAPI31Keywords final : public SchemaTransformRule { static inline const std::array KEYWORDS{ {"example", "discriminator", "externalDocs", "xml"}}; + // A keyword the caller asked to carry across as the keyword it already was, + // rather than as the author data it would otherwise be read as. + // + // The caller names the dialect of a document, not of each position in it, and + // a document may embed a resource written in another dialect whose members + // are exactly the author data this rule exists to protect. What tells the two + // apart is the value: the source dialect constrained the keyword it named, so + // one carrying a shape that dialect never allowed was never that keyword + [[nodiscard]] auto is_left_alone(const sourcemeta::core::JSON &schema, + const std::string_view keyword) const + -> bool { + if (std::ranges::find(this->keywords_left_alone_, keyword) == + this->keywords_left_alone_.cend()) { + return false; + } + + // Three of these name an object and `example` names anything at all. A + // value of another shape was never the keyword it is named after, and + // carrying it across as one would leave an output the target's own + // meta-schema rejects + return keyword == "example" || + schema.at(sourcemeta::core::JSON::String{keyword}).is_object(); + } + + const std::vector keywords_left_alone_; mutable std::unordered_map renames_; }; diff --git a/vendor/blaze/src/convert/rules/upgrade_openapi_3_0_to_draft_4.h b/vendor/blaze/src/convert/rules/upgrade_openapi_3_0_to_draft_4.h new file mode 100644 index 000000000..fa0efd4eb --- /dev/null +++ b/vendor/blaze/src/convert/rules/upgrade_openapi_3_0_to_draft_4.h @@ -0,0 +1,524 @@ +class UpgradeOpenAPI30ToDraft4 final : public SchemaTransformRule { +public: + UpgradeOpenAPI30ToDraft4(const ConvertTarget target) + : SchemaTransformRule{"upgrade_openapi_3_0_to_draft_4"}, + keeps_read_only_{names_read_only(target)}, + keeps_deprecated_{names_deprecated(target)}, + keeps_openapi_annotations_{names_openapi_annotations(target)}, + keeps_default_mapping_{target == ConvertTarget::OpenAPI32} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::SchemaVocabularies &vocabularies, + const Site &site, const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF( + vocabularies.contains(SchemaVocabularies::Known::OPENAPI_3_0_BASE) && + schema.is_object()); + + return holds_openapi_3_0_override(schema) || + holds_ignored_sibling(schema) || + this->has_pending_openapi_3_0_pattern(schema) || + holds_promoted_keyword(schema) || + holds_movable_definitions(schema) || holds_pending_shape(schema) || + at_dialect_declaration(schema, site); + } + + auto transform(sourcemeta::core::JSON &schema, const Site &site) const + -> void override { + this->renames_.clear(); + this->relocations_.clear(); + + // OpenAPI 3.0 section 4.7.23: a Reference Object cannot be extended with + // additional properties, and any property added to one SHALL be ignored. An + // object carrying `$ref` can only be a Reference Object, because the Schema + // Object closes itself to additional properties and does not name `$ref`, + // so every member beside it was ignored and has to go on being ignored. + // + // Carrying one across as the keyword a target names does two wrong things + // at once: it reads a constraint the source never applied, and it hands the + // target's own meta-schema a value to type-check that nothing ever checked. + // So none of the rewriting below applies to a Reference Object, and its + // members go across as the author data they already were + if (schema.defines("$ref")) { + for (const auto &name : member_names(schema)) { + if (name == "$ref" || name == "$schema" || name.starts_with("x-")) { + continue; + } + + this->shadow(schema, name); + } + } else { + // Before the shadowing below, so that a `nullable` this consumes is gone + // by the time the annotations are renamed and one it declines is still + // there to be renamed with them + fold_nullable_into_type(schema); + + // Comparing the two meta-schemas member by member, the OpenAPI 3.0 Schema + // Object is looser in exactly three places and equal or stricter + // everywhere else: `enum` states `uniqueItems: false`, the three + // applicator arrays carry no `minItems`, and it states none of the + // keyword dependencies Draft 4 does. The first two are shapes, rewritten + // here, and the third is answered for below + deduplicate_enum(schema); + fold_empty_applicators(schema); + + // Draft 4 requires each of these to accompany the bound it modifies, + // while the OpenAPI 3.0 Schema Object states no dependency between + // keywords at all. One standing on its own modifies a bound that is not + // there, and an `allOf` with no branches asserts nothing of the instance, + // so each goes across as the author data it was. None of the three can + // stay where it is: Draft 4 reads an incomplete bound and an empty + // applicator as keywords it will not accept + for (const auto &[keyword, companion] : UNACCOMPANIED) { + if (schema.defines(sourcemeta::core::JSON::String{keyword}) && + !schema.defines(sourcemeta::core::JSON::String{companion})) { + this->shadow(schema, keyword); + } + } + + // After the fold above, which puts what an empty `anyOf` or `oneOf` means + // into `allOf` and so reuses an empty one as the container it needs. An + // `allOf` still empty here is one the author wrote alone + if (schema.defines("allOf") && schema.at("allOf").is_array() && + schema.at("allOf").empty()) { + this->shadow(schema, "allOf"); + } + + for (const auto &keyword : KEYWORDS) { + if (!schema.defines(sourcemeta::core::JSON::String{keyword}) || + this->carries_as_keyword(schema, keyword)) { + continue; + } + + this->shadow(schema, keyword); + } + + // Taken out before the shadowing below, so that what moves is the map the + // author wrote under this name and not a `definitions` that the shadowing + // has since put there. Doing it the other way round hands the shadowed + // member straight back to the move, which promotes the author's data to + // the keyword the shadowing was there to protect it from + auto definitions{take_definitions(schema, this->relocations_)}; + + for (const auto &keyword : PROMOTED_BY_DRAFT_4) { + this->shadow(schema, keyword); + } + + if (!definitions.empty()) { + schema.assign("definitions", std::move(definitions)); + } + } + + bump_dialect(schema, site, DRAFT_4_URL); + } + + // A reference is rebased through these in order, so one whose source names a + // position another's destination sits under has to come first. Shadowing + // `definitions` names the very position the move below places its entries + // under, so the renames are published ahead of the moves: the other way + // round, the rename catches an entry the move has just put there and sends + // every reference to it somewhere nothing lives + [[nodiscard]] auto relocations() const -> std::vector override { + std::vector result; + result.reserve(this->renames_.size() + this->relocations_.size()); + for (const auto &[old_name, new_name] : this->renames_) { + result.emplace_back(sourcemeta::core::Pointer{old_name}, + sourcemeta::core::Pointer{new_name}); + } + + result.insert(result.cend(), this->relocations_.cbegin(), + this->relocations_.cend()); + return result; + } + +private: + // The only member the dialect reads a named subschema under, which is how a + // document on it keeps anything reusable at all. Its applicators hold + // subschemas too, but none of them by a name the author chooses + // NOLINTNEXTLINE(cert-err58-cpp,bugprone-throwing-static-initialization) + static inline const std::string DEFINITIONS{"x-definitions"}; + + // NOLINTNEXTLINE(cert-err58-cpp,bugprone-throwing-static-initialization) + static inline const std::string DRAFT_4_URL{ + "http://json-schema.org/draft-04/schema#"}; + + // What the OpenAPI 3.0 Schema Object adds that Draft 4 has no word for, less + // the `nullable` the rewrite above consumes. Every one of these annotates + // rather than asserts, so shadowing one changes no verdict, and Draft 4 reads + // an `x-` member as the unknown keyword it is. All but `nullable` are + // shadowed only where the target has no word for them, which + // `carries_as_keyword` settles: three became keywords of a later draft, and + // four are named by the OpenAPI dialects above 3.0 + // Which targets name each of the keywords this edge can carry across. Spelled + // out rather than compared, because `ConvertTarget` is a public enumeration + // that promises no ordering, so reading one out of the declaration order + // would make rearranging that header change what this rule does + static auto names_read_only(const ConvertTarget target) -> bool { + return target == ConvertTarget::Draft7 || + target == ConvertTarget::Draft201909 || + target == ConvertTarget::Draft202012 || + target == ConvertTarget::OpenAPI31 || + target == ConvertTarget::OpenAPI32; + } + + static auto names_deprecated(const ConvertTarget target) -> bool { + return target == ConvertTarget::Draft201909 || + target == ConvertTarget::Draft202012 || + target == ConvertTarget::OpenAPI31 || + target == ConvertTarget::OpenAPI32; + } + + static auto names_openapi_annotations(const ConvertTarget target) -> bool { + return target == ConvertTarget::OpenAPI31 || + target == ConvertTarget::OpenAPI32; + } + + static constexpr std::array KEYWORDS{ + {"nullable", "discriminator", "xml", "externalDocs", "example", + "deprecated", "readOnly", "writeOnly"}}; + + // Where a `mapping` entry names a schema inside the container this edge moves + // NOLINTNEXTLINE(cert-err58-cpp,bugprone-throwing-static-initialization) + static inline const std::string MAPPING_INTO_DEFINITIONS{"#/x-definitions/"}; + + // Each of these modifies the bound beside it, which Draft 4 requires to be + // there and OpenAPI 3.0 does not + static constexpr std::array, 2> + UNACCOMPANIED{ + {{"exclusiveMinimum", "minimum"}, {"exclusiveMaximum", "maximum"}}}; + + // The other direction: what Draft 4 names that the OpenAPI 3.0 Schema Object + // does not, so a document carrying one carries author data that the target + // would start reading as a keyword. `id` is the one that matters most, as + // Draft 4 takes it for the identifier that every reference in the document + // resolves against. + // + // Two positions can carry one of these, and the Schema Object is neither: + // there the dialect prohibits all five outright. A Reference Object + // constrains nothing beyond its `$ref`, though the branch above answers for + // that one by shadowing every member it holds. The other is an entry of an + // `x-definitions` map, which the dialect constrains with `^x-` and so not at + // all. That is the position this list is for, and it is narrow: an entry + // something references is read as a Schema Object and refused, so what + // survives to here sits in an entry nothing reads. It still has to be + // shadowed, because the member would otherwise reach the target as a keyword + // whose value makes the output invalid against the dialect it declares. + // + // `$schema` is left out because it is the one of the six that names a + // dialect. Where framing would read one, `bump_dialect` writes the rung's own + // in its place, and anywhere else `ShadowStrayDialectDeclaration` carries it + // across as the author data it is + static constexpr std::array PROMOTED_BY_DRAFT_4{ + {"id", "definitions", "dependencies", "patternProperties", + "additionalItems"}}; + + // Carry a member across as the author data it is, under a name the target + // reads as nothing. A name already taken gains another prefix rather than + // overwriting what is there + auto shadow(sourcemeta::core::JSON &schema, + const std::string_view keyword) const -> void { + const std::string keyword_name{keyword}; + if (!schema.defines(keyword_name)) { + return; + } + + std::string prefixed_name{"x-" + keyword_name}; + while (schema.defines(prefixed_name) || names_core_reads(prefixed_name)) { + prefixed_name.insert(0, "x-"); + } + + this->renames_.emplace(keyword_name, prefixed_name); + schema.rename(keyword_name, std::move(prefixed_name)); + } + + // Whether this subschema still carries something of the dialect that the + // transform will act on. Each of those is either consumed or shadowed, so the + // answer turns false once it has run and the conversion stops finding work + // here. One carried across as itself is not work, or the answer would never + // turn false + [[nodiscard]] auto + has_pending_openapi_3_0_pattern(const sourcemeta::core::JSON &subschema) const + -> bool { + for (const auto &keyword : KEYWORDS) { + if (subschema.defines(sourcemeta::core::JSON::String{keyword}) && + !this->carries_as_keyword(subschema, keyword)) { + return true; + } + } + + return false; + } + + // Three of these became keywords of a later draft with the meaning OpenAPI + // 3.0 already gave them, so a target defining one takes it as it stands + // rather than as the author data the shadowing would make of it. `readOnly` + // and `writeOnly` are native from Draft 7 and `deprecated` from 2019-09. + // + // This is the one place in the module where a rule reads the target the + // caller finally asked for rather than the rung in front of it. It has to: + // Draft 4 cannot express any of the three, so what the waypoint should carry + // is only answerable from where the document is going. The cost is that + // converting by way of Draft 4 no longer reproduces the direct result for + // these three, which `convertsuite` records per fixture + [[nodiscard]] auto carries_as_keyword(const sourcemeta::core::JSON &subschema, + const std::string_view keyword) const + -> bool { + if (keyword == "readOnly" || keyword == "writeOnly") { + return this->keeps_read_only_; + } + + if (keyword == "deprecated") { + return this->keeps_deprecated_; + } + + if (!this->keeps_openapi_annotations_) { + return false; + } + + // Of the four the OpenAPI dialects name for themselves, only this one is + // stated more loosely by 3.0 than by the dialects above it, so only this + // one has a shape to answer for + if (keyword == "discriminator") { + const auto &discriminator{ + subschema.at(sourcemeta::core::JSON::String{keyword})}; + return this->is_expressible_discriminator(discriminator) && + mapping_survives_the_move(discriminator); + } + + // `nullable` is not among them: no OpenAPI dialect above 3.0 has it, as a + // type union says it there instead + return keyword == "xml" || keyword == "externalDocs" || + keyword == "example"; + } + + // A `mapping` value may name a schema by reference, and the frame indexes no + // such reference, so nothing follows one when the schema it names moves. The + // one member this edge moves is the subschema container, so a mapping naming + // a position inside it would be left pointing where nothing is any more. + // Carrying that across as a keyword would publish a discriminator that lies + // about where its branches are, so it goes across as author data instead, + // which at least reads as the record of the author's intent that it is + static auto + mapping_survives_the_move(const sourcemeta::core::JSON &discriminator) + -> bool { + const auto *mapping{discriminator.try_at("mapping")}; + if (mapping == nullptr || !mapping->is_object()) { + return true; + } + + for (const auto &entry : mapping->as_object()) { + if (entry.second.is_string() && + entry.second.to_string().starts_with(MAPPING_INTO_DEFINITIONS)) { + return false; + } + } + + return true; + } + + // OpenAPI 3.0 leaves its Discriminator Object open, naming `propertyName` and + // `mapping` without closing the object to anything else, while 3.1 and 3.2 + // close theirs to those two and the extension prefix. One carrying anything + // more is a discriminator the target cannot read, so it goes across as the + // author data it has to be there rather than as a keyword that would make the + // output invalid against the dialect it declares. + // + // Which members are asked about depends on the target, because the two + // shapes differ: 3.1 names `propertyName` and `mapping`, and 3.2 names + // `defaultMapping` as well. A discriminator carrying that one is a + // discriminator 3.2 reads and 3.1 does not + [[nodiscard]] auto is_expressible_discriminator( + const sourcemeta::core::JSON &discriminator) const -> bool { + if (!discriminator.is_object()) { + return false; + } + + for (const auto &entry : discriminator.as_object()) { + if (entry.first == "propertyName" || entry.first == "mapping" || + entry.first.starts_with("x-")) { + continue; + } + + if (entry.first == "defaultMapping" && this->keeps_default_mapping_) { + continue; + } + + return false; + } + + return true; + } + + static auto holds_promoted_keyword(const sourcemeta::core::JSON &schema) + -> bool { + for (const auto &keyword : PROMOTED_BY_DRAFT_4) { + if (schema.defines(sourcemeta::core::JSON::String{keyword})) { + return true; + } + } + + return false; + } + + // The members of a Reference Object that the dialect ignored and that a + // target would start reading. One already carrying the extension prefix is + // read as nothing anywhere, and a `$schema` is answered for by the rule that + // settles a dialect declaration + static auto holds_ignored_sibling(const sourcemeta::core::JSON &schema) + -> bool { + if (!schema.defines("$ref")) { + return false; + } + + for (const auto &entry : schema.as_object()) { + if (entry.first != "$ref" && entry.first != "$schema" && + !entry.first.starts_with("x-")) { + return true; + } + } + + return false; + } + + // Taken as values, since shadowing renames the members being walked + static auto member_names(const sourcemeta::core::JSON &schema) + -> std::vector { + std::vector names; + names.reserve(schema.size()); + for (const auto &entry : schema.as_object()) { + names.push_back(entry.first); + } + + return names; + } + + // The extension prefix is not a free namespace: core reads two names in it, + // this dialect's subschema container and the marker that names a dialect. A + // member prefixed onto either would hand core a schema map or a dialect the + // author never wrote, so both count as taken and the prefix grows past them + static auto names_core_reads(const std::string_view name) -> bool { + return name == DEFINITIONS || name == DIALECT_OVERRIDE_KEYWORD; + } + + // Core reads the marker as the dialect of the position carrying it, so one + // naming OpenAPI 3.0 holds that position on this dialect until it is cleared. + // Nothing else can clear it: every rung of the ladder is gated on a + // vocabulary this dialect does not have, so the position would keep both the + // marker and whatever spelling the dialect gave its keywords + static auto holds_openapi_3_0_override(const sourcemeta::core::JSON &schema) + -> bool { + const auto *value{schema.try_at(DIALECT_OVERRIDE_KEYWORD)}; + return value != nullptr && value->is_string() && + names_openapi_3_0_dialect(value->to_string()); + } + + // Whether a shape only the OpenAPI 3.0 dialect admits is still here. Each is + // either rewritten or shadowed by the transform, so the answer turns false + // once it has run + static auto holds_pending_shape(const sourcemeta::core::JSON &schema) + -> bool { + const auto *options{schema.try_at("enum")}; + if (options != nullptr && options->is_array() && !options->unique()) { + return true; + } + + if ((schema.defines("exclusiveMinimum") && !schema.defines("minimum")) || + (schema.defines("exclusiveMaximum") && !schema.defines("maximum"))) { + return true; + } + + for (const auto &keyword : {"allOf", "anyOf", "oneOf"}) { + const auto *branches{ + schema.try_at(sourcemeta::core::JSON::String{keyword})}; + if (branches != nullptr && branches->is_array() && branches->empty()) { + return true; + } + } + + return false; + } + + // The extension prefix constrains nothing, so a member of this one holds + // whatever its author put there, while `definitions` reads every member of + // itself as a schema. Only what is one has somewhere to go + static auto holds_schema(const sourcemeta::core::JSON &entry) -> bool { + return entry.is_object() || entry.is_boolean(); + } + + // Neither OpenAPI 3.0 nor Draft 4 has a boolean schema, and yet the walker + // reads every member of this one as a subschema, so a reference naming a + // boolean here is asserting something and has to keep asserting it. Draft 4 + // says the same two things with the schema every instance satisfies and the + // schema none does + static auto as_object_schema(const sourcemeta::core::JSON &entry) + -> sourcemeta::core::JSON { + if (!entry.is_boolean()) { + return entry; + } + + auto result{sourcemeta::core::JSON::make_object()}; + if (!entry.to_boolean()) { + result.assign("not", sourcemeta::core::JSON::make_object()); + } + + return result; + } + + static auto holds_movable_definitions(const sourcemeta::core::JSON &schema) + -> bool { + if (!schema.defines(DEFINITIONS) || !schema.at(DEFINITIONS).is_object()) { + return false; + } + + for (const auto &entry : schema.at(DEFINITIONS).as_object()) { + if (holds_schema(entry.second)) { + return true; + } + } + + return false; + } + + // Draft 4 keeps under `definitions` what the OpenAPI 3.0 Schema Object keeps + // here, and reads nothing of a schema under a member it does not know, so + // every entry that is a schema is taken out for the caller to place and the + // references reaching one follow it. Anything else stays where it was rather + // than being carried somewhere that would read it as a schema it is not + static auto take_definitions(sourcemeta::core::JSON &schema, + std::vector &relocations) + -> sourcemeta::core::JSON { + auto moved{sourcemeta::core::JSON::make_object()}; + if (!holds_movable_definitions(schema)) { + return moved; + } + + auto kept{sourcemeta::core::JSON::make_object()}; + for (const auto &entry : schema.at(DEFINITIONS).as_object()) { + if (!holds_schema(entry.second)) { + kept.assign(entry.first, entry.second); + continue; + } + + relocations.emplace_back( + sourcemeta::core::Pointer{DEFINITIONS, entry.first}, + sourcemeta::core::Pointer{"definitions", entry.first}); + moved.assign(entry.first, as_object_schema(entry.second)); + } + + schema.erase(DEFINITIONS); + if (!kept.empty()) { + schema.assign(DEFINITIONS, std::move(kept)); + } + + return moved; + } + + const bool keeps_read_only_; + const bool keeps_deprecated_; + const bool keeps_openapi_annotations_; + const bool keeps_default_mapping_; + mutable std::unordered_map renames_; + mutable std::vector relocations_; +}; diff --git a/vendor/blaze/src/evaluator/evaluator_describe.cc b/vendor/blaze/src/evaluator/evaluator_describe.cc index 17a6c9823..4efe585db 100644 --- a/vendor/blaze/src/evaluator/evaluator_describe.cc +++ b/vendor/blaze/src/evaluator/evaluator_describe.cc @@ -1083,6 +1083,17 @@ auto describe(const bool valid, const Instruction &step, return "The value was expected to be an integer above the given minimum"; } + if (step.type == + sourcemeta::blaze::InstructionIndex::AssertionNumberIntegerBounded) { + std::ostringstream message; + const auto &value{instruction_value(step)}; + message << "The " << value_type_name(target) << " value "; + describe_stringify(target, message); + message << " was expected to be an integer between " << value.first + << " and " << value.second; + return message.str(); + } + if (step.type == sourcemeta::blaze::InstructionIndex::AssertionObjectPropertiesSimple) { return "The object value was expected to validate against the defined " @@ -1717,6 +1728,32 @@ auto describe(const bool valid, const Instruction &step, "string is short enough to satisfy a negative maximum length"; } + // The lower bounds below compile the same way when the bound is too large + // to hold, as nothing can be big enough to satisfy one + if (keyword == "minLength") { + return "The value was expected to be of a type other than string, as no " + "string is long enough to satisfy a minimum length that large"; + } + + if (keyword == "minItems") { + return "The value was expected to be of a type other than array, as no " + "array has enough items to satisfy a minimum size that large"; + } + + if (keyword == "minProperties") { + return "The value was expected to be of a type other than object, as no " + "object has enough properties to satisfy a minimum size that " + "large"; + } + + // `contains` compiles the same way when its `minContains` is too large to + // hold, as no array has that many items for the subschema to match + if (keyword == "contains") { + return "The value was expected to be of a type other than array, as no " + "array has enough matching items to satisfy a minimum count that " + "large"; + } + std::ostringstream message; describe_not_types_check(valid, target.type(), instruction_value(step), message); @@ -2291,6 +2328,23 @@ auto describe(const bool valid, const Instruction &step, return message.str(); } + if (step.type == sourcemeta::blaze::InstructionIndex::AssertionNumberType) { + std::ostringstream message; + message << "The " << value_type_name(target) << " value "; + describe_stringify(target, message); + message << " was expected to be exactly representable as an IEEE 754 "; + switch (instruction_value(step)) { + case ValueNumberType::Float: + message << "single precision floating point number"; + break; + case ValueNumberType::Double: + message << "double precision floating point number"; + break; + } + + return message.str(); + } + if (step.type == sourcemeta::blaze::InstructionIndex::AssertionStringType) { assert(target.is_string()); std::ostringstream message; @@ -2360,6 +2414,9 @@ auto describe(const bool valid, const Instruction &step, case ValueStringType::Color: message << " CSS 2 color"; break; + case ValueStringType::Base64: + message << " RFC 4648 Base64 string"; + break; default: return unknown(); } diff --git a/vendor/blaze/src/evaluator/evaluator_json.cc b/vendor/blaze/src/evaluator/evaluator_json.cc index 28d0f9f1d..7f159f9f1 100644 --- a/vendor/blaze/src/evaluator/evaluator_json.cc +++ b/vendor/blaze/src/evaluator/evaluator_json.cc @@ -44,6 +44,7 @@ auto value_from_json(const sourcemeta::core::JSON &wrapper) case 22: return sourcemeta::core::from_json(value); case 23: return sourcemeta::core::from_json(value); case 24: return sourcemeta::core::from_json(value); + case 25: return sourcemeta::core::from_json(value); // clang-format on default: std::unreachable(); diff --git a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator.h b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator.h index 60ba80c65..4ffdd52e9 100644 --- a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator.h +++ b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator.h @@ -46,7 +46,7 @@ struct Template { }; /// @ingroup evaluator -constexpr std::size_t JSON_VERSION{7}; +constexpr std::size_t JSON_VERSION{9}; /// @ingroup evaluator /// Parse a template from JSON diff --git a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h index dd606c188..339980962 100644 --- a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h +++ b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h @@ -13,6 +13,8 @@ #include #include +#include // assert + // TODO(C++23): Replace SOURCEMETA_ASSUME with [[assume]] when available // across all compilers (Clang 19, GCC 13) #if defined(__clang__) @@ -242,6 +244,33 @@ inline auto effective_type_strict_real(const JSON &instance) noexcept } } +// An integral number that no 64-bit integer can hold cannot be narrowed to one +// to be compared, and `as_integer` throws rather than saturating, so the +// comparison happens in a domain wide enough to hold any of them instead. +// A real converts through `exact_from` because the constructor spells a double +// with 17 significant digits, which is too few to tell apart two integers that +// a bound of this width can sit between +inline auto as_decimal(const JSON &instance) -> Decimal { + assert(instance.is_integral()); + return instance.is_decimal() ? instance.to_decimal() + : Decimal::exact_from(instance.to_real()); +} + +inline auto integer_within_bounds(const JSON &target, + const ValueIntegerBounds &value) -> bool { + if (target.is_integer()) { + const auto integer{target.to_integer()}; + return integer >= value.first && integer <= value.second; + } + + if (target.is_integral()) { + const auto decimal{as_decimal(target)}; + return decimal >= Decimal{value.first} && decimal <= Decimal{value.second}; + } + + return false; +} + template inline auto assume_value(const Value &variant) noexcept -> const T & { const auto *pointer{std::get_if(&variant)}; @@ -837,15 +866,7 @@ INSTRUCTION_HANDLER(AssertionDivisible) { } INSTRUCTION_DIRECT(AssertionTypeIntegerBounded, ValueIntegerBounds) { - if (target.is_integer()) { - const auto integer{target.to_integer()}; - return integer >= value.first && integer <= value.second; - } - if (target.is_integral()) { - const auto integer{target.as_integer()}; - return integer >= value.first && integer <= value.second; - } - return false; + return integer_within_bounds(target, value); } INSTRUCTION_HANDLER(AssertionTypeIntegerBounded) { @@ -875,7 +896,7 @@ INSTRUCTION_DIRECT(AssertionTypeIntegerLowerBound, ValueIntegerBounds) { return target.to_integer() >= value.first; } if (target.is_integral()) { - return target.as_integer() >= value.first; + return as_decimal(target) >= Decimal{value.first}; } return false; } @@ -891,6 +912,13 @@ INSTRUCTION_DIRECT(AssertionTypeIntegerLowerBoundStrict, ValueIntegerBounds) { if (target.is_integer()) { return target.to_integer() >= value.first; } + // An integer too large for 64 bits is held as a decimal, which the strict + // type check still reads as an integer when it was written as digits alone. + // The bounded counterpart needs no such branch, as a value out of that range + // cannot sit within bounds of that width either way + if (effective_type_strict_real(target) == JSON::Type::Integer) { + return target.to_decimal() >= Decimal{value.first}; + } return false; } @@ -901,6 +929,30 @@ INSTRUCTION_HANDLER(AssertionTypeIntegerLowerBoundStrict) { EVALUATE_END(AssertionTypeIntegerLowerBoundStrict); } +INSTRUCTION_DIRECT(AssertionNumberIntegerBounded, ValueIntegerBounds) { + return integer_within_bounds(target, value); +} + +INSTRUCTION_HANDLER(AssertionNumberIntegerBounded) { + EVALUATE_BEGIN_NON_STRING(AssertionNumberIntegerBounded, target.is_number()); + result = DIRECT(AssertionNumberIntegerBounded, target, + assume_value_copy(instruction.value)); + EVALUATE_END(AssertionNumberIntegerBounded); +} + +INSTRUCTION_HANDLER(AssertionNumberType) { + EVALUATE_BEGIN_NON_STRING(AssertionNumberType, target.is_number()); + switch (assume_value_copy(instruction.value)) { + case ValueNumberType::Float: + result = target.is_float(); + break; + case ValueNumberType::Double: + result = target.is_double(); + break; + } + EVALUATE_END(AssertionNumberType); +} + INSTRUCTION_HANDLER(AssertionStringType) { EVALUATE_BEGIN_IF_STRING(AssertionStringType); const auto value{assume_value_copy(instruction.value)}; @@ -968,6 +1020,9 @@ INSTRUCTION_HANDLER(AssertionStringType) { case ValueStringType::Color: result = is_css2_color(target); break; + case ValueStringType::Base64: + result = sourcemeta::core::is_base64(target); + break; default: std::unreachable(); } @@ -2484,12 +2539,12 @@ INSTRUCTION_HANDLER(LoopItemsPropertiesExactlyTypeStrictHash) { // Otherwise why emit this instruction? assert(!value.second.first.empty()); + result = true; + if (!target.is_array()) [[unlikely]] { EVALUATE_END(LoopItemsPropertiesExactlyTypeStrictHash); } - result = true; - const auto hashes_size{value.second.first.size()}; for (const auto &item : target.as_array()) { if (!item.is_object()) [[unlikely]] { @@ -2595,6 +2650,7 @@ INSTRUCTION_HANDLER(LoopItemsPropertiesExactlyTypeStrictHash3) { assert(!value.second.first.empty()); if (!target.is_array()) [[unlikely]] { + result = true; EVALUATE_END(LoopItemsPropertiesExactlyTypeStrictHash3); } @@ -2826,7 +2882,7 @@ using DispatchHandler = bool (*)( template // Must have same order as InstructionIndex // NOLINTNEXTLINE(modernize-avoid-c-arrays) -static constexpr DispatchHandler HANDLERS[101] = { +static constexpr DispatchHandler HANDLERS[103] = { AssertionFail, AssertionDefines, AssertionDefinesStrict, @@ -2867,6 +2923,8 @@ static constexpr DispatchHandler HANDLERS[101] = { AssertionTypeIntegerBoundedStrict, AssertionTypeIntegerLowerBound, AssertionTypeIntegerLowerBoundStrict, + AssertionNumberIntegerBounded, + AssertionNumberType, AssertionStringType, AssertionPropertyType, AssertionPropertyTypeEvaluate, diff --git a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h index 0c926a27d..8e08a4ed6 100644 --- a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h +++ b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h @@ -60,6 +60,8 @@ enum class InstructionIndex : std::uint8_t { AssertionTypeIntegerBoundedStrict, AssertionTypeIntegerLowerBound, AssertionTypeIntegerLowerBoundStrict, + AssertionNumberIntegerBounded, + AssertionNumberType, AssertionStringType, AssertionPropertyType, AssertionPropertyTypeEvaluate, @@ -166,6 +168,8 @@ constexpr std::string_view INSTRUCTION_NAMES[] = { "AssertionTypeIntegerBoundedStrict", "AssertionTypeIntegerLowerBound", "AssertionTypeIntegerLowerBoundStrict", + "AssertionNumberIntegerBounded", + "AssertionNumberType", "AssertionStringType", "AssertionPropertyType", "AssertionPropertyTypeEvaluate", diff --git a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_value.h b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_value.h index 54dbe2120..504758313 100644 --- a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_value.h +++ b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_value.h @@ -139,7 +139,8 @@ enum class ValueStringType : std::uint8_t { RelativeJSONPointer, UUID, Regex, - Color + Color, + Base64 }; /// @ingroup evaluator @@ -189,13 +190,19 @@ using ValueObjectProperties = std::vector< std::tuple>; /// @ingroup evaluator -using Value = std::variant< - ValueNone, ValueJSON, ValueSet, ValueString, ValueProperty, ValueStrings, - ValueStringSet, ValueTypes, ValueType, ValueRegex, ValueUnsignedInteger, - ValueRange, ValueBoolean, ValueNamedIndexes, ValueStringType, - ValueStringMap, ValuePropertyFilter, ValueIndexPair, ValuePointer, - ValueTypedProperties, ValueStringHashes, ValueTypedHashes, - ValueIntegerBounds, ValueIntegerBoundsWithSize, ValueObjectProperties>; +/// Represents a compiler step number logical type +enum class ValueNumberType : std::uint8_t { Float, Double }; + +/// @ingroup evaluator +using Value = + std::variant; } // namespace sourcemeta::blaze diff --git a/vendor/core/CMakeLists.txt b/vendor/core/CMakeLists.txt index 46301b87d..bad2eecb5 100644 --- a/vendor/core/CMakeLists.txt +++ b/vendor/core/CMakeLists.txt @@ -37,9 +37,9 @@ option(SOURCEMETA_CORE_JSONLD "Build the Sourcemeta Core JSON-LD library" ON) option(SOURCEMETA_CORE_JSONL "Build the Sourcemeta Core JSONL library" ON) option(SOURCEMETA_CORE_YAML "Build the Sourcemeta Core YAML library" ON) option(SOURCEMETA_CORE_JSONRPC "Build the Sourcemeta Core JSON-RPC library" ON) -option(SOURCEMETA_CORE_MCP "Build the Sourcemeta Core MCP library" ON) option(SOURCEMETA_CORE_HTTP "Build the Sourcemeta Core HTTP library" ON) option(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL "Use system cURL for the Sourcemeta Core HTTP library" OFF) +option(SOURCEMETA_CORE_MCP "Build the Sourcemeta Core MCP library" ON) option(SOURCEMETA_CORE_OPENAPI "Build the Sourcemeta Core OpenAPI library" ON) option(SOURCEMETA_CORE_JOSE "Build the Sourcemeta Core JOSE library" ON) option(SOURCEMETA_CORE_OAUTH "Build the Sourcemeta Core OAuth library" ON) @@ -271,14 +271,14 @@ if(SOURCEMETA_CORE_JSONRPC) add_subdirectory(src/core/jsonrpc) endif() -if(SOURCEMETA_CORE_MCP) - add_subdirectory(src/core/mcp) -endif() - if(SOURCEMETA_CORE_HTTP) add_subdirectory(src/core/http) endif() +if(SOURCEMETA_CORE_MCP) + add_subdirectory(src/core/mcp) +endif() + if(SOURCEMETA_CORE_OPENAPI) add_subdirectory(src/core/openapi) endif() @@ -470,14 +470,14 @@ if(SOURCEMETA_CORE_TESTS) add_subdirectory(test/jsonrpc) endif() - if(SOURCEMETA_CORE_MCP) - add_subdirectory(test/mcp) - endif() - if(SOURCEMETA_CORE_HTTP) add_subdirectory(test/http) endif() + if(SOURCEMETA_CORE_MCP) + add_subdirectory(test/mcp) + endif() + if(SOURCEMETA_CORE_OPENAPI) add_subdirectory(test/openapi) endif() diff --git a/vendor/core/config.cmake.in b/vendor/core/config.cmake.in index 88ea4047a..7b62dd62a 100644 --- a/vendor/core/config.cmake.in +++ b/vendor/core/config.cmake.in @@ -32,8 +32,8 @@ if(NOT SOURCEMETA_CORE_COMPONENTS) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonld) list(APPEND SOURCEMETA_CORE_COMPONENTS yaml) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonrpc) - list(APPEND SOURCEMETA_CORE_COMPONENTS mcp) list(APPEND SOURCEMETA_CORE_COMPONENTS http) + list(APPEND SOURCEMETA_CORE_COMPONENTS mcp) list(APPEND SOURCEMETA_CORE_COMPONENTS openapi) list(APPEND SOURCEMETA_CORE_COMPONENTS jose) list(APPEND SOURCEMETA_CORE_COMPONENTS oauth) @@ -222,12 +222,26 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonrpc.cmake") elseif(component STREQUAL "mcp") - include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") + if("@SOURCEMETA_CORE_CRYPTO_BACKEND@" STREQUAL "openssl") + find_dependency(OpenSSL 3.0) + endif() + if(@SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL@) + find_dependency(CURL) + endif() include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_crypto.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_punycode.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_idna.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_dns.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_time.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_ip.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_uri.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_http.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonrpc.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_mcp.cmake") elseif(component STREQUAL "http") diff --git a/vendor/core/src/core/crypto/crypto_aes_block.h b/vendor/core/src/core/crypto/crypto_aes_block.h index 9b877fc1a..ac7c3d88d 100644 --- a/vendor/core/src/core/crypto/crypto_aes_block.h +++ b/vendor/core/src/core/crypto/crypto_aes_block.h @@ -153,16 +153,18 @@ inline auto aes_expand_key(const std::string_view key) -> AesKeySchedule { return schedule; } +// AddRoundKey of FIPS 197 Section 5.1.4, which adds the round key of the given +// round into the state a byte at a time over the field of two elements +inline auto aes_add_round_key(const AesKeySchedule &schedule, AesBlock &state, + const std::size_t round) -> void { + for (std::size_t index = 0; index < 16; ++index) { + state[index] ^= schedule.round_keys[(round * 16) + index]; + } +} + inline auto aes_encrypt_block(const AesKeySchedule &schedule, AesBlock state) -> AesBlock { - const auto &round_keys{schedule.round_keys}; - const auto add_round_key{[&round_keys, &state](const std::size_t round) { - for (std::size_t index = 0; index < 16; ++index) { - state[index] ^= round_keys[(round * 16) + index]; - } - }}; - - add_round_key(0); + aes_add_round_key(schedule, state, 0); for (std::size_t round = 1; round <= schedule.rounds; ++round) { for (auto &byte : state) { byte = AES_SUBSTITUTION[byte]; @@ -194,7 +196,7 @@ inline auto aes_encrypt_block(const AesKeySchedule &schedule, AesBlock state) } } - add_round_key(round); + aes_add_round_key(schedule, state, round); } return state; @@ -202,14 +204,7 @@ inline auto aes_encrypt_block(const AesKeySchedule &schedule, AesBlock state) inline auto aes_decrypt_block(const AesKeySchedule &schedule, AesBlock state) -> AesBlock { - const auto &round_keys{schedule.round_keys}; - const auto add_round_key{[&round_keys, &state](const std::size_t round) { - for (std::size_t index = 0; index < 16; ++index) { - state[index] ^= round_keys[(round * 16) + index]; - } - }}; - - add_round_key(schedule.rounds); + aes_add_round_key(schedule, state, schedule.rounds); for (std::size_t round = schedule.rounds; round-- > 0;) { // InvShiftRows over the column-major state (FIPS 197 Section 5.3.1) const AesBlock shifted{{state[0], state[13], state[10], state[7], state[4], @@ -222,7 +217,7 @@ inline auto aes_decrypt_block(const AesKeySchedule &schedule, AesBlock state) byte = AES_INVERSE_SUBSTITUTION[byte]; } - add_round_key(round); + aes_add_round_key(schedule, state, round); if (round != 0) { // InvMixColumns (FIPS 197 Section 5.3.3) diff --git a/vendor/core/src/core/crypto/crypto_base64.cc b/vendor/core/src/core/crypto/crypto_base64.cc index 959863207..9fb28b0aa 100644 --- a/vendor/core/src/core/crypto/crypto_base64.cc +++ b/vendor/core/src/core/crypto/crypto_base64.cc @@ -19,7 +19,9 @@ constexpr std::string_view BASE64_ALPHABET{ constexpr std::string_view BASE64URL_ALPHABET{ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"}; -constexpr std::uint8_t INVALID_SEXTET{0xFF}; +// The greatest value either alphabet denotes, so that a table entry above it +// stands for an octet outside the alphabet +constexpr std::uint32_t MAX_SEXTET{0x3F}; // The inverse of each alphabet, mapping every octet to the sextet it denotes, // or to the invalid sextet when the octet is not part of that alphabet @@ -106,23 +108,20 @@ auto encode(const std::string_view input, const std::string_view alphabet, } } -template -auto decode_into(const std::string_view input, - const std::array &table, const bool padding, - Output &output) -> bool { - // Decoding appends to the output, so a failure after some bytes were written - // rolls the output back to its original length rather than leaving a partial - // decode in a reused buffer - const auto base{output.size()}; +// The characters that carry data, with any trailing padding removed, for an +// input whose overall shape is accepted. RFC 4648 Section 4: "Special +// processing is performed if fewer than 24 bits are available at the end of the +// data being encoded. A full encoding quantum is always completed at the end of +// a quantity", hence the padded form must be a multiple of four characters. A +// trailing quantum of a single character carries no whole octet and is never +// produced by either form +auto significant(const std::string_view input, const bool padding) + -> std::optional { auto data{input}; if (padding) { - // RFC 4648 Section 4: "Special processing is performed if fewer than 24 - // bits are available at the end of the data being encoded. A full encoding - // quantum is always completed at the end of a quantity", hence the padded - // form must be a multiple of four characters if (data.size() % 4 != 0) { - return false; + return std::nullopt; } if (data.ends_with('=')) { @@ -134,9 +133,64 @@ auto decode_into(const std::string_view input, } if (data.size() % 4 == 1) { + return std::nullopt; + } + + return data; +} + +// RFC 4648 Section 3.5: "Implementations MAY chose to reject the encoding if +// the pad bits have not been set to zero". We reject so that every value has +// exactly one accepted encoding. Only the last character of a trailing partial +// quantum carries such bits +auto canonical(const std::string_view data, + const std::array &table) -> bool { + const auto remaining{data.size() % 4}; + if (remaining == 0) { + return true; + } + + const std::uint32_t last{table[static_cast(data.back())]}; + if (remaining == 2) { + return (last & 0x0FU) == 0; + } + + return (last & 0x03U) == 0; +} + +// Deciding validity never needs an assembled octet, so the characters are only +// looked up to accumulate whether any of them fell outside the alphabet +auto validate(const std::string_view input, + const std::array &table, const bool padding) + -> bool { + const auto data{significant(input, padding)}; + if (!data.has_value()) { return false; } + std::uint32_t combined{0}; + for (const auto character : data.value()) { + combined |= table[static_cast(character)]; + } + + return combined <= MAX_SEXTET && canonical(data.value(), table); +} + +template +auto decode_into(const std::string_view input, + const std::array &table, const bool padding, + Output &output) -> bool { + const auto accepted{significant(input, padding)}; + if (!accepted.has_value()) { + return false; + } + + const auto data{accepted.value()}; + + // Decoding appends to the output, so a failure after some bytes were written + // rolls the output back to its original length rather than leaving a partial + // decode in a reused buffer + const auto base{output.size()}; output.reserve(output.size() + ((data.size() / 4) * 3) + 2); std::size_t index{0}; @@ -148,8 +202,7 @@ auto decode_into(const std::string_view input, table[static_cast(data[index + 2])]}; const std::uint32_t fourth{ table[static_cast(data[index + 3])]}; - if (first == INVALID_SEXTET || second == INVALID_SEXTET || - third == INVALID_SEXTET || fourth == INVALID_SEXTET) { + if ((first | second | third | fourth) > MAX_SEXTET) { output.resize(base, '\0'); return false; } @@ -162,16 +215,12 @@ auto decode_into(const std::string_view input, index += 4; } - // RFC 4648 Section 3.5: "Implementations MAY chose to reject the encoding - // if the pad bits have not been set to zero". We reject so that every value - // has exactly one accepted encoding const auto remaining{data.size() - index}; if (remaining == 2) { const std::uint32_t first{table[static_cast(data[index])]}; const std::uint32_t second{ table[static_cast(data[index + 1])]}; - if (first == INVALID_SEXTET || second == INVALID_SEXTET || - (second & 0x0FU) != 0) { + if ((first | second) > MAX_SEXTET || !canonical(data, table)) { output.resize(base, '\0'); return false; } @@ -183,8 +232,7 @@ auto decode_into(const std::string_view input, table[static_cast(data[index + 1])]}; const std::uint32_t third{ table[static_cast(data[index + 2])]}; - if (first == INVALID_SEXTET || second == INVALID_SEXTET || - third == INVALID_SEXTET || (third & 0x03U) != 0) { + if ((first | second | third) > MAX_SEXTET || !canonical(data, table)) { output.resize(base, '\0'); return false; } @@ -237,6 +285,10 @@ auto base64_decode(const std::string_view input, SecureString &output) -> bool { output); } +auto is_base64(const std::string_view input) -> bool { + return validate(input, BASE64_DECODE_TABLE, true); +} + auto base64url_encode(const std::string_view input, std::ostream &output) -> void { output << base64url_encode(input); @@ -280,4 +332,8 @@ auto base64url_decode(const std::string_view input) return output; } +auto is_base64url(const std::string_view input) -> bool { + return validate(input, BASE64URL_DECODE_TABLE, false); +} + } // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_bignum.h b/vendor/core/src/core/crypto/crypto_bignum.h index 61e23fe91..4f9dae95e 100644 --- a/vendor/core/src/core/crypto/crypto_bignum.h +++ b/vendor/core/src/core/crypto/crypto_bignum.h @@ -15,6 +15,7 @@ #include // std::max, std::min #include // std::array #include // std::bit_width +#include // assert #include // std::size_t #include // std::uint8_t, std::uint64_t #include // std::optional @@ -115,12 +116,11 @@ inline auto bignum_from_u64(const std::uint64_t value) noexcept template inline auto bignum_from_hex(const std::string_view hex) -> BasicBignum { - // An odd length is decoded as if a zero nibble had been prepended + // An odd length is decoded as if a zero nibble had been prepended. Every + // number read this way is one written out in this source, so a spelling that + // is no number at all is a mistake here rather than anything a caller did const auto bytes{hex_to_bytes(hex, true)}; - if (!bytes.has_value()) { - return BasicBignum{}; - } - + assert(bytes.has_value()); return bignum_from_bytes(bytes.value()); } @@ -249,8 +249,10 @@ inline auto bignum_reduce(BasicBignum &value, const auto divisor_words{modulus.size}; // A zero modulus defines no residue to reduce into, and the division below - // reads the top two divisor words. An RSA modulus is only bounded from above - // on import, so a key carrying a one- or two-octet modulus reaches here + // would read past the divisor looking for its top two words. Nothing here + // reduces into one, as every modulus is the prime or the order of a curve or + // the modulus of a key, but turning it away is what holds those reads inside + // the divisor whether or not assertions are compiled in if (divisor_words == 0) { return; } diff --git a/vendor/core/src/core/crypto/crypto_ecc.h b/vendor/core/src/core/crypto/crypto_ecc.h index a9df10557..158b16f62 100644 --- a/vendor/core/src/core/crypto/crypto_ecc.h +++ b/vendor/core/src/core/crypto/crypto_ecc.h @@ -12,6 +12,7 @@ #include "crypto_bignum.h" #include // std::array +#include // assert #include // std::size_t #include // std::uint8_t, std::uint64_t #include // std::string_view @@ -316,10 +317,14 @@ inline auto point_is_infinity(const JacobianPoint &point) noexcept -> bool { inline auto point_double(const JacobianPoint &point, const EllipticCurveParameters &curve) -> JacobianPoint { - if (point_is_infinity(point) || bignum_is_zero(point.y)) { + if (point_is_infinity(point)) { return {}; } + // A point whose ordinate is zero has order two, which a curve of prime order + // does not have + assert(!bignum_is_zero(point.y)); + // The doubling formula for curves with coefficient -3 (EFD dbl-2001-b), // which trades the coefficient multiplication and a squaring for one more // subtraction, and computes every small multiple as a chain of modular @@ -359,13 +364,11 @@ inline auto point_double(const JacobianPoint &point, // Point addition in Jacobian coordinates inline auto point_add(const JacobianPoint &left, const JacobianPoint &right, const EllipticCurveParameters &curve) -> JacobianPoint { - if (point_is_infinity(left)) { - return right; - } - - if (point_is_infinity(right)) { - return left; - } + // The one caller combines two points it built with a unit third coordinate, + // so neither is the point at infinity. The mixed addition below is the one + // that accumulates from there + assert(!point_is_infinity(left)); + assert(!point_is_infinity(right)); const auto &prime{curve.prime}; const auto left_z_squared{field_mod_multiply(left.z, left.z, curve)}; diff --git a/vendor/core/src/core/crypto/crypto_eddsa.h b/vendor/core/src/core/crypto/crypto_eddsa.h index 84bd1acee..980b7fe17 100644 --- a/vendor/core/src/core/crypto/crypto_eddsa.h +++ b/vendor/core/src/core/crypto/crypto_eddsa.h @@ -16,6 +16,7 @@ #include "crypto_shake256.h" #include // std::array +#include // assert #include // std::size_t #include // std::uint8_t #include // std::optional, std::nullopt @@ -345,10 +346,8 @@ inline auto edwards_public_key_point(const CurveBignum &scalar, inline auto edwards25519_decode_point(const std::string_view encoding, const EdwardsParameters ¶meters) -> std::optional { - if (encoding.size() != 32) { - return std::nullopt; - } - + // Whoever hands an encoding over has already held it to the width of one + assert(encoding.size() == 32); // The final bit holds the sign of x, the remaining bits the little-endian y std::string bytes{encoding}; const auto sign_bit{ @@ -542,10 +541,8 @@ inline auto edwards25519_prune_scalar(std::string &scalar_bytes) noexcept // Section 5.1.5) inline auto edwards25519_public_key(const std::string_view secret) -> std::optional { - if (secret.size() != 32) { - return std::nullopt; - } - + // A key of this kind only comes about once its seed was read at this width + assert(secret.size() == 32); const auto ¶meters{edwards25519()}; auto hashed{sha512_digest(secret)}; const SecureBufferScope hashed_scope{hashed.data(), hashed.size()}; @@ -562,10 +559,8 @@ inline auto edwards25519_public_key(const std::string_view secret) inline auto edwards25519_sign(const std::string_view secret, const std::string_view message) -> std::optional { - if (secret.size() != 32) { - return std::nullopt; - } - + // A key of this kind only comes about once its seed was read at this width + assert(secret.size() == 32); const auto ¶meters{edwards25519()}; // The key derivation hash carries both the secret scalar and the nonce // prefix, so it and everything derived from it below is wiped before @@ -638,10 +633,8 @@ inline auto edwards25519_sign(const std::string_view secret, inline auto edwards448_decode_point(const std::string_view encoding, const EdwardsParameters ¶meters) -> std::optional { - if (encoding.size() != 57) { - return std::nullopt; - } - + // Whoever hands an encoding over has already held it to the width of one + assert(encoding.size() == 57); // The final bit holds the sign of x, the remaining bits the little-endian y std::string bytes{encoding}; const auto sign_bit{ @@ -815,10 +808,8 @@ inline auto edwards448_prune_scalar(std::string &scalar_bytes) noexcept // Section 5.2.5) inline auto edwards448_public_key(const std::string_view secret) -> std::optional { - if (secret.size() != 57) { - return std::nullopt; - } - + // A key of this kind only comes about once its seed was read at this width + assert(secret.size() == 57); const auto ¶meters{edwards448()}; auto digest{shake256(secret, 114)}; const SecureStringScope digest_scope{digest}; @@ -833,10 +824,8 @@ inline auto edwards448_public_key(const std::string_view secret) inline auto edwards448_sign(const std::string_view secret, const std::string_view message) -> std::optional { - if (secret.size() != 57) { - return std::nullopt; - } - + // A key of this kind only comes about once its seed was read at this width + assert(secret.size() == 57); const auto ¶meters{edwards448()}; // The key derivation hash carries both the secret scalar and the nonce // prefix, so it and everything derived from it below is wiped before diff --git a/vendor/core/src/core/crypto/crypto_hkdf_apple.cc b/vendor/core/src/core/crypto/crypto_hkdf_apple.cc index e1f168909..881011dbd 100644 --- a/vendor/core/src/core/crypto/crypto_hkdf_apple.cc +++ b/vendor/core/src/core/crypto/crypto_hkdf_apple.cc @@ -20,7 +20,11 @@ auto to_selector(const sourcemeta::core::KDFHash hash) noexcept -> int { return 2; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto octets(const std::string_view value) noexcept -> const unsigned char * { diff --git a/vendor/core/src/core/crypto/crypto_hkdf_openssl.cc b/vendor/core/src/core/crypto/crypto_hkdf_openssl.cc index 5a72bad77..65500459a 100644 --- a/vendor/core/src/core/crypto/crypto_hkdf_openssl.cc +++ b/vendor/core/src/core/crypto/crypto_hkdf_openssl.cc @@ -40,7 +40,11 @@ auto digest_name(const sourcemeta::core::KDFHash hash) noexcept -> const return "SHA512"; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } // An empty view may carry a null pointer, which the parameter interface would diff --git a/vendor/core/src/core/crypto/crypto_hkdf_windows.cc b/vendor/core/src/core/crypto/crypto_hkdf_windows.cc index 9d1f7a1fe..5778fe94b 100644 --- a/vendor/core/src/core/crypto/crypto_hkdf_windows.cc +++ b/vendor/core/src/core/crypto/crypto_hkdf_windows.cc @@ -26,7 +26,11 @@ auto hash_name(const sourcemeta::core::KDFHash hash) noexcept -> LPCWSTR { return BCRYPT_SHA512_ALGORITHM; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto hash_name_bytes(const sourcemeta::core::KDFHash hash) noexcept -> ULONG { @@ -39,7 +43,11 @@ auto hash_name_bytes(const sourcemeta::core::KDFHash hash) noexcept -> ULONG { return sizeof(BCRYPT_SHA512_ALGORITHM); } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto as_native(const std::string_view value) noexcept -> PUCHAR { diff --git a/vendor/core/src/core/crypto/crypto_pkcs8.h b/vendor/core/src/core/crypto/crypto_pkcs8.h index dba3939f4..a3453dd2c 100644 --- a/vendor/core/src/core/crypto/crypto_pkcs8.h +++ b/vendor/core/src/core/crypto/crypto_pkcs8.h @@ -51,6 +51,21 @@ enum class PKCS8KeyKind : std::uint8_t { RSA, EllipticCurve, Edwards }; // The parsed shape of an RFC 5958 PrivateKeyInfo, where `key` views the // algorithm specific privateKey octets that each backend parses further +// The components of a PKCS#1 RSAPrivateKey, as views into the key they were +// read from. Handing these back is what keeps whatever consumes the key from +// walking the same structure a second time, where a second walk can only +// repeat what the gate below already turned away +struct RSAPrivateKeyParts { + std::string_view version{}; + std::string_view modulus{}; + std::string_view public_exponent{}; + std::string_view private_exponent{}; + // Where the components of the two-prime form begin, which the gate below + // does not look at because what is required of them is left to whatever + // consumes the key + std::string_view rest{}; +}; + struct PKCS8Key { PKCS8KeyKind kind; EllipticCurve curve; @@ -59,6 +74,9 @@ struct PKCS8Key { // Set when the algorithm is id-RSASSA-PSS rather than rsaEncryption, so that // such a key is refused for RSASSA-PKCS1-v1_5 signing (RFC 8017 Appendix A.2) bool rsa_pss_restricted{false}; + // Set when the algorithm is one of the RSA ones, where what the key holds + // has already been read apart to admit it + RSAPrivateKeyParts rsa{}; }; // Whether a PKCS#1 RSAPrivateKey carries usable components (RFC 8017 Appendix @@ -71,34 +89,35 @@ struct PKCS8Key { // in each backend, since only the reference one reads these components itself // and the rest hand the blob straight to a platform that does not apply the // rule -inline auto rsa_private_key_acceptable(const std::string_view key) -> bool { +inline auto read_rsa_private_key(const std::string_view key) + -> std::optional { // A canonical RSAPrivateKey is exactly one SEQUENCE, so bytes trailing it // mark a malformed encoding (X.690 Section 10.1), the same rule the enclosing // PrivateKeyInfo is held to const auto sequence{der_read(key)}; if (!sequence.has_value() || sequence->tag != 0x30 || !sequence->rest.empty()) { - return false; + return std::nullopt; } const auto version{der_read(sequence->content)}; if (!version.has_value() || version->tag != 0x02) { - return false; + return std::nullopt; } const auto modulus{der_read(version->rest)}; if (!modulus.has_value() || modulus->tag != 0x02) { - return false; + return std::nullopt; } const auto public_exponent{der_read(modulus->rest)}; if (!public_exponent.has_value() || public_exponent->tag != 0x02) { - return false; + return std::nullopt; } const auto private_exponent{der_read(public_exponent->rest)}; if (!private_exponent.has_value() || private_exponent->tag != 0x02) { - return false; + return std::nullopt; } const auto modulus_value{der_unsigned_integer(modulus->content)}; @@ -106,13 +125,55 @@ inline auto rsa_private_key_acceptable(const std::string_view key) -> bool { der_unsigned_integer(public_exponent->content)}; const auto private_exponent_value{ der_unsigned_integer(private_exponent->content)}; - return modulus_value.has_value() && public_exponent_value.has_value() && - private_exponent_value.has_value() && !modulus_value->empty() && - !private_exponent_value->empty() && - modulus_value->size() <= MAXIMUM_KEY_BYTES && - private_exponent_value->size() <= MAXIMUM_KEY_BYTES && - rsa_public_exponent_acceptable(public_exponent_value.value(), - modulus_value.value()); + if (!modulus_value.has_value() || !public_exponent_value.has_value() || + !private_exponent_value.has_value() || modulus_value->empty() || + private_exponent_value->empty() || + modulus_value->size() > MAXIMUM_KEY_BYTES || + private_exponent_value->size() > MAXIMUM_KEY_BYTES || + !rsa_public_exponent_acceptable(public_exponent_value.value(), + modulus_value.value())) { + return std::nullopt; + } + + return RSAPrivateKeyParts{.version = version->content, + .modulus = modulus_value.value(), + .public_exponent = public_exponent_value.value(), + .private_exponent = private_exponent_value.value(), + .rest = private_exponent->rest}; +} + +// Whether a SEC 1 ECPrivateKey carries a usable private scalar, which RFC 5915 +// Section 3 writes as the OCTET STRING following the version. SEC 1 Section 3.2 +// draws the private key from the interval [1, n-1], so a zero scalar, or one at +// or above the group order, names no key. Checked here rather than in each +// backend, as the platform ones hand the blob straight to a system that does +// not apply the rule +inline auto ec_private_key_acceptable(const std::string_view key, + const EllipticCurve curve) -> bool { + // A canonical ECPrivateKey is exactly one SEQUENCE, so bytes trailing it mark + // a malformed encoding (X.690 Section 10.1), the same rule the enclosing + // PrivateKeyInfo is held to + const auto sequence{der_read(key)}; + if (!sequence.has_value() || sequence->tag != 0x30 || + !sequence->rest.empty()) { + return false; + } + + // RFC 5915 Section 3 constrains the version to ecPrivkeyVer1, saying it + // "SHALL be set to ecPrivkeyVer1, which is of type INTEGER and whose value is + // one (1)", so no other value names a key this reads + const auto version{der_read(sequence->content)}; + if (!version.has_value() || version->tag != 0x02 || + version->content.size() != 1 || version->content.front() != '\x01') { + return false; + } + + const auto scalar{der_read(version->rest)}; + if (!scalar.has_value() || scalar->tag != 0x04) { + return false; + } + + return ec_private_scalar_in_range(scalar->content, curve); } // Parse an RFC 5958 PrivateKeyInfo, identifying the algorithm from its object @@ -160,7 +221,8 @@ inline auto parse_pkcs8(const std::string_view der) -> std::optional { // NOLINTEND(modernize-raw-string-literal) if (oid->content == RSA || oid->content == RSA_PSS) { - if (!rsa_private_key_acceptable(private_key->content)) { + const auto parts{read_rsa_private_key(private_key->content)}; + if (!parts.has_value()) { return std::nullopt; } @@ -168,7 +230,8 @@ inline auto parse_pkcs8(const std::string_view der) -> std::optional { .curve = {}, .edwards_curve = {}, .key = private_key->content, - .rsa_pss_restricted = oid->content == RSA_PSS}; + .rsa_pss_restricted = oid->content == RSA_PSS, + .rsa = parts.value()}; } if (oid->content == ED25519 || oid->content == ED448) { @@ -202,6 +265,10 @@ inline auto parse_pkcs8(const std::string_view der) -> std::optional { return std::nullopt; } + if (!ec_private_key_acceptable(private_key->content, curve)) { + return std::nullopt; + } + return PKCS8Key{.kind = PKCS8KeyKind::EllipticCurve, .curve = curve, .edwards_curve = {}, diff --git a/vendor/core/src/core/crypto/crypto_random_other.cc b/vendor/core/src/core/crypto/crypto_random_other.cc index 36a9b319f..aba6a0568 100644 --- a/vendor/core/src/core/crypto/crypto_random_other.cc +++ b/vendor/core/src/core/crypto/crypto_random_other.cc @@ -24,8 +24,6 @@ #include // std::min #include // std::size_t #include // std::uint8_t -#include // std::ifstream -#include // std::ios::binary #include // std::span #include // std::runtime_error #endif @@ -50,37 +48,21 @@ auto fill_random_bytes(std::span bytes) -> void { } #else // getentropy draws from the kernel cryptographic generator and fails closed, - // never returning low-quality bytes, but fills at most 256 bytes per call + // never returning low-quality bytes, but fills at most 256 bytes per call. + // + // There is no second source behind it. The call is reached through the + // kernel one that every system this builds on carries, and a refusal from + // it is a refusal by whoever set the policy that produced it rather than a + // gap to route around: reading the random device instead would walk past a + // sandbox that turned the call away on purpose. So a failure here is final, + // which is how the generator on the other side of this file answers too constexpr std::size_t maximum_per_call{256}; std::size_t offset{0}; while (offset < bytes.size()) { const auto chunk{std::min(bytes.size() - offset, maximum_per_call)}; if (getentropy(bytes.data() + offset, chunk) != 0) { - // Read the rest from the kernel random device, which is only needed on - // the rare platforms where getentropy is present but the kernel call is - // unavailable - std::ifstream device{"/dev/urandom", std::ios::binary}; - if (!device) { - throw std::runtime_error("Could not open the system random device"); - } - - // A single read can return fewer bytes than requested when interrupted - // by a signal, so the buffer is filled across as many reads as it takes, - // failing only when a read makes no progress or the device faults - while (offset < bytes.size()) { - device.read(reinterpret_cast(bytes.data() + offset), - static_cast(bytes.size() - offset)); - const auto count{device.gcount()}; - if (device.bad() || count <= 0) { - throw std::runtime_error( - "Could not read from the system random device"); - } - - offset += static_cast(count); - device.clear(); - } - - return; + throw std::runtime_error( + "Could not generate random bytes with the system entropy call"); } offset += chunk; diff --git a/vendor/core/src/core/crypto/crypto_rsa_oaep_apple.cc b/vendor/core/src/core/crypto/crypto_rsa_oaep_apple.cc index e708ab324..43db7f44a 100644 --- a/vendor/core/src/core/crypto/crypto_rsa_oaep_apple.cc +++ b/vendor/core/src/core/crypto/crypto_rsa_oaep_apple.cc @@ -29,7 +29,11 @@ auto to_oaep_algorithm(const RSAOAEPHash hash) -> SecKeyAlgorithm { return kSecKeyAlgorithmRSAEncryptionOAEPSHA256; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } } // namespace diff --git a/vendor/core/src/core/crypto/crypto_rsa_oaep_openssl.cc b/vendor/core/src/core/crypto/crypto_rsa_oaep_openssl.cc index 7f59cebd5..4648cc357 100644 --- a/vendor/core/src/core/crypto/crypto_rsa_oaep_openssl.cc +++ b/vendor/core/src/core/crypto/crypto_rsa_oaep_openssl.cc @@ -22,7 +22,11 @@ auto to_message_digest(const RSAOAEPHash hash) -> const EVP_MD * { return EVP_sha256(); } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto configure(EVP_PKEY_CTX *context, const RSAOAEPHash hash) -> bool { diff --git a/vendor/core/src/core/crypto/crypto_rsa_oaep_windows.cc b/vendor/core/src/core/crypto/crypto_rsa_oaep_windows.cc index 6b2632601..dd5ab0162 100644 --- a/vendor/core/src/core/crypto/crypto_rsa_oaep_windows.cc +++ b/vendor/core/src/core/crypto/crypto_rsa_oaep_windows.cc @@ -24,7 +24,11 @@ auto to_cng_algorithm(const RSAOAEPHash hash) -> LPCWSTR { return BCRYPT_SHA256_ALGORITHM; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto as_buffer(const std::string_view value) -> PUCHAR { diff --git a/vendor/core/src/core/crypto/crypto_sign_apple.cc b/vendor/core/src/core/crypto/crypto_sign_apple.cc index 887db8217..6fdcfbbf7 100644 --- a/vendor/core/src/core/crypto/crypto_sign_apple.cc +++ b/vendor/core/src/core/crypto/crypto_sign_apple.cc @@ -34,7 +34,11 @@ auto to_sec_key_pkcs1_v15_algorithm( return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA512; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } // These algorithm variants fix the salt length to the hash function output, @@ -51,7 +55,11 @@ auto to_sec_key_pss_algorithm( return kSecKeyAlgorithmRSASignatureMessagePSSSHA512; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto to_sec_key_ecdsa_algorithm( @@ -66,7 +74,11 @@ auto to_sec_key_ecdsa_algorithm( return kSecKeyAlgorithmECDSASignatureMessageX962SHA512; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto make_data(const std::string_view value) -> CFDataRef { @@ -142,13 +154,13 @@ auto native_ec_private_key(const std::string_view sec1, rest = element->rest; } - const auto stripped_scalar{ - sourcemeta::core::strip_left(scalar->content, '\x00')}; - if (point.empty() || stripped_scalar.empty() || - stripped_scalar.size() > field_bytes) { + if (point.empty()) { return nullptr; } + const auto stripped_scalar{ + sourcemeta::core::strip_left(scalar->content, '\x00')}; + std::string data{point}; data.append(sourcemeta::core::pad_left(stripped_scalar, field_bytes, '\x00')); return native_private_key(kSecAttrKeyTypeECSECPrimeRandom, data); @@ -332,7 +344,11 @@ auto make_private_key(const std::string_view pem) -> std::optional { } } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto make_ec_private_key(const EllipticCurve curve, @@ -482,7 +498,11 @@ auto eddsa_sign(const PrivateKey &key, const std::string_view message) return edwards448_sign(internal->edwards_seed, message); } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto derive_public_key(const PrivateKey &key) -> std::optional { @@ -542,7 +562,11 @@ auto derive_public_key(const PrivateKey &key) -> std::optional { } } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } } // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sign_openssl.cc b/vendor/core/src/core/crypto/crypto_sign_openssl.cc index 1912651bb..70b423020 100644 --- a/vendor/core/src/core/crypto/crypto_sign_openssl.cc +++ b/vendor/core/src/core/crypto/crypto_sign_openssl.cc @@ -35,7 +35,11 @@ auto to_message_digest( return EVP_sha512(); } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } // Refuse encrypted documents by offering no password, rather than letting the @@ -53,7 +57,11 @@ auto to_group_name(const sourcemeta::core::EllipticCurve curve) noexcept return "P-521"; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto to_pkey_id(const sourcemeta::core::EdwardsCurve curve) noexcept -> int { @@ -64,7 +72,11 @@ auto to_pkey_id(const sourcemeta::core::EdwardsCurve curve) noexcept -> int { return EVP_PKEY_ED448; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } // The signing functions only cover the three NIST prime curves, so a parsed key @@ -511,7 +523,11 @@ auto derive_public_key(const PrivateKey &key) -> std::optional { } } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } } // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sign_other.cc b/vendor/core/src/core/crypto/crypto_sign_other.cc index 4e4dd2a9c..cca367452 100644 --- a/vendor/core/src/core/crypto/crypto_sign_other.cc +++ b/vendor/core/src/core/crypto/crypto_sign_other.cc @@ -192,8 +192,8 @@ auto hmac(const SignatureHashFunction hash, const std::string_view key, return secure_digest_message(hash, outer_input); } -// RFC 6979 Section 2.3.2 bits2int, which is also the FIPS 186-4 Section 6.4 -// truncation of a bit string to the leftmost order-length bits +// RFC 6979 Section 2.3.2 bits2int, which is also the FIPS 186-5 Section 6.4.1 +// step 2 truncation of a bit string to the leftmost order-length bits auto bits2int(const std::string_view bits, const std::size_t order_bits) -> CurveBignum { auto value{bignum_from_bytes(bits)}; @@ -223,7 +223,7 @@ auto sign_rsa(const PrivateKey::Internal &key, return bignum_to_bytes(representative, key.modulus.size()); } -// The signature for one nonce candidate (FIPS 186-4 Section 6.4.1), returning +// The signature for one nonce candidate (FIPS 186-5 Section 6.4.1), returning // no value when the candidate must be rejected and a fresh one drawn auto ecdsa_signature_for_nonce(const CurveBignum &nonce, const CurveBignum &digest_integer, @@ -242,7 +242,11 @@ auto ecdsa_signature_for_nonce(const CurveBignum &nonce, // The ladder leaves its projective output fixed-width rather than normalized, // so it does not leak the secret nonce through a value-dependent loop. The // nonce lies in [1, n), so the result is never the point at infinity, and the - // r == 0 rejection below is the FIPS 186-4 restart condition regardless + // r == 0 rejection below draws another candidate regardless, which is what + // RFC 6979 Section 3.2 step h asks for. FIPS 186-5 Section 6.4.1 step 11 + // instead ends in failure where the nonce came from its own deterministic + // procedure, that procedure yielding the same nonce again, where advancing + // the generator state the way RFC 6979 does yields a different one auto r{point_affine_x_constant_time(point, parameters)}; bignum_reduce(r, parameters.order); if (bignum_is_zero(r)) { @@ -275,7 +279,7 @@ auto ecdsa_signature_for_nonce(const CurveBignum &nonce, return signature; } -// ECDSA signature generation (FIPS 186-4 Section 6.4.1) with the per-signature +// ECDSA signature generation (FIPS 186-5 Section 6.4.1) with the per-signature // nonce derived deterministically from the private key and the message digest, // so that the signature never depends on the quality of the random generator // (RFC 6979 Section 3.2) @@ -337,10 +341,13 @@ auto sign_ecdsa(const EllipticCurve curve, const SignatureHashFunction hash, return signature; } - // RFC 6979 Section 3.2 step h: reseed before the next candidate - SecureString reseed{hmac_value}; - reseed.push_back('\x00'); - hmac_key = hmac(hash, hmac_key, reseed); + // RFC 6979 Section 3.2 step h, sub-step 3 advances the generator state + // before drawing the next candidate. Section 3.3 notes that the reseeding + // the underlying construction offers is never invoked here, so this is + // that state advance and not a reseed + SecureString key_input{hmac_value}; + key_input.push_back('\x00'); + hmac_key = hmac(hash, hmac_key, key_input); hmac_value = hmac(hash, hmac_key, hmac_value); } @@ -485,61 +492,22 @@ auto make_private_key(const std::string_view pem) -> std::optional { switch (parsed->kind) { case PKCS8KeyKind::RSA: { - // RFC 8017 Appendix A.1.2: RSAPrivateKey is a SEQUENCE of version, - // modulus, publicExponent, privateExponent, and the further primes - const auto sequence{der_read(parsed->key)}; - if (!sequence.has_value() || sequence->tag != 0x30) { - return std::nullopt; - } - - const auto version{der_read(sequence->content)}; - if (!version.has_value() || version->tag != 0x02) { - return std::nullopt; - } - - const auto modulus{der_read(version->rest)}; - if (!modulus.has_value() || modulus->tag != 0x02) { - return std::nullopt; - } - - const auto public_exponent{der_read(modulus->rest)}; - if (!public_exponent.has_value() || public_exponent->tag != 0x02) { - return std::nullopt; - } - - const auto private_exponent{der_read(public_exponent->rest)}; - if (!private_exponent.has_value() || private_exponent->tag != 0x02) { - return std::nullopt; - } - - // Decode each field as a canonical non-negative DER INTEGER, so a - // negative or non-canonically encoded value cannot be silently - // reinterpreted as a different positive number and used for signing - const auto modulus_value{der_unsigned_integer(modulus->content)}; - const auto public_exponent_value{ - der_unsigned_integer(public_exponent->content)}; - const auto private_exponent_value{ - der_unsigned_integer(private_exponent->content)}; - if (!modulus_value.has_value() || !public_exponent_value.has_value() || - !private_exponent_value.has_value() || modulus_value->empty() || - private_exponent_value->empty() || - modulus_value->size() > MAXIMUM_KEY_BYTES || - private_exponent_value->size() > MAXIMUM_KEY_BYTES || - !rsa_public_exponent_acceptable(public_exponent_value.value(), - modulus_value.value())) { - return std::nullopt; - } - + // RFC 8017 Appendix A.1.2 has RSAPrivateKey be a SEQUENCE of version, + // modulus, publicExponent, privateExponent and the further primes, and + // reading it apart is what admitted this key in the first place. Reading + // it again here would only repeat checks already made, which left this + // arm unable to turn anything away and the tests that name it covering + // the gate rather than the arm // RFC 8017 Appendix A.1.2: the two-prime form carries the CRT components // after the private exponent, which the private operation uses to take // the cheaper path of RFC 8017 Section 5.1.2 step 2.b const auto crt{ - read_rsa_crt_components(version->content, private_exponent->rest)}; + read_rsa_crt_components(parsed->rsa.version, parsed->rsa.rest)}; return PrivateKey{new PrivateKey::Internal{ .kind = PrivateKey::Type::RSA, - .modulus = std::string{modulus_value.value()}, - .public_exponent = std::string{public_exponent_value.value()}, - .private_exponent = std::string{private_exponent_value.value()}, + .modulus = std::string{parsed->rsa.modulus}, + .public_exponent = std::string{parsed->rsa.public_exponent}, + .private_exponent = std::string{parsed->rsa.private_exponent}, .prime1 = crt.has_value() ? std::string{crt->prime1} : std::string{}, .prime2 = crt.has_value() ? std::string{crt->prime2} : std::string{}, .exponent1 = @@ -575,19 +543,9 @@ auto make_private_key(const std::string_view pem) -> std::optional { } const auto stripped_scalar{strip_left(scalar->content, '\x00')}; - const auto scalar_width{curve_field_bytes(parsed->curve)}; - // A zero or oversized scalar is not a valid private key - if (stripped_scalar.empty() || stripped_scalar.size() > scalar_width) { - return std::nullopt; - } - - auto padded_scalar{pad_left(stripped_scalar, scalar_width, '\x00')}; + auto padded_scalar{ + pad_left(stripped_scalar, curve_field_bytes(parsed->curve), '\x00')}; const SecureStringScope padded_scalar_scope{padded_scalar}; - // A scalar at or above the curve order is not a valid private key, so it - // is rejected before it drives the public point recovery - if (!ec_private_scalar_in_range(padded_scalar, parsed->curve)) { - return std::nullopt; - } auto point{ec_public_from_scalar(parsed->curve, padded_scalar)}; return PrivateKey{ new PrivateKey::Internal{.kind = PrivateKey::Type::EllipticCurve, @@ -833,12 +791,9 @@ auto derive_public_key(const PrivateKey &key) -> std::optional { return make_rsa_public_key(internal->modulus, internal->public_exponent); case PrivateKey::Type::EllipticCurve: // The public coordinates are populated for every elliptic curve private - // key, whether supplied directly or recomputed from a PEM scalar at parse - // time, so the guard is a defensive check rather than a common path - if (internal->coordinate_x.empty()) { - return std::nullopt; - } - + // key, whether supplied directly or recomputed from a PEM scalar at + // parse time + assert(!internal->coordinate_x.empty()); return make_ec_public_key(internal->elliptic_curve, internal->coordinate_x, internal->coordinate_y); case PrivateKey::Type::Edwards: diff --git a/vendor/core/src/core/crypto/crypto_sign_windows.cc b/vendor/core/src/core/crypto/crypto_sign_windows.cc index a6fe5b679..6b28915cb 100644 --- a/vendor/core/src/core/crypto/crypto_sign_windows.cc +++ b/vendor/core/src/core/crypto/crypto_sign_windows.cc @@ -40,7 +40,11 @@ auto to_cng_algorithm( return BCRYPT_SHA512_ALGORITHM; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto to_ecdsa_algorithm(const sourcemeta::core::EllipticCurve curve) noexcept @@ -54,7 +58,11 @@ auto to_ecdsa_algorithm(const sourcemeta::core::EllipticCurve curve) noexcept return BCRYPT_ECDSA_P521_ALGORITHM; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto to_ecc_private_magic(const sourcemeta::core::EllipticCurve curve) noexcept @@ -68,7 +76,11 @@ auto to_ecc_private_magic(const sourcemeta::core::EllipticCurve curve) noexcept return BCRYPT_ECDSA_PRIVATE_P521_MAGIC; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } struct KeyPair { @@ -226,13 +238,13 @@ auto native_ec_private_key(const sourcemeta::core::EllipticCurve curve, rest = element->rest; } - const auto stripped_scalar{ - sourcemeta::core::strip_left(scalar->content, '\x00')}; - if (point.size() != 2 * field_bytes || stripped_scalar.empty() || - stripped_scalar.size() > field_bytes) { + if (point.size() != 2 * field_bytes) { return {.algorithm = nullptr, .key = nullptr}; } + const auto stripped_scalar{ + sourcemeta::core::strip_left(scalar->content, '\x00')}; + BCRYPT_ECCKEY_BLOB header{}; header.dwMagic = to_ecc_private_magic(curve); header.cbKey = static_cast(field_bytes); @@ -414,7 +426,11 @@ auto make_private_key(const std::string_view pem) -> std::optional { } } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto make_ec_private_key(const EllipticCurve curve, @@ -548,7 +564,11 @@ auto eddsa_sign(const PrivateKey &key, const std::string_view message) return edwards448_sign(internal->edwards_seed, message); } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto derive_public_key(const PrivateKey &key) -> std::optional { @@ -607,7 +627,11 @@ auto derive_public_key(const PrivateKey &key) -> std::optional { } } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } } // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_apple.cc b/vendor/core/src/core/crypto/crypto_verify_apple.cc index 44d71be8d..16777bf47 100644 --- a/vendor/core/src/core/crypto/crypto_verify_apple.cc +++ b/vendor/core/src/core/crypto/crypto_verify_apple.cc @@ -33,7 +33,11 @@ auto to_sec_key_pkcs1_v15_algorithm( return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA512; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } // These algorithm variants fix the salt length to the hash function output, @@ -50,7 +54,11 @@ auto to_sec_key_pss_algorithm( return kSecKeyAlgorithmRSASignatureMessagePSSSHA512; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto to_sec_key_ecdsa_algorithm( @@ -65,7 +73,11 @@ auto to_sec_key_ecdsa_algorithm( return kSecKeyAlgorithmECDSASignatureMessageX962SHA512; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto make_data(const std::string_view value) -> CFDataRef { @@ -355,7 +367,11 @@ auto eddsa_verify(const PublicKey &key, const std::string_view message, return edwards448_verify(internal->edwards_point, message, signature); } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto rsa_public_components(const PublicKey &key) diff --git a/vendor/core/src/core/crypto/crypto_verify_openssl.cc b/vendor/core/src/core/crypto/crypto_verify_openssl.cc index bd71e9196..d3b84cc7f 100644 --- a/vendor/core/src/core/crypto/crypto_verify_openssl.cc +++ b/vendor/core/src/core/crypto/crypto_verify_openssl.cc @@ -33,7 +33,11 @@ auto to_message_digest( return EVP_sha512(); } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto to_group_name(const sourcemeta::core::EllipticCurve curve) noexcept @@ -47,7 +51,11 @@ auto to_group_name(const sourcemeta::core::EllipticCurve curve) noexcept return "P-521"; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto to_pkey_id(const sourcemeta::core::EdwardsCurve curve) noexcept -> int { @@ -58,7 +66,11 @@ auto to_pkey_id(const sourcemeta::core::EdwardsCurve curve) noexcept -> int { return EVP_PKEY_ED448; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto native_rsa_key(const std::string_view modulus, diff --git a/vendor/core/src/core/crypto/crypto_verify_other.cc b/vendor/core/src/core/crypto/crypto_verify_other.cc index f58990a46..97d6f4613 100644 --- a/vendor/core/src/core/crypto/crypto_verify_other.cc +++ b/vendor/core/src/core/crypto/crypto_verify_other.cc @@ -178,8 +178,8 @@ auto to_curve_parameters(const EllipticCurve curve) -> EllipticCurveParameters { return curve_p521(); } -// FIPS 186-4 Section 6.4 step 2, deriving the integer e from the leftmost bits -// of the message digest, truncated to the bit length of the order +// FIPS 186-5 Section 6.4.2 step 3, deriving the integer e from the leftmost +// bits of the message digest, truncated to the bit length of the order auto digest_to_integer(const SignatureHashFunction hash, const std::string_view message, const std::size_t order_bits) -> CurveBignum { @@ -268,7 +268,7 @@ auto verify_pss(const SignatureHashFunction hash, return emsa_pss_verify(hash, message, encoded_message, encoded_bits); } -// ECDSA verification (FIPS 186-4 Section 6.4) over the raw public point +// ECDSA verification (FIPS 186-5 Section 6.4.2) over the raw public point auto verify_ecdsa(const EllipticCurve curve, const SignatureHashFunction hash, const std::string_view coordinate_x, const std::string_view coordinate_y, @@ -288,19 +288,18 @@ auto verify_ecdsa(const EllipticCurve curve, const SignatureHashFunction hash, const auto s{ bignum_from_bytes(signature.substr(field_bytes))}; - // FIPS 186-4 Section 6.4.2 step 1: both integers must lie in [1, n - 1] + // FIPS 186-5 Section 6.4.2 step 1: both integers must lie in [1, n - 1] if (bignum_is_zero(r) || bignum_compare(r, parameters.order) >= 0 || bignum_is_zero(s) || bignum_compare(s, parameters.order) >= 0) { return false; } - // Reject coordinates wider than the field, matching the platform backends and - // preventing an oversized input from being truncated into a valid key + // Every coordinate that reaches here was stripped of its leading zeros and + // held to the width of the field when the key it belongs to was made, which + // is where an oversized input is turned away const auto stripped_x{strip_left(coordinate_x, '\x00')}; const auto stripped_y{strip_left(coordinate_y, '\x00')}; - if (stripped_x.size() > field_bytes || stripped_y.size() > field_bytes) { - return false; - } + assert(stripped_x.size() <= field_bytes && stripped_y.size() <= field_bytes); const auto public_x{bignum_from_bytes(stripped_x)}; const auto public_y{bignum_from_bytes(stripped_y)}; @@ -330,13 +329,13 @@ auto verify_ecdsa(const EllipticCurve curve, const SignatureHashFunction hash, const auto point{point_double_scalar_multiply(u1, generator, u2, public_point, parameters)}; - // FIPS 186-4 Section 6.4.2 step 6: reject when the combination is the point + // FIPS 186-5 Section 6.4.2 step 6: reject when the combination is the point // at infinity if (point_is_infinity(point)) { return false; } - // FIPS 186-4 Section 6.4.2 step 7: the signature is valid when the affine x + // FIPS 186-5 Section 6.4.2 step 9: the signature is valid when the affine x // coordinate, reduced modulo the order, equals r auto candidate{point_affine_x(point, parameters)}; bignum_reduce(candidate, parameters.order); diff --git a/vendor/core/src/core/crypto/crypto_verify_windows.cc b/vendor/core/src/core/crypto/crypto_verify_windows.cc index 7fe080434..f09a7b2b0 100644 --- a/vendor/core/src/core/crypto/crypto_verify_windows.cc +++ b/vendor/core/src/core/crypto/crypto_verify_windows.cc @@ -38,7 +38,11 @@ auto to_cng_algorithm( return BCRYPT_SHA512_ALGORITHM; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto to_ecdsa_algorithm(const sourcemeta::core::EllipticCurve curve) noexcept @@ -52,7 +56,11 @@ auto to_ecdsa_algorithm(const sourcemeta::core::EllipticCurve curve) noexcept return BCRYPT_ECDSA_P521_ALGORITHM; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto to_ecc_public_magic(const sourcemeta::core::EllipticCurve curve) noexcept @@ -66,7 +74,11 @@ auto to_ecc_public_magic(const sourcemeta::core::EllipticCurve curve) noexcept return BCRYPT_ECDSA_PUBLIC_P521_MAGIC; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } struct KeyPair { @@ -355,7 +367,11 @@ auto eddsa_verify(const PublicKey &key, const std::string_view message, return edwards448_verify(internal->edwards_point, message, signature); } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto rsa_public_components(const PublicKey &key) diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_base64.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_base64.h index 51e1a3e23..63137cc95 100644 --- a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_base64.h +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_base64.h @@ -93,6 +93,21 @@ auto SOURCEMETA_CORE_CRYPTO_EXPORT base64_decode(const std::string_view input) auto SOURCEMETA_CORE_CRYPTO_EXPORT base64_decode(const std::string_view input, SecureString &output) -> bool; +/// @ingroup crypto +/// Check whether a string is a canonical padded Base64 encoding (RFC 4648 +/// Section 4) without decoding it, accepting exactly the inputs that decoding +/// accepts. The empty string encodes no bytes. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::is_base64("Zm9vYmFy")); +/// assert(!sourcemeta::core::is_base64("Zm9vYmF")); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT is_base64(const std::string_view input) + -> bool; + /// @ingroup crypto /// Encode a byte sequence using unpadded Base64url (RFC 4648 Section 5) into a /// stream. For example: @@ -167,6 +182,21 @@ base64url_encode(const std::string_view input, SecureString &output) -> void; auto SOURCEMETA_CORE_CRYPTO_EXPORT base64url_decode(const std::string_view input) -> std::optional; +/// @ingroup crypto +/// Check whether a string is a canonical unpadded Base64url encoding (RFC 4648 +/// Section 5) without decoding it, accepting exactly the inputs that decoding +/// accepts. The empty string encodes no bytes. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::is_base64url("Zm8")); +/// assert(!sourcemeta::core::is_base64url("Zm8=")); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT is_base64url(const std::string_view input) + -> bool; + } // namespace sourcemeta::core #endif diff --git a/vendor/core/src/core/diff/diff.cc b/vendor/core/src/core/diff/diff.cc index 081bd669b..756ba6429 100644 --- a/vendor/core/src/core/diff/diff.cc +++ b/vendor/core/src/core/diff/diff.cc @@ -69,6 +69,31 @@ auto to_identities( } } +// The search is given its workspace here rather than alongside the result, so +// that the two buffers it sizes to the span of the change are given up before +// anything is made of what the search found +auto compare_with_myers(const std::vector &original_identities, + const std::vector &modified_identities, + const std::size_t prefix, + const std::size_t original_end, + const std::size_t modified_end, + std::vector &original_changed, + std::vector &modified_changed) -> void { + sourcemeta::core::internal::MyersWorkspace workspace; + // A search only needs its workspace once both sides have something left, as + // any other case resolves to a plain insertion or deletion + if (original_end > prefix && modified_end > prefix) { + const auto span{(original_end - prefix) + (modified_end - prefix)}; + workspace.forward.assign((2 * span) + 3, 0); + workspace.backward.assign((2 * span) + 3, 0); + workspace.offset = static_cast(span + 1); + } + + sourcemeta::core::internal::myers_compare( + original_identities, prefix, original_end, modified_identities, prefix, + modified_end, original_changed, modified_changed, workspace); +} + // Slide every run of changed lines as far towards the end of the input as the // surrounding lines allow, which is what makes paired changes line up auto compact_changes(const std::vector &lines, @@ -225,22 +250,11 @@ auto diff(const std::string_view original, const std::string_view modified, std::vector modified_changed(modified_size, false); switch (algorithm) { - case Diff::Algorithm::Myers: { - internal::MyersWorkspace workspace; - // A search only needs its workspace once both sides have something left, - // as any other case resolves to a plain insertion or deletion - if (original_end > prefix && modified_end > prefix) { - const auto span{(original_end - prefix) + (modified_end - prefix)}; - workspace.forward.assign((2 * span) + 3, 0); - workspace.backward.assign((2 * span) + 3, 0); - workspace.offset = static_cast(span + 1); - } - - internal::myers_compare(original_identities, prefix, original_end, - modified_identities, prefix, modified_end, - original_changed, modified_changed, workspace); + case Diff::Algorithm::Myers: + compare_with_myers(original_identities, modified_identities, prefix, + original_end, modified_end, original_changed, + modified_changed); break; - } } compact_changes(result.original, result.original_ends_with_newline, diff --git a/vendor/core/src/core/diff/myers.h b/vendor/core/src/core/diff/myers.h index afb54c21f..c1ba7f363 100644 --- a/vendor/core/src/core/diff/myers.h +++ b/vendor/core/src/core/diff/myers.h @@ -45,8 +45,13 @@ inline auto myers_middle_snake(const std::vector &original, workspace.forward[static_cast(offset + 1)] = 0; workspace.backward[static_cast(offset + 1)] = 0; - const auto limit{(original_size + modified_size + 1) / 2}; - for (std::ptrdiff_t depth{0}; depth <= limit; ++depth) { + [[maybe_unused]] const auto limit{(original_size + modified_size + 1) / 2}; + // A middle snake always lands within the bounds the search was given, so one + // of the returns below is always taken. Spelling the search as a loop with no + // exit of its own is what says as much to every compiler, which otherwise has + // to be told separately that the end of the function is never reached + for (std::ptrdiff_t depth{0};; ++depth) { + assert(depth <= limit); for (std::ptrdiff_t diagonal{-depth}; diagonal <= depth; diagonal += 2) { const auto lower{ workspace.forward[static_cast(offset + diagonal - 1)]}; @@ -120,12 +125,6 @@ inline auto myers_middle_snake(const std::vector &original, } } } - - assert(false); - return {.original_start = original_start, - .modified_start = modified_start, - .original_end = original_end, - .modified_end = modified_end}; } inline auto myers_compare(const std::vector &original, @@ -135,6 +134,11 @@ inline auto myers_compare(const std::vector &original, std::vector &original_changed, std::vector &modified_changed, MyersWorkspace &workspace) -> void { + // The caller takes off the lines the two sides share at the head and at the + // tail before the search begins, but a middle snake can still leave a + // sub-problem whose own ends match: a forward search never looks behind + // where its snake starts, and a backward search never looks past where its + // snake ends, so each recursion trims again for itself while (original_start < original_end && modified_start < modified_end && original[original_start] == modified[modified_start]) { original_start += 1; diff --git a/vendor/core/src/core/dns/idn_hostname.cc b/vendor/core/src/core/dns/idn_hostname.cc index 97f43464f..672bbad51 100644 --- a/vendor/core/src/core/dns/idn_hostname.cc +++ b/vendor/core/src/core/dns/idn_hostname.cc @@ -4,6 +4,7 @@ #include #include +#include // assert #include // std::size_t #include // std::optional #include // std::string, std::u32string @@ -60,12 +61,11 @@ auto validate_idn_labels(const std::u32string_view codepoints) -> bool { // the corresponding A-label. std::size_t a_label_octets{label.size()}; if (*kind == IDNALabelKind::ULabel) { - try { - const auto body{utf32_to_punycode(decoded)}; - a_label_octets = 4 + body.size(); - } catch (const PunycodeError &) { - return false; - } + // Classifying the label held it to the octet count a label may carry, + // which leaves it far shorter than what the position counters of the + // encoding could ever run past, so the encoding always answers + const auto body{utf32_to_punycode(decoded)}; + a_label_octets = 4 + body.size(); } else if (*kind == IDNALabelKind::Ascii) { // RFC 5891 §4.2.3.1: a label must not contain "--" in the third and // fourth positions unless it is a valid A-label (an "xn--" ACE label, @@ -85,11 +85,9 @@ auto validate_idn_labels(const std::u32string_view codepoints) -> bool { } } - // RFC 5891 §4.2.4 / RFC 1035 §2.3.4: each label, in A-label form, - // must be 1-63 octets - if (a_label_octets > 63) { - return false; - } + // RFC 5891 §4.2.4 / RFC 1035 §2.3.4: each label, in A-label form, must be + // 1-63 octets, which classifying it above already held it to + assert(a_label_octets <= 63); if (!decoded_labels.empty()) { total_octets += 1; diff --git a/vendor/core/src/core/email/email.cc b/vendor/core/src/core/email/email.cc index fd15ad1cd..21c078106 100644 --- a/vendor/core/src/core/email/email.cc +++ b/vendor/core/src/core/email/email.cc @@ -6,6 +6,8 @@ #include "helpers.h" +#include // assert + namespace sourcemeta::core { // RFC 5321 §4.1.2 Mailbox grammar, returning the position of the separator @@ -74,7 +76,8 @@ static auto mailbox_separator(const std::string_view value) while (position < value.size() && value[position] != '@') { const auto character{value[position]}; if (character == '.') { - if (!atom_started || previous_was_dot) { + assert(!atom_started || !previous_was_dot); + if (!atom_started) { return std::nullopt; } previous_was_dot = true; diff --git a/vendor/core/src/core/email/helpers.h b/vendor/core/src/core/email/helpers.h index 8a53dc766..6b38f93f8 100644 --- a/vendor/core/src/core/email/helpers.h +++ b/vendor/core/src/core/email/helpers.h @@ -4,6 +4,7 @@ #include #include +#include // assert #include // std::uint8_t, std::uint16_t #include // std::string #include // std::string_view @@ -106,10 +107,9 @@ inline auto is_address_literal(const std::string_view domain) -> bool { if (domain.back() != ']') { return false; } - // RFC 5321 §4.5.3.1.2: 255-octet cap on a domain "name or number" - if (domain.size() > 255) { - return false; - } + // RFC 5321 §4.5.3.1.2 caps a domain "name or number" at 255 octets, which + // the 254-octet mailbox total of §4.5.3.1.3 already leaves out of reach + assert(domain.size() <= 255); const auto inner{domain.substr(1, domain.size() - 2)}; // RFC 5321 §4.1.3: IPv6-address-literal = "IPv6:" IPv6-addr. The tag names // the syntax that the rest of the literal follows, so a payload that is not diff --git a/vendor/core/src/core/gzip/deflate.h b/vendor/core/src/core/gzip/deflate.h index 3fcbc67e1..7017302eb 100644 --- a/vendor/core/src/core/gzip/deflate.h +++ b/vendor/core/src/core/gzip/deflate.h @@ -781,9 +781,6 @@ class DeflateEncoder { std::size_t distance{0}; std::size_t length{this->find(input, size, position, distance)}; this->insert(input, position); - if (length > 0 && length < 4) { - length = 0; - } if (previous_available && previous_length >= 4 && length <= previous_length) { diff --git a/vendor/core/src/core/gzip/inflate.h b/vendor/core/src/core/gzip/inflate.h index 7f4aa589a..9951d3032 100644 --- a/vendor/core/src/core/gzip/inflate.h +++ b/vendor/core/src/core/gzip/inflate.h @@ -664,10 +664,7 @@ class InflateDecoder { break; case State::StoredLength: - if (!this->read_stored_length(buffers)) { - return this->leave(buffers, InflateStatus::NeedInput); - } - + this->read_stored_length(buffers); break; case State::StoredData: { const auto progress{this->copy_stored_data(buffers)}; @@ -1100,13 +1097,12 @@ class InflateDecoder { } } - auto read_stored_length(InflateBuffers &buffers) -> bool { + // A block header is only read once the whole margin is in hand, and reading + // it gives back more input than it takes, so these four bytes always stand + // behind it unless the source ended in the middle of them + auto read_stored_length(InflateBuffers &buffers) -> void { if (available_input(buffers) < 4) { - if (buffers.input_final) { - unexpected_end(); - } - - return false; + unexpected_end(); } const auto length{static_cast(buffers.input_next[0]) | @@ -1121,7 +1117,6 @@ class InflateDecoder { buffers.input_next += 4; this->stored_remaining_ = length; this->state_ = State::StoredData; - return true; } auto copy_stored_data(InflateBuffers &buffers) -> InflateStatus { diff --git a/vendor/core/src/core/html/include/sourcemeta/core/html_escape.h b/vendor/core/src/core/html/include/sourcemeta/core/html_escape.h index 3a4171695..28ffb8dd9 100644 --- a/vendor/core/src/core/html/include/sourcemeta/core/html_escape.h +++ b/vendor/core/src/core/html/include/sourcemeta/core/html_escape.h @@ -9,6 +9,7 @@ #include #include // std::array +#include // assert #include // std::same_as #include // std::size_t #include // std::uint8_t, std::uint64_t @@ -138,9 +139,10 @@ inline auto html_escape_append(Output &output, const std::string_view input) break; default: // The no-break space is replaced by its named entity (HTML Living - // Standard "escaping a string" step 2) - if (static_cast(input[position]) == 0xC2 && - position + 1 < size && + // Standard "escaping a string" step 2). The table above admits only + // its lead byte here, the five characters above having their own arm + assert(static_cast(input[position]) == 0xC2); + if (position + 1 < size && static_cast(input[position + 1]) == 0xA0) { replacement = " "; consumed = 2; diff --git a/vendor/core/src/core/html/include/sourcemeta/core/html_writer.h b/vendor/core/src/core/html/include/sourcemeta/core/html_writer.h index 46d059362..c46438778 100644 --- a/vendor/core/src/core/html/include/sourcemeta/core/html_writer.h +++ b/vendor/core/src/core/html/include/sourcemeta/core/html_writer.h @@ -45,7 +45,6 @@ class SOURCEMETA_CORE_HTML_EXPORT HTMLWriter { /// has no effect. SOURCEMETA_FORCEINLINE auto close() -> HTMLWriter & { this->tag_open_ = false; - assert(!this->tag_stack_.empty()); if (this->tag_stack_.empty()) [[unlikely]] { return *this; } diff --git a/vendor/core/src/core/http/CMakeLists.txt b/vendor/core/src/core/http/CMakeLists.txt index 6758a8370..a9e7f27f9 100644 --- a/vendor/core/src/core/http/CMakeLists.txt +++ b/vendor/core/src/core/http/CMakeLists.txt @@ -10,14 +10,17 @@ endif() sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME http PRIVATE_HEADERS problem.h status.h method.h message.h error.h system.h - aws_sigv4.h syntax.h - SOURCES helpers.h problem.cc match_accept.cc match_accept_language.cc + aws_sigv4.h syntax.h tracecontext.h baggage.h + SOURCES helpers.h entropy.h problem.cc match_accept.cc match_accept_language.cc negotiate_encoding.cc from_date.cc format_link.cc format_vary.cc field_list.cc accept_includes_all.cc content_type_matches.cc parse_bearer.cc parse_media_type.cc match_media_range.cc cache_control_max_age.cc serialize_cookie.cc aws_sigv4.cc serialize_challenge.cc parse_challenges.cc + is_traceparent.cc parse_traceparent.cc is_tracestate.cc + make_traceparent.cc continue_traceparent.cc + is_baggage.cc is_baggage_value.cc serialize_cache_control.cc scan_quoted_string.cc ${SOURCEMETA_CORE_HTTP_CLIENT_SOURCE}) diff --git a/vendor/core/src/core/http/client_curl.cc b/vendor/core/src/core/http/client_curl.cc index ae4ea0ea8..52be89fa9 100644 --- a/vendor/core/src/core/http/client_curl.cc +++ b/vendor/core/src/core/http/client_curl.cc @@ -54,6 +54,7 @@ auto curl_slist_free_all(curl_slist *list) -> void; // Option ids are a type-class base plus an index in libcurl's headers; the // resolved values are reproduced here (see the trailing comments) constexpr CURLcode CURLE_OK{0}; +constexpr CURLcode CURLE_OPERATION_TIMEDOUT{28}; constexpr long CURL_GLOBAL_ALL{3}; // SSL(1<<0) | WIN32(1<<1) constexpr CURLoption CURLOPT_URL{10002}; // STRINGPOINT + 2 constexpr CURLoption CURLOPT_FOLLOWLOCATION{52}; // LONG + 52 @@ -434,6 +435,13 @@ auto HTTPSystemRequest::send() const -> HTTPResponse { std::string{HTTP_RESPONSE_TOO_LARGE_MESSAGE}}; } + // libcurl reports an expired CURLOPT_TIMEOUT_MS and an expired + // CURLOPT_CONNECTTIMEOUT_MS with the same code, so which of the two + // bounds elapsed cannot be recovered here + if (code == CURLE_OPERATION_TIMEDOUT) { + throw HTTPTimeoutError{this->method_, this->url_, this->timeout_}; + } + throw HTTPError{this->method_, this->url_, api.easy_strerror(code)}; } diff --git a/vendor/core/src/core/http/client_darwin.mm b/vendor/core/src/core/http/client_darwin.mm index 9d01800db..ba606e6b1 100644 --- a/vendor/core/src/core/http/client_darwin.mm +++ b/vendor/core/src/core/http/client_darwin.mm @@ -29,6 +29,7 @@ auto to_nsstring(const std::string_view input) -> NSString * { @interface SourcemetaCoreHTTPDelegate : NSObject @property(nonatomic, assign) sourcemeta::core::HTTPResponse *response; @property(nonatomic, assign) std::string *failure; +@property(nonatomic, assign) bool *timedOut; @property(nonatomic, strong) dispatch_semaphore_t semaphore; @property(nonatomic, assign) BOOL hasMaximumResponseSize; @property(nonatomic, assign) std::size_t maximumResponseSize; @@ -90,6 +91,11 @@ - (void)URLSession:(NSURLSession *)session if (self.failure->empty()) { if (error != nil) { self.failure->assign([error.localizedDescription UTF8String]); + // NSURLSession reports an expired timeoutIntervalForRequest and an + // expired timeoutIntervalForResource with the same code, so which of + // the two bounds elapsed cannot be recovered here + *self.timedOut = [error.domain isEqualToString:NSURLErrorDomain] && + error.code == NSURLErrorTimedOut; } else if (![task.response isKindOfClass:[NSHTTPURLResponse class]]) { self.failure->assign("The response is not an HTTP response"); } else { @@ -124,6 +130,7 @@ - (void)URLSession:(NSURLSession *)session // terminate the process, so failures are recorded here and thrown // from the calling thread once the request settles std::string failure; + bool timed_out{false}; @autoreleasepool { NSURL *target{[NSURL URLWithString:to_nsstring(this->url_)]}; @@ -164,6 +171,7 @@ - (void)URLSession:(NSURLSession *)session [[SourcemetaCoreHTTPDelegate alloc] init]}; delegate.response = &response; delegate.failure = &failure; + delegate.timedOut = &timed_out; delegate.semaphore = dispatch_semaphore_create(0); delegate.hasMaximumResponseSize = this->maximum_response_size_.has_value() ? YES : NO; @@ -185,6 +193,10 @@ - (void)URLSession:(NSURLSession *)session } if (!failure.empty()) { + if (timed_out) { + throw HTTPTimeoutError{this->method_, this->url_, this->timeout_}; + } + throw HTTPError{this->method_, this->url_, failure}; } diff --git a/vendor/core/src/core/http/client_windows.cc b/vendor/core/src/core/http/client_windows.cc index 44a261f41..ff514de74 100644 --- a/vendor/core/src/core/http/client_windows.cc +++ b/vendor/core/src/core/http/client_windows.cc @@ -48,6 +48,21 @@ auto to_winhttp_timeout(const std::chrono::milliseconds value) -> int { return static_cast(value.count()); } +// WinHTTP reports an expired resolution, connection, send, or receive bound +// from WinHttpSetTimeouts with the same error, so which of the configured +// bounds elapsed cannot be recovered here +[[noreturn]] auto +throw_transfer_failure(const sourcemeta::core::HTTPMethod method, + const std::string &url, + const std::chrono::milliseconds timeout, + const DWORD error, const char *message) -> void { + if (error == ERROR_WINHTTP_TIMEOUT) { + throw sourcemeta::core::HTTPTimeoutError{method, url, timeout}; + } + + throw sourcemeta::core::HTTPError{method, url, message}; +} + class WinHTTPHandle { public: WinHTTPHandle(const HINTERNET handle) : handle_{handle} {} @@ -224,15 +239,19 @@ auto HTTPSystemRequest::send() const -> HTTPResponse { : request_headers.c_str(), request_headers.empty() ? 0 : static_cast(request_headers.size()), body_data, body_size, body_size, 0)}; + // The last error is captured before wiping the serialised headers, so the + // classification never reads a code some later call has replaced + const auto send_error{sent ? ERROR_SUCCESS : GetLastError()}; secure_zero(request_headers.data(), request_headers.size() * sizeof(wchar_t)); if (!sent) { - throw HTTPError{this->method_, this->url_, - "Failed to send the HTTP request"}; + throw_transfer_failure(this->method_, this->url_, this->timeout_, + send_error, "Failed to send the HTTP request"); } if (!WinHttpReceiveResponse(request_handle.get(), nullptr)) { - throw HTTPError{this->method_, this->url_, - "Failed to receive the HTTP response"}; + throw_transfer_failure(this->method_, this->url_, this->timeout_, + GetLastError(), + "Failed to receive the HTTP response"); } DWORD status_code{0}; @@ -252,8 +271,9 @@ auto HTTPSystemRequest::send() const -> HTTPResponse { while (true) { DWORD available{0}; if (!WinHttpQueryDataAvailable(request_handle.get(), &available)) { - throw HTTPError{this->method_, this->url_, - "Failed to read the HTTP response body"}; + throw_transfer_failure(this->method_, this->url_, this->timeout_, + GetLastError(), + "Failed to read the HTTP response body"); } if (available == 0) { @@ -273,8 +293,9 @@ auto HTTPSystemRequest::send() const -> HTTPResponse { DWORD read{0}; if (!WinHttpReadData(request_handle.get(), response.body.data() + offset, available, &read)) { - throw HTTPError{this->method_, this->url_, - "Failed to read the HTTP response body"}; + throw_transfer_failure(this->method_, this->url_, this->timeout_, + GetLastError(), + "Failed to read the HTTP response body"); } response.body.resize(offset + read); diff --git a/vendor/core/src/core/http/continue_traceparent.cc b/vendor/core/src/core/http/continue_traceparent.cc new file mode 100644 index 000000000..2064afe82 --- /dev/null +++ b/vendor/core/src/core/http/continue_traceparent.cc @@ -0,0 +1,52 @@ +#include +#include + +#include // std::ranges::copy +#include // std::array +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::span +#include // std::string_view + +#include "entropy.h" + +namespace sourcemeta::core { + +auto http_continue_traceparent(const std::string_view value, const bool sampled) + -> std::optional> { + // The value that arrived is the only source for the trace, so one that does + // not parse leaves nothing to carry on from + if (!http_is_traceparent(value)) { + return std::nullopt; + } + + std::array parent{}; + http_draw_identifier(parent); + + // The revision is the one this library implements rather than the one that + // was read, since a value is reconstructed "according to the highest version + // of the specification known to the implementation", and whatever a later + // one appended goes with it + // https://www.w3.org/TR/trace-context/#versioning-of-traceparent + std::array result{}; + result[0] = '0'; + result[1] = '0'; + result[2] = '-'; + + // The trace carries on, so its identifier is the one that arrived + std::ranges::copy(value.substr(3, 32), result.begin() + 3); + result[35] = '-'; + + // The operation is this one rather than the caller's, and drawing a fresh + // identifier is also what lets the sampling decision differ from the one + // that arrived: "The parent-id field MUST be set to a new value with the + // sampled flag update" + // https://www.w3.org/TR/trace-context/#mutating-the-traceparent-field + bytes_to_hex(parent, std::span{result}.subspan(36, 16)); + result[52] = '-'; + result[53] = '0'; + result[54] = sampled ? '1' : '0'; + return result; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/entropy.h b/vendor/core/src/core/http/entropy.h new file mode 100644 index 000000000..5dd7c1add --- /dev/null +++ b/vendor/core/src/core/http/entropy.h @@ -0,0 +1,33 @@ +#ifndef SOURCEMETA_CORE_HTTP_ENTROPY_H_ +#define SOURCEMETA_CORE_HTTP_ENTROPY_H_ + +#include + +#include // std::ranges::all_of +#include // std::uint8_t +#include // std::span +#include // std::runtime_error + +namespace sourcemeta::core { + +// An identifier of all zeroes is invalid, so a draw that lands on one cannot +// be written out. Taking the draw again covers that, and giving up after a +// few attempts reports a provider that only ever returns nothing rather than +// trusting it or spinning on it +// https://www.w3.org/TR/trace-context/#trace-id +inline auto http_draw_identifier(const std::span bytes) -> void { + for (auto attempt{0}; attempt < 8; ++attempt) { + random_bytes(bytes); + if (!std::ranges::all_of( + bytes, [](const std::uint8_t byte) { return byte == 0; })) { + return; + } + } + + throw std::runtime_error{ + "Could not draw a non-zero trace context identifier"}; +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/http/from_date.cc b/vendor/core/src/core/http/from_date.cc index c2ff17f10..af74a0dca 100644 --- a/vendor/core/src/core/http/from_date.cc +++ b/vendor/core/src/core/http/from_date.cc @@ -7,7 +7,7 @@ namespace sourcemeta::core { -auto http_from_date(const std::string_view value) noexcept +auto http_from_date(const std::string_view value) -> std::optional { if (const auto result{from_imf_fixdate(value)}; result.has_value()) { return result; diff --git a/vendor/core/src/core/http/helpers.h b/vendor/core/src/core/http/helpers.h index 18cbc07da..999432010 100644 --- a/vendor/core/src/core/http/helpers.h +++ b/vendor/core/src/core/http/helpers.h @@ -7,6 +7,7 @@ #include // assert #include // std::size_t #include // std::uint8_t, std::uint16_t +#include // std::span #include // std::string_view #include // std::pair @@ -130,9 +131,11 @@ inline auto http_for_each_parameter(const std::string_view parameters, Visitor visit) -> void { std::size_t position{0}; while (position < parameters.size()) { - if (parameters[position] == ';') { - ++position; - } + // Splitting an entry from its parameters keeps the semicolon with them, + // and the scan of each parameter ends on the next one, so one always + // stands where a parameter begins + assert(parameters[position] == ';'); + ++position; while (position < parameters.size() && http_is_ows(parameters[position])) { ++position; } @@ -339,9 +342,8 @@ inline auto http_split_media_range(const std::string_view parameters) noexcept float quality{1.0F}; while (position < parameters.size()) { const std::size_t separator{position}; - if (parameters[position] == ';') { - ++position; - } + assert(parameters[position] == ';'); + ++position; while (position < parameters.size() && http_is_ows(parameters[position])) { ++position; } diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http.h b/vendor/core/src/core/http/include/sourcemeta/core/http.h index 6b439d5c8..81f410059 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http.h @@ -7,6 +7,7 @@ // NOLINTBEGIN(misc-include-cleaner) #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include // NOLINTEND(misc-include-cleaner) #include // std::chrono::system_clock @@ -232,7 +234,7 @@ auto http_negotiate_encoding( /// "Sun Nov 6 08:49:37 1994").has_value()); /// ``` SOURCEMETA_CORE_HTTP_EXPORT -auto http_from_date(const std::string_view value) noexcept +auto http_from_date(const std::string_view value) -> std::optional; /// @ingroup http diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_baggage.h b/vendor/core/src/core/http/include/sourcemeta/core/http_baggage.h new file mode 100644 index 000000000..62b5b8f4c --- /dev/null +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_baggage.h @@ -0,0 +1,232 @@ +#ifndef SOURCEMETA_CORE_HTTP_BAGGAGE_H_ +#define SOURCEMETA_CORE_HTTP_BAGGAGE_H_ + +#ifndef SOURCEMETA_CORE_HTTP_EXPORT +#include +#endif + +#include +#include + +#include // assert +#include // std::invocable +#include // std::optional +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup http +/// Whether the input is a well-formed baggage value, which is the part of a +/// member after its equals sign. The grammar is in W3C Baggage Section 3.3.1, +/// and this is stricter than it on one point, noted below. +/// +/// The permitted octets leave out the control characters, whitespace, the +/// double quote, the comma, the semicolon and the backslash, so a value +/// carrying any of those has to percent-encode it. This also requires that a +/// percent introduces an encoded octet, which is stricter than the grammar, +/// where the percent is an octet like any other. The prose is what forbids a +/// bare one. An empty value is well formed, where a tracestate value has to +/// end in a mandatory non-blank character and so cannot be empty. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_is_baggage_value("alice")); +/// assert(sourcemeta::core::http_is_baggage_value("DF%2028")); +/// assert(sourcemeta::core::http_is_baggage_value("")); +/// assert(!sourcemeta::core::http_is_baggage_value("DF 28")); +/// assert(!sourcemeta::core::http_is_baggage_value("100%")); +/// ``` +/// +/// @see https://www.w3.org/TR/baggage/#value +/// @see https://www.w3.org/TR/trace-context/#value +SOURCEMETA_CORE_HTTP_EXPORT +auto http_is_baggage_value(const std::string_view value) noexcept -> bool; + +/// @ingroup http +/// Whether the input is a well-formed baggage field value. The grammar is in +/// W3C Baggage Section 3.3.1, and whitespace at the two outer ends of the +/// input is accepted although the list production does not place any there, +/// since a field value does not carry such whitespace to begin with. +/// +/// A member is a key and a value, optionally trailed by a semicolon-delimited +/// property set whose own entries may be a bare key. Whitespace around a key +/// or a value is allowed and is not part of it. A repeated key is allowed, +/// since "Uniqueness of keys between multiple list-members in a +/// baggage-string is not guaranteed", where tracestate holds that "Only one +/// entry per key is allowed". +/// +/// The grammar caps the list at 180 members. The separate figures of 64 +/// members and 8192 bytes are the point past which a platform "MAY drop +/// list-members" rather than a bound on what is well formed, so they are not +/// checked here. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_is_baggage( +/// "userId=alice,serverNode=DF%2028,isProduction=false")); +/// assert(sourcemeta::core::http_is_baggage("key1=value1;property1;k=v")); +/// assert(sourcemeta::core::http_is_baggage("userId = alice")); +/// assert(sourcemeta::core::http_is_baggage("a=1,a=2")); +/// assert(!sourcemeta::core::http_is_baggage("")); +/// assert(!sourcemeta::core::http_is_baggage("novalue")); +/// ``` +/// +/// @see https://www.w3.org/TR/baggage/#definition +/// @see https://www.w3.org/TR/baggage/#baggage-string +/// @see https://www.w3.org/TR/baggage/#limits +/// @see https://www.w3.org/TR/trace-context/#combined-header-value +/// @see https://www.rfc-editor.org/rfc/rfc9110#section-5.5 +SOURCEMETA_CORE_HTTP_EXPORT +auto http_is_baggage(const std::string_view value) noexcept -> bool; + +/// @ingroup http +/// Visit each baggage member in order as a key, a value, and the property set +/// trailing it, which is empty when the member carries none. The value is +/// handed over still encoded, since decoding it is a separate concern with a +/// rule of its own: "percent-encoded octet sequences that do not match the +/// UTF-8 encoding scheme MUST be replaced with the replacement code point +/// (U+FFFD)". The whole input is +/// checked before anything is visited, so a caller never acts on part of a +/// malformed value, which for baggage means never propagating a pair out of a +/// value it then discards. +/// +/// A key may repeat and the order of members is significant, so a caller +/// collecting these wants a sequence rather than a map. Every view borrows the +/// input, so the value they came from must outlive them. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::string flat; +/// assert(sourcemeta::core::http_parse_baggage( +/// "userId=alice,serverNode=DF%2028", +/// [&flat](const auto key, const auto value, const auto properties) { +/// flat += std::string{key} + "=" + std::string{value} + "[" + +/// std::string{properties} + "];"; +/// })); +/// assert(flat == "userId=alice[];serverNode=DF%2028[];"); +/// ``` +/// +/// @see https://www.w3.org/TR/baggage/#definition +/// @see https://www.w3.org/TR/baggage/#value +template + requires std::invocable +inline auto http_parse_baggage(const std::string_view value, Callback callback) + -> bool { + if (!http_is_baggage(value)) { + return false; + } + + auto rest{value}; + while (true) { + const auto comma{rest.find(',')}; + const auto member{trim(rest.substr(0, comma), http_is_ows)}; + const auto semicolon{member.find(';')}; + const auto pair{trim(member.substr(0, semicolon), http_is_ows)}; + const auto equal{pair.find('=')}; + + // Every member holds an equals sign, which the check above has settled, so + // the position below is never the one that stands for not found + assert(equal != std::string_view::npos); + callback(trim(pair.substr(0, equal), http_is_ows), + trim(pair.substr(equal + 1), http_is_ows), + semicolon == std::string_view::npos + ? member.substr(member.size()) + : member.substr(semicolon + 1)); + if (comma == std::string_view::npos) { + return true; + } + + rest.remove_prefix(comma + 1); + } +} + +/// @ingroup http +/// Visit each entry of a baggage property set in order, as a key and a value +/// that is absent when the entry is a bare key. That distinction is why the +/// value is optional rather than empty: the grammar admits an entry with no +/// value at all and an entry whose value is the empty string, and they are not +/// the same thing. +/// +/// The input is the property set handed over when visiting a member, which is +/// what follows the first semicolon and so does not include it. An empty set +/// is a set of no entries and parses. Note that the specification writes a set +/// with that leading semicolon still attached, where this does not take one. +/// +/// The input is checked before anything is visited, and nothing is visited +/// when it does not parse. Every view borrows the input. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::string flat; +/// assert(sourcemeta::core::http_parse_baggage_properties( +/// "property1;propertyKey=propertyValue", +/// [&flat](const auto key, const auto value) { +/// flat += std::string{key} + +/// (value.has_value() ? "=" + std::string{*value} : "") + ";"; +/// })); +/// assert(flat == "property1;propertyKey=propertyValue;"); +/// ``` +/// +/// @see https://www.w3.org/TR/baggage/#property +template + requires std::invocable> +inline auto http_parse_baggage_properties(const std::string_view properties, + Callback callback) -> bool { + // The repetition that carries properties may run zero times, so a member + // without any hands over nothing, and nothing is a set of no entries rather + // than a malformed one + // https://www.w3.org/TR/baggage/#definition + if (properties.empty()) { + return true; + } + + auto pending{properties}; + while (true) { + const auto semicolon{pending.find(';')}; + const auto entry{trim(pending.substr(0, semicolon), http_is_ows)}; + const auto equal{entry.find('=')}; + if (!http_is_token(trim(entry.substr(0, equal), http_is_ows)) || + (equal != std::string_view::npos && + !http_is_baggage_value(trim(entry.substr(equal + 1), http_is_ows)))) { + return false; + } + + if (semicolon == std::string_view::npos) { + break; + } + + pending.remove_prefix(semicolon + 1); + } + + auto rest{properties}; + while (true) { + const auto semicolon{rest.find(';')}; + const auto entry{trim(rest.substr(0, semicolon), http_is_ows)}; + const auto equal{entry.find('=')}; + callback(trim(entry.substr(0, equal), http_is_ows), + equal == std::string_view::npos + ? std::optional{} + : trim(entry.substr(equal + 1), http_is_ows)); + if (semicolon == std::string_view::npos) { + return true; + } + + rest.remove_prefix(semicolon + 1); + } +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_error.h b/vendor/core/src/core/http/include/sourcemeta/core/http_error.h index bfd64d03a..65344f135 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http_error.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_error.h @@ -8,6 +8,7 @@ #include #include +#include // std::chrono::milliseconds #include // std::uint16_t #include // std::runtime_error #include // std::string @@ -59,6 +60,39 @@ class SOURCEMETA_CORE_HTTP_EXPORT HTTPError : public std::runtime_error { std::string url_; }; +/// @ingroup http +/// An error for a request that did not complete within its configured +/// timeout. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const sourcemeta::core::HTTPTimeoutError error{ +/// sourcemeta::core::HTTPMethod::GET, +/// "https://example.com", std::chrono::seconds{30}}; +/// assert(error.timeout() == std::chrono::seconds{30}); +/// ``` +class SOURCEMETA_CORE_HTTP_EXPORT HTTPTimeoutError : public HTTPError { +public: + /// Construct an error from the request method, URL, and the total timeout + /// that was configured for the request + HTTPTimeoutError(const HTTPMethod method, std::string url, + const std::chrono::milliseconds timeout) + : HTTPError{method, std::move(url), "The HTTP request timed out"}, + timeout_{timeout} {} + + /// Get the total timeout that was configured for the request. A backend + /// does not report which of the configured bounds elapsed, so this is the + /// total timeout even when a narrower connection timeout was the one hit + [[nodiscard]] auto timeout() const noexcept -> std::chrono::milliseconds { + return this->timeout_; + } + +private: + std::chrono::milliseconds timeout_; +}; + /// @ingroup http /// An error for a response with an unsuccessful status code, owning a copy /// of the status data. For example: diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_message.h b/vendor/core/src/core/http/include/sourcemeta/core/http_message.h index 4168e3bae..de641e327 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http_message.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_message.h @@ -9,6 +9,7 @@ #include // std::nullopt, std::optional #include // std::string #include // std::string_view +#include // std::is_same_v #include // std::move namespace sourcemeta::core { @@ -397,26 +398,29 @@ inline auto http_serialize_headers(const Headers &headers) -> std::string { continue; } - // RFC 9112 §5.1 notes that "a single SP preceding the field line - // value is preferred for consistent readability by humans" + // A field value either carries its own bytes or already is the bytes, so + // which of the two it is settles only what gets read out of it here. A + // value that carries them hands back a view of storage it keeps, which is + // what lets the view below outlive the call that produced it + std::string_view bytes; if constexpr (requires { value.bytes(); }) { - if (http_field_line_has_forbidden_byte(value.bytes())) { - continue; - } - - result += name; - result += ": "; - result += value.bytes(); + static_assert(std::is_same_v, + "A field value must hand back a view of bytes it keeps, " + "as an owning result would leave nothing to write out"); + bytes = value.bytes(); } else { - if (http_field_line_has_forbidden_byte(value)) { - continue; - } + bytes = value; + } - result += name; - result += ": "; - result += value; + if (http_field_line_has_forbidden_byte(bytes)) { + continue; } + // RFC 9112 §5.1 notes that "a single SP preceding the field line + // value is preferred for consistent readability by humans" + result += name; + result += ": "; + result += bytes; result += "\r\n"; } diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_method.h b/vendor/core/src/core/http/include/sourcemeta/core/http_method.h index f9d5465fe..e5b6ff7a7 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http_method.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_method.h @@ -64,7 +64,11 @@ constexpr auto http_method_string(const HTTPMethod method) noexcept return "PATCH"; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } } // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_syntax.h b/vendor/core/src/core/http/include/sourcemeta/core/http_syntax.h index 135da803a..1d773853d 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http_syntax.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_syntax.h @@ -63,6 +63,31 @@ inline auto http_is_tchar(const char character) noexcept -> bool { } } +/// @ingroup http +/// Whether the character is one a field value may carry. RFC 9110 Section 5.5 +/// admits the visible characters and obsolete text, along with the spaces and +/// horizontal tabs between them, and nothing else: +/// "field-vchar = VCHAR / obs-text" +/// Every other control character is excluded, and a recipient of one of those +/// "MUST either reject the message or replace each of those characters with SP +/// before further processing". For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_is_field_octet('a')); +/// assert(sourcemeta::core::http_is_field_octet(' ')); +/// assert(sourcemeta::core::http_is_field_octet('\t')); +/// assert(!sourcemeta::core::http_is_field_octet('\n')); +/// ``` +/// +/// @see https://www.rfc-editor.org/rfc/rfc9110#section-5.5 +inline auto http_is_field_octet(const char character) noexcept -> bool { + const auto byte{static_cast(character)}; + return byte == 0x09 || (byte >= 0x20 && byte != 0x7F); +} + /// @ingroup http /// Whether a character may appear in the alphabet of a token68 (RFC 7235 /// Section 2.1) or b64token (RFC 6750 Section 2.1) credential, before its @@ -157,12 +182,11 @@ inline auto http_is_token(const std::string_view value) noexcept -> bool { /// ``` inline auto http_encode_quoted_string(const std::string_view value, std::string &sink) -> bool { + // RFC 9110 Section 5.6.4: quoted text and a quoted-pair admit what a field + // value admits, so a control character beyond the horizontal tab makes the + // value unencodable, carriage return and line feed among them for (const auto character : value) { - const auto byte{static_cast(character)}; - // RFC 9110 Section 5.6.4: qdtext and quoted-pair admit HTAB, SP through - // VCHAR, and obs-text, so every other control character (CR and LF among - // them) makes the value unencodable - if (byte != 0x09 && (byte < 0x20 || byte == 0x7F)) { + if (!http_is_field_octet(character)) { return false; } } diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_tracecontext.h b/vendor/core/src/core/http/include/sourcemeta/core/http_tracecontext.h new file mode 100644 index 000000000..705a1a8da --- /dev/null +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_tracecontext.h @@ -0,0 +1,298 @@ +#ifndef SOURCEMETA_CORE_HTTP_TRACECONTEXT_H_ +#define SOURCEMETA_CORE_HTTP_TRACECONTEXT_H_ + +#ifndef SOURCEMETA_CORE_HTTP_EXPORT +#include +#endif + +#include +#include + +#include // std::array +#include // std::invocable +#include // std::uint8_t +#include // std::optional +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup http +/// The fields of a traceparent field value. Every view borrows the input, so +/// the value these came from must outlive them. +/// +/// @see https://www.w3.org/TR/trace-context/#traceparent-header +struct HTTPTraceParent { + /// The revision of the field format this value was written to. Note that a + /// value being forwarded is not sent on under this revision but under "the + /// highest version of the specification known to the implementation", so a + /// higher one read here is reported rather than endorsed. + /// + /// @see https://www.w3.org/TR/trace-context/#versioning-of-traceparent + std::uint8_t version{0}; + /// Identifies the trace this request belongs to, as 32 lowercase hexadecimal + /// characters + std::string_view trace_id; + /// Identifies the operation that made this request, as 16 lowercase + /// hexadecimal characters + std::string_view parent_id; + /// An eight bit field of which one bit is currently defined + std::uint8_t flags{0}; +}; + +/// @ingroup http +/// Whether the sampled bit is set, which records that the caller may have +/// recorded trace data rather than that it did. The remaining bits are +/// reserved and a reader is told to mask rather than compare: "As this is a +/// bit field, you cannot interpret flags by decoding the hex value and looking +/// at the resulting number". For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_traceparent_sampled(0x01)); +/// assert(!sourcemeta::core::http_traceparent_sampled(0x00)); +/// assert(sourcemeta::core::http_traceparent_sampled(0x05)); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#trace-flags +/// @see https://www.w3.org/TR/trace-context/#sampled-flag +constexpr auto http_traceparent_sampled(const std::uint8_t flags) noexcept + -> bool { + return (flags & 0x01U) != 0; +} + +/// @ingroup http +/// Whether the input is a well-formed traceparent field value. The grammar in +/// W3C Trace Context Section 3.2.2 is written for version `00`, and a higher +/// version is admitted under the rules for reading one in Section 3.2.4. +/// +/// Version `00` is exactly 55 characters and admits nothing further. A later +/// version may append, since a reader is told to "check that the 2 characters +/// are either the end of the string or a dash", so a longer value must carry a +/// dash where the flags end. Whatever follows that dash is left +/// uninterpreted, because "Vendors MUST NOT parse or assume anything about +/// unknown fields for this version", but it is still held to the octets a +/// field value may carry, so a carriage return or a line feed there is +/// refused. Version `ff` is invalid, and an identifier of all zeroes is +/// "considered an invalid value". For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_is_traceparent( +/// "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")); +/// assert(!sourcemeta::core::http_is_traceparent( +/// "ff-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")); +/// assert(!sourcemeta::core::http_is_traceparent( +/// "00-00000000000000000000000000000000-b7ad6b7169203331-01")); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#traceparent-header-field-values +SOURCEMETA_CORE_HTTP_EXPORT +auto http_is_traceparent(const std::string_view value) noexcept -> bool; + +/// @ingroup http +/// Parse a traceparent field value into its fields, or nothing when it is not +/// well formed. The identifiers borrow the input. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto parent{sourcemeta::core::http_parse_traceparent( +/// "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")}; +/// assert(parent.has_value()); +/// assert(parent->version == 0); +/// assert(parent->trace_id == "0af7651916cd43dd8448eb211c80319c"); +/// assert(parent->parent_id == "b7ad6b7169203331"); +/// assert(sourcemeta::core::http_traceparent_sampled(parent->flags)); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#traceparent-header-field-values +SOURCEMETA_CORE_HTTP_EXPORT +auto http_parse_traceparent(const std::string_view value) noexcept + -> std::optional; + +/// @ingroup http +/// Generate a traceparent field value that starts a new trace, drawing both +/// identifiers from the random provider of the crypto module. This is what a +/// request that arrived without one needs, and it is also the mutation that +/// regenerates every property, which a service acting as "a front gate into +/// secure networks" performs to shed what it was handed. +/// +/// Note that the identifiers are only unguessable to the degree that provider +/// is, which varies with how the library was built, and that the requirement +/// on a generator is strict: "Random number generators MUST NOT rely on any +/// information that can potentially be user-identifiable". For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto value{sourcemeta::core::http_make_traceparent(true)}; +/// const std::string_view view{value.data(), value.size()}; +/// assert(view.size() == 55); +/// assert(sourcemeta::core::http_is_traceparent(view)); +/// assert(view.starts_with("00-")); +/// assert(view.ends_with("-01")); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#mutating-the-traceparent-field +/// @see https://www.w3.org/TR/trace-context/#randomness-of-trace-id +/// @see https://www.w3.org/TR/trace-context/#privacy-of-traceparent-field +SOURCEMETA_CORE_HTTP_EXPORT +auto http_make_traceparent(const bool sampled) -> std::array; + +/// @ingroup http +/// Generate the traceparent field value to send onward for a request that +/// arrived carrying one. The trace continues, so its identifier is kept, while +/// the operation is this one rather than the caller's, which is the mutation +/// the specification calls "the most typical" and says "should be considered a +/// default". +/// +/// The input is the field value that arrived rather than its parsed fields, so +/// that what is carried over is known to be well formed, and nothing comes +/// back for one that does not parse. +/// +/// The sampling decision is the one being made here rather than the one that +/// arrived, since the flag "reflects the caller's recording behavior" and the +/// caller is now this service. Read the arriving decision off the parsed flags +/// to pass it on unchanged. A value read at a later revision is written back +/// at the revision this library implements, which is the downgrade a reader of +/// one owes, and whatever that value appended is dropped with it. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto value{sourcemeta::core::http_continue_traceparent( +/// "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", true)}; +/// assert(value.has_value()); +/// const std::string_view view{value->data(), value->size()}; +/// assert(sourcemeta::core::http_is_traceparent(view)); +/// assert(view.substr(3, 32) == "0af7651916cd43dd8448eb211c80319c"); +/// assert(view.substr(36, 16) != "b7ad6b7169203331"); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#mutating-the-traceparent-field +/// @see https://www.w3.org/TR/trace-context/#versioning-of-traceparent +SOURCEMETA_CORE_HTTP_EXPORT +auto http_continue_traceparent(const std::string_view value, const bool sampled) + -> std::optional>; + +/// @ingroup http +/// Whether the input is a well-formed tracestate field value (RFC 5234 ABNF in +/// W3C Trace Context Section 3.3.1). +/// +/// The grammar is permissive in ways that surprise: at most 32 members, and +/// "Empty and whitespace-only list members are allowed. Vendors MUST accept +/// empty tracestate headers". Since "Spaces and horizontal tabs surrounding +/// list-members are ignored", a member padded on either side is accepted and +/// so is one made only of padding. Padding at the two outer ends of the whole +/// value is a little more than the list production writes down, and is +/// accepted on that same sentence. A member's value, in contrast, cannot be +/// empty. Note that baggage differs on every one of those points. +/// +/// This rejects a repeated key, which the grammar alone does not forbid, on +/// the rule that "Only one entry per key is allowed because the entry +/// represents that last position in the trace". A reader is offered the +/// alternative of dropping just the repeated entry, but only in a section +/// marked non-normative. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_is_tracestate( +/// "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE")); +/// assert(sourcemeta::core::http_is_tracestate("")); +/// assert(!sourcemeta::core::http_is_tracestate("foo=")); +/// assert(!sourcemeta::core::http_is_tracestate("a=1,a=2")); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#tracestate-header +SOURCEMETA_CORE_HTTP_EXPORT +auto http_is_tracestate(const std::string_view value) noexcept -> bool; + +/// @ingroup http +/// Visit each tracestate member in order as a key and a value, skipping the +/// empty members the grammar allows. The whole value is checked before +/// anything is visited, so a caller never acts on part of a malformed value, +/// and nothing is visited when it does not parse. Every key and value borrows +/// the input, so the value they came from must outlive them. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::string flat; +/// assert(sourcemeta::core::http_parse_tracestate( +/// "rojo=1 , , congo=2", +/// [&flat](const auto key, const auto value) { +/// flat += std::string{key} + "=" + std::string{value} + ";"; +/// })); +/// assert(flat == "rojo=1;congo=2;"); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#list +template + requires std::invocable +inline auto http_parse_tracestate(const std::string_view value, + Callback callback) -> bool { + if (!http_is_tracestate(value)) { + return false; + } + auto rest{value}; + while (true) { + const auto comma{rest.find(',')}; + const auto entry{trim(rest.substr(0, comma), http_is_ows)}; + const auto equal{entry.find('=')}; + if (equal != std::string_view::npos) { + callback(entry.substr(0, equal), entry.substr(equal + 1)); + } + if (comma == std::string_view::npos) { + return true; + } + rest.remove_prefix(comma + 1); + } +} + +/// @ingroup http +/// Collect each tracestate member in order into a container of pairs. Every +/// key and value borrows the input, so the value they came from must outlive +/// the container. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// #include +/// +/// std::vector> members; +/// assert(sourcemeta::core::http_parse_tracestate("rojo=1,congo=2", members)); +/// assert(members.size() == 2); +/// assert(members.front().first == "rojo"); +/// ``` +/// +/// @see https://www.w3.org/TR/trace-context/#list +template + requires requires(Container container, std::string_view entry) { + container.emplace_back(entry, entry); + } +inline auto http_parse_tracestate(const std::string_view value, + Container &members) -> bool { + return http_parse_tracestate( + value, + [&members](const std::string_view key, const std::string_view entry) + -> void { members.emplace_back(key, entry); }); +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/http/is_baggage.cc b/vendor/core/src/core/http/is_baggage.cc new file mode 100644 index 000000000..76ba4314a --- /dev/null +++ b/vendor/core/src/core/http/is_baggage.cc @@ -0,0 +1,84 @@ +#include +#include + +#include // std::size_t +#include // std::string_view + +namespace sourcemeta::core { + +auto http_is_baggage(const std::string_view value) noexcept -> bool { + std::size_t members{0}; + auto rest{value}; + while (true) { + // The list production is one member followed by at most 179 more, so 180 + // is the cap. The separate figures of 64 members and 8192 bytes are the + // point past which a platform "MAY drop list-members" rather than a bound + // on what is well formed, so they are left alone + // https://www.w3.org/TR/baggage/#definition + // https://www.w3.org/TR/baggage/#limits + if (members >= 180) { + return false; + } + + ++members; + const auto comma{rest.find(',')}; + auto member{trim(rest.substr(0, comma), http_is_ows)}; + + // A member carries its own pair and then any number of properties, and + // only the pair must hold an equals sign, since "Additional metadata MAY + // be appended to values in the form of property set" whose entries are + // "keys and/or key-value pairs". The list production offers no alternative + // for an empty member, so one is malformed, where tracestate allows one: + // "Empty and whitespace-only list members are allowed" + // https://www.w3.org/TR/baggage/#property + // https://www.w3.org/TR/baggage/#definition + // https://www.w3.org/TR/trace-context/#tracestate-header-field-values + bool pair{true}; + while (true) { + const auto semicolon{member.find(';')}; + const auto entry{trim(member.substr(0, semicolon), http_is_ows)}; + const auto equal{entry.find('=')}; + if (pair && equal == std::string_view::npos) { + return false; + } + + // "Leading and trailing whitespaces (OWS) are allowed and are not + // considered to be a part of the key", which is itself the token this + // module already knows. The list production brackets whitespace around + // each key and value but not around the whole field value, so stripping + // at the two outer ends accepts a little more than the grammar writes + // down. It costs nothing, since "A field value does not include leading + // or trailing whitespace" in the first place + // https://www.w3.org/TR/baggage/#key + // https://www.rfc-editor.org/rfc/rfc9110#section-5.5 + if (!http_is_token(trim(entry.substr(0, equal), http_is_ows))) { + return false; + } + + // Only the first equals sign separates, since a value may carry more of + // them: "Parsers MUST NOT assume that the equal sign is only used to + // separate key and value". An absent one is a property given as a bare + // key, which the pair itself is not allowed to be + // https://www.w3.org/TR/baggage/#value + if (equal != std::string_view::npos && + !http_is_baggage_value(trim(entry.substr(equal + 1), http_is_ows))) { + return false; + } + + pair = false; + if (semicolon == std::string_view::npos) { + break; + } + + member.remove_prefix(semicolon + 1); + } + + if (comma == std::string_view::npos) { + return true; + } + + rest.remove_prefix(comma + 1); + } +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/is_baggage_value.cc b/vendor/core/src/core/http/is_baggage_value.cc new file mode 100644 index 000000000..a4925753b --- /dev/null +++ b/vendor/core/src/core/http/is_baggage_value.cc @@ -0,0 +1,47 @@ +#include +#include + +#include // std::size_t +#include // std::string_view + +namespace { + +// The octet production runs in five alternatives, a single character and four +// ranges, which its own comment explains as the US-ASCII characters other than +// the control ones, whitespace, the double quote, the comma, the semicolon and +// the backslash. The four gaps between those alternatives are where the last +// four of those sit +// https://www.w3.org/TR/baggage/#definition +auto is_baggage_octet(const char character) noexcept -> bool { + const auto byte{static_cast(character)}; + return byte >= 0x21 && byte <= 0x7E && byte != '"' && byte != ',' && + byte != ';' && byte != '\\'; +} + +} // namespace + +namespace sourcemeta::core { + +auto http_is_baggage_value(const std::string_view value) noexcept -> bool { + for (std::size_t index{0}; index < value.size(); ++index) { + if (!is_baggage_octet(value[index])) { + return false; + } + + // The percent is an octet like any other as far as the grammar is + // concerned, so this is stricter than the grammar on the strength of the + // prose: "The percent code point (U+0025) MUST be percent-encoded" + // https://www.w3.org/TR/baggage/#value + if (value[index] == '%') { + if (!is_percent_triplet(value, index)) { + return false; + } + + index += 2; + } + } + + return true; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/is_traceparent.cc b/vendor/core/src/core/http/is_traceparent.cc new file mode 100644 index 000000000..a93a7c071 --- /dev/null +++ b/vendor/core/src/core/http/is_traceparent.cc @@ -0,0 +1,72 @@ +#include +#include + +#include // std::all_of +#include // std::size_t +#include // std::string_view + +namespace sourcemeta::core { + +auto http_is_traceparent(const std::string_view value) noexcept -> bool { + // The size test comes first so that the indexing below stays in range, as a + // view offers no character past its end, and the dashes sit where the four + // fields meet: "If the size of the header is shorter than 55 characters, the + // vendor should not parse the header and should restart the trace". A value + // carrying fields of a later version must put a dash where the flags end: + // "Vendors MUST check that the 2 characters are either the end of the string + // or a dash". The lowest version is the whole value, so it admits nothing + // beyond the flags + // https://www.w3.org/TR/trace-context/#versioning-of-traceparent + // + // What a later version appends past that dash is left uninterpreted, since + // the rules for reading one enumerate three checks and no more, and "Vendors + // MUST NOT parse or assume anything about unknown fields for this version". + // Reading nothing into those bytes is not the same as taking any byte at + // all, though: whatever they turn out to mean, they travel in a field value, + // and the octets one may hold are settled below + if (value.size() < 55 || value[2] != '-' || value[35] != '-' || + value[52] != '-' || (value.starts_with("00") && value.size() != 55) || + (value.size() > 55 && value[55] != '-')) { + return false; + } + + // "Version ff is invalid" + // https://www.w3.org/TR/trace-context/#version + if (value.starts_with("ff")) { + return false; + } + + // Every field is lowercase hexadecimal, including the version prefix, whose + // failure to parse is what tells a reader to start a new trace: + // "it's not 2 hex characters followed by a dash". The flags are read as a + // bit field rather than compared, so no value of them is rejected here + // https://www.w3.org/TR/trace-context/#traceparent-header-field-values + for (std::size_t index{0}; index < 55; ++index) { + if (index != 2 && index != 35 && index != 52 && + (!is_hex_digit(value[index]) || !is_lowercase(value[index]))) { + return false; + } + } + + // Holding the appended bytes to the octets of a field value says nothing + // about what they mean, so it leaves them as opaque as the rule above + // demands. "Field values containing CR, LF, or NUL characters are invalid + // and dangerous", and a recipient of one "MUST either reject the message or + // replace each of those characters with SP before further processing", of + // which refusing the value is the first + // https://www.rfc-editor.org/rfc/rfc9110#section-5.5 + const auto appended{value.substr(55)}; + if (!std::all_of(appended.begin(), appended.end(), http_is_field_octet)) { + return false; + } + + // "If the trace-id value is invalid (for example if it contains non-allowed + // characters or all zeros), vendors MUST ignore the traceparent", and the + // same holds of the eight byte identifier + // https://www.w3.org/TR/trace-context/#trace-id + // https://www.w3.org/TR/trace-context/#parent-id + return value.substr(3, 32).find_first_not_of('0') != std::string_view::npos && + value.substr(36, 16).find_first_not_of('0') != std::string_view::npos; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/is_tracestate.cc b/vendor/core/src/core/http/is_tracestate.cc new file mode 100644 index 000000000..51534312d --- /dev/null +++ b/vendor/core/src/core/http/is_tracestate.cc @@ -0,0 +1,146 @@ +#include +#include + +#include // std::all_of, std::find +#include // std::array +#include // std::size_t +#include // std::string_view + +namespace { + +// A key is either a plain identifier or a tenant and a system joined by an at +// sign, and the three differ in how long they may be and in what may start +// them: only a tenant admits a leading digit, where the other two require a +// lowercase letter +// https://www.w3.org/TR/trace-context/#key +auto key_part(const std::string_view identifier, const bool tenant, + const std::size_t maximum) noexcept -> bool { + if (identifier.empty() || identifier.size() > maximum || + !((identifier.front() >= 'a' && identifier.front() <= 'z') || + (tenant && sourcemeta::core::is_digit(identifier.front())))) { + return false; + } + return std::all_of( + identifier.begin() + 1, identifier.end(), [](const char character) { + return (character >= 'a' && character <= 'z') || + sourcemeta::core::is_digit(character) || character == '_' || + character == '-' || character == '*' || character == '/'; + }); +} + +// "There can be a maximum of 32 list-members in a list", which bounds both the +// count and the keys kept to compare against +// https://www.w3.org/TR/trace-context/#tracestate-header-field-values +constexpr std::size_t MAXIMUM_MEMBERS{32}; + +} // namespace + +namespace sourcemeta::core { + +auto http_is_tracestate(const std::string_view value) noexcept -> bool { + // A list holds at most 32 members, so the keys seen so far fit in a fixed + // array that a linear scan searches, which keeps this free of allocation + std::array keys; + std::size_t seen{0}; + std::size_t members{0}; + auto rest{value}; + while (true) { + // The limit counts the empty members too, since each is a list-member of + // its own + if (members >= MAXIMUM_MEMBERS) { + return false; + } + const auto comma{rest.find(',')}; + + // "Spaces and horizontal tabs surrounding list-members are ignored", so + // they are stripped from every member wherever it sits, and what is left + // of one made of nothing else is the empty member the next rule allows. + // The list production brackets only its inner boundaries, so stripping at + // the two outer ends accepts a little more than the grammar writes down. + // That is deliberate and costs nothing, since "A field value does not + // include leading or trailing whitespace" in the first place, and + // stripping can never turn one well-formed member into another, because a + // key cannot begin with a space or a tab and a value cannot end with one + // https://www.w3.org/TR/trace-context/#tracestate-header-field-values + // https://www.w3.org/TR/trace-context/#list + // https://www.rfc-editor.org/rfc/rfc9110#section-5.5 + const auto entry{trim(rest.substr(0, comma), http_is_ows)}; + + // "Empty and whitespace-only list members are allowed. Vendors MUST accept + // empty tracestate headers but SHOULD avoid sending them", which is there + // because "some vendors automatically inject whitespace after a comma + // separator, even in the case of an empty header". Baggage has no such + // alternative in its own member production, so a reader coming from that + // parser should not expect this + // https://www.w3.org/TR/trace-context/#tracestate-header-field-values + // https://www.w3.org/TR/baggage/#definition + if (!entry.empty()) { + const auto equal{entry.find('=')}; + if (equal == std::string_view::npos) { + return false; + } + const auto key{entry.substr(0, equal)}; + const auto content{entry.substr(equal + 1)}; + const auto at_position{key.find('@')}; + + // A simple key is one character plus 255, a tenant 1 plus 240 and a + // system 1 plus 13, which is where the three bounds come from + // https://www.w3.org/TR/trace-context/#key + const bool valid_key{ + at_position == std::string_view::npos + ? key_part(key, false, 256) + : key_part(key.substr(0, at_position), true, 241) && + key_part(key.substr(at_position + 1), false, 14)}; + if (!valid_key) { + return false; + } + + // "Only one entry per key is allowed because the entry represents that + // last position in the trace", which the grammar itself cannot say. The + // obligation that follows it is addressed to a producer and is not in + // capitals, so it is not a requirement in the sense this document + // reserves for those: "vendors must overwrite their entry upon reentry + // to their tracing system". A reader is offered the alternative of + // dropping the repeated entry instead of the whole value, which this + // declines so that a caller is told rather than quietly corrected, and + // which is only offered in a section marked non-normative. Baggage is + // the opposite, where "Uniqueness of keys between multiple list-members + // in a baggage-string is not guaranteed" + // https://www.w3.org/TR/trace-context/#combined-header-value + // https://www.w3.org/TR/trace-context/#a-traceparent-is-received + // https://www.w3.org/TR/baggage/#baggage-string + if (std::find(keys.begin(), keys.begin() + seen, key) != + keys.begin() + seen) { + return false; + } + keys[seen] = key; + ++seen; + + // "The value is an opaque string containing up to 256 printable ASCII + // [RFC0020] characters (i.e., the range 0x20 to 0x7E) except comma (,) + // and (=)", and it ends in a mandatory non-blank character, which + // stripping the surrounding whitespace has already settled. Baggage + // instead counts zero or more of its own octets, so it permits an empty + // value + // https://www.w3.org/TR/trace-context/#value + // https://www.w3.org/TR/baggage/#definition + if (content.empty() || content.size() > 256 || + !std::all_of(content.begin(), content.end(), + [](const char character) { + const auto byte{static_cast(character)}; + return byte >= 0x20 && byte <= 0x7E && + character != ',' && character != '='; + })) { + return false; + } + } + + ++members; + if (comma == std::string_view::npos) { + return true; + } + rest.remove_prefix(comma + 1); + } +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/make_traceparent.cc b/vendor/core/src/core/http/make_traceparent.cc new file mode 100644 index 000000000..49125413f --- /dev/null +++ b/vendor/core/src/core/http/make_traceparent.cc @@ -0,0 +1,37 @@ +#include +#include + +#include // std::array +#include // std::uint8_t +#include // std::span + +#include "entropy.h" + +namespace sourcemeta::core { + +auto http_make_traceparent(const bool sampled) -> std::array { + // Sixteen bytes identify the trace and eight the operation within it, and + // neither may be all zeroes, so the two are drawn separately + std::array trace{}; + std::array parent{}; + http_draw_identifier(trace); + http_draw_identifier(parent); + + std::array result{}; + result[0] = '0'; + result[1] = '0'; + result[2] = '-'; + bytes_to_hex(trace, std::span{result}.subspan(3, 32)); + result[35] = '-'; + bytes_to_hex(parent, std::span{result}.subspan(36, 16)); + result[52] = '-'; + + // Only the lowest bit carries a meaning, and the rest are left clear because + // "Vendors MUST set those to zero" + // https://www.w3.org/TR/trace-context/#other-flags + result[53] = '0'; + result[54] = sampled ? '1' : '0'; + return result; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/match_accept.cc b/vendor/core/src/core/http/match_accept.cc index e3e4d1364..fc8394527 100644 --- a/vendor/core/src/core/http/match_accept.cc +++ b/vendor/core/src/core/http/match_accept.cc @@ -22,9 +22,6 @@ auto http_match_accept(const std::string_view accept_header, std::string_view best{}; float best_quality{0.0F}; std::uint8_t best_specificity{0}; - std::size_t best_order{candidates.size()}; - - std::size_t order{0}; for (const auto candidate : candidates) { assert(!candidate.empty()); // RFC 9110 §12.5.1: a candidate may itself carry media-type parameters @@ -55,18 +52,16 @@ auto http_match_accept(const std::string_view accept_header, candidate_specificity = specificity; } }); + // A candidate only displaces one already held by being better, so the + // first of several equally good ones is the one kept if (candidate_quality > 0.0F && (candidate_quality > best_quality || (candidate_quality == best_quality && - candidate_specificity > best_specificity) || - (candidate_quality == best_quality && - candidate_specificity == best_specificity && order < best_order))) { + candidate_specificity > best_specificity))) { best = candidate; best_quality = candidate_quality; best_specificity = candidate_specificity; - best_order = order; } - ++order; } return best; } diff --git a/vendor/core/src/core/http/match_accept_language.cc b/vendor/core/src/core/http/match_accept_language.cc index d3050f7e5..96db1b817 100644 --- a/vendor/core/src/core/http/match_accept_language.cc +++ b/vendor/core/src/core/http/match_accept_language.cc @@ -45,9 +45,6 @@ auto http_match_accept_language( std::string_view best{}; float best_quality{0.0F}; std::size_t best_specificity{0}; - std::size_t best_order{candidates.size()}; - - std::size_t order{0}; for (const auto candidate : candidates) { assert(!candidate.empty()); assert(candidate.find_first_of(" \t,;*/") == std::string_view::npos); @@ -73,18 +70,16 @@ auto http_match_accept_language( candidate_specificity = specificity; } }); + // A candidate only displaces one already held by being better, so the + // first of several equally good ones is the one kept if (candidate_quality > 0.0F && (candidate_quality > best_quality || (candidate_quality == best_quality && - candidate_specificity > best_specificity) || - (candidate_quality == best_quality && - candidate_specificity == best_specificity && order < best_order))) { + candidate_specificity > best_specificity))) { best = candidate; best_quality = candidate_quality; best_specificity = candidate_specificity; - best_order = order; } - ++order; } return best; } diff --git a/vendor/core/src/core/http/parse_media_type.cc b/vendor/core/src/core/http/parse_media_type.cc index d9033c8f9..d7a6ba40d 100644 --- a/vendor/core/src/core/http/parse_media_type.cc +++ b/vendor/core/src/core/http/parse_media_type.cc @@ -2,6 +2,7 @@ #include "helpers.h" +#include // assert #include // std::size_t #include // std::optional, std::nullopt #include // std::string @@ -24,11 +25,14 @@ auto http_is_parameters(const std::string_view parameters) -> bool { std::string_view value; std::size_t position{0}; while (position < span.size()) { - while (position < span.size() && http_is_ows(span[position])) { + // The tail was trimmed with this very predicate, so a run of optional + // whitespace always stops before the end of the span and a byte follows it + while (http_is_ows(span[position])) { position += 1; + assert(position < span.size()); } - if (position >= span.size() || span[position] != ';') { + if (span[position] != ';') { return false; } diff --git a/vendor/core/src/core/http/parse_traceparent.cc b/vendor/core/src/core/http/parse_traceparent.cc new file mode 100644 index 000000000..c94d19dfb --- /dev/null +++ b/vendor/core/src/core/http/parse_traceparent.cc @@ -0,0 +1,39 @@ +#include +#include + +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::string_view + +namespace { + +auto byte_at(const std::string_view value, + const std::string_view::size_type position) noexcept + -> std::uint8_t { + return static_cast( + (sourcemeta::core::hex_digit_value(value[position]) << 4) | + sourcemeta::core::hex_digit_value(value[position + 1])); +} + +} // namespace + +namespace sourcemeta::core { + +auto http_parse_traceparent(const std::string_view value) noexcept + -> std::optional { + if (!http_is_traceparent(value)) { + return std::nullopt; + } + + // The fields sit at fixed offsets, since a later version keeps the first + // four where version 00 puts them and appends beyond the flags, which a + // reader is told to ignore: "Vendors MUST NOT parse or assume anything about + // unknown fields for this version" + // https://www.w3.org/TR/trace-context/#versioning-of-traceparent + return HTTPTraceParent{.version = byte_at(value, 0), + .trace_id = value.substr(3, 32), + .parent_id = value.substr(36, 16), + .flags = byte_at(value, 53)}; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/scan_quoted_string.cc b/vendor/core/src/core/http/scan_quoted_string.cc index c0da05909..a75c1d42b 100644 --- a/vendor/core/src/core/http/scan_quoted_string.cc +++ b/vendor/core/src/core/http/scan_quoted_string.cc @@ -5,19 +5,6 @@ #include // std::string #include // std::string_view -namespace { - -// RFC 9110 Section 5.6.4: qdtext and the escaped octet of a quoted-pair both -// admit HTAB, SP, the visible characters, and obs-text, so every other control -// character is rejected. The double quote and the backslash are handled by the -// caller before this check -auto http_is_quoted_content(const char character) noexcept -> bool { - const auto byte{static_cast(character)}; - return byte == 0x09 || (byte >= 0x20 && byte != 0x7f); -} - -} // namespace - namespace sourcemeta::core { auto http_scan_quoted_string(const std::string_view input, @@ -28,14 +15,16 @@ auto http_scan_quoted_string(const std::string_view input, return std::nullopt; } + // RFC 9110 Section 5.6.4: quoted text and the escaped octet of a quoted-pair + // both admit what a field value admits, less the double quote and the + // backslash, and those two are what this scan is tracking anyway const std::size_t content_start{position + 1}; std::size_t scan{content_start}; bool escaped{false}; while (scan < input.size()) { const char character{input[scan]}; if (character == '\\') { - if (scan + 1 >= input.size() || - !http_is_quoted_content(input[scan + 1])) { + if (scan + 1 >= input.size() || !http_is_field_octet(input[scan + 1])) { return std::nullopt; } @@ -67,7 +56,7 @@ auto http_scan_quoted_string(const std::string_view input, return scan + 1; } - if (!http_is_quoted_content(character)) { + if (!http_is_field_octet(character)) { return std::nullopt; } diff --git a/vendor/core/src/core/http/serialize_cache_control.cc b/vendor/core/src/core/http/serialize_cache_control.cc index 65687ff32..adb148e5a 100644 --- a/vendor/core/src/core/http/serialize_cache_control.cc +++ b/vendor/core/src/core/http/serialize_cache_control.cc @@ -68,7 +68,11 @@ auto visibility_directive( return "private"; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } // RFC 9110 §5.1: field-name = token, so a name outside that set could not be diff --git a/vendor/core/src/core/http/serialize_cookie.cc b/vendor/core/src/core/http/serialize_cookie.cc index e0d3e06c6..743f1fdd6 100644 --- a/vendor/core/src/core/http/serialize_cookie.cc +++ b/vendor/core/src/core/http/serialize_cookie.cc @@ -81,7 +81,11 @@ auto same_site_token(const sourcemeta::core::HTTPCookieSameSite value) noexcept return COOKIE_SAME_SITE_NONE; } + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) std::unreachable(); +#endif } auto required_size(const sourcemeta::core::HTTPCookie &cookie, diff --git a/vendor/core/src/core/idna/idna.cc b/vendor/core/src/core/idna/idna.cc index 3abb2d051..304a4ecfe 100644 --- a/vendor/core/src/core/idna/idna.cc +++ b/vendor/core/src/core/idna/idna.cc @@ -5,6 +5,7 @@ #include #include // std::ranges::lower_bound +#include // assert #include // std::size_t #include // std::optional, std::nullopt #include // std::string, std::u32string @@ -67,11 +68,10 @@ auto validate_a_label_body(const std::string_view encoded, // RFC 5891 §4.2.1: "verify that the A-label produced by the step in // Section 4.4 matches the one provided as input", so re-encoding the decoded // U-label must yield the lowercased bytes we decoded from - try { - return utf32_to_punycode(decoded) == body; - } catch (const PunycodeError &) { - return false; - } + // The A-label this decoded came from is held to the octet count a label may + // carry, which leaves what is re-encoded far shorter than what the position + // counters of the encoding could run past, so the encoding always answers + return utf32_to_punycode(decoded) == body; } } // namespace @@ -408,10 +408,15 @@ auto idna_passes_bidi_rule(const std::u32string_view label) noexcept -> bool { } // RFC 5893 §2 conditions 3 and 6: the label must end with a specific - // class, optionally followed by NSM characters. - for (std::size_t index = label.size(); index-- > 0;) { + // class, optionally followed by NSM characters. The first character was held + // to L, R, or AL above and none of those is an NSM, so walking back from the + // end always reaches a character that answers, which is why the walk is + // spelled without an exit of its own + for (std::size_t index{label.size()};;) { + index -= 1; const auto class_value{bidi_class(label[index])}; if (class_value == BidiClass::NonspacingMark) { + assert(index > 0); continue; } if (is_rtl_label) { @@ -423,8 +428,6 @@ auto idna_passes_bidi_rule(const std::u32string_view label) noexcept -> bool { return class_value == BidiClass::LeftToRight || class_value == BidiClass::EuropeanNumber; } - - return false; } auto idna_is_valid_a_label(const std::string_view label) -> bool { diff --git a/vendor/core/src/core/ip/ipv6.cc b/vendor/core/src/core/ip/ipv6.cc index d16064804..ac0863a5c 100644 --- a/vendor/core/src/core/ip/ipv6.cc +++ b/vendor/core/src/core/ip/ipv6.cc @@ -4,6 +4,7 @@ #include "ip_helpers.h" #include // std::array +#include // assert #include // std::size_t #include // std::uint8_t, std::uint16_t #include // std::optional, std::nullopt @@ -86,9 +87,11 @@ auto is_ipv6(const std::string_view address) -> bool { continue; } position += 1; - if (position >= size) { - return false; - } + // An address whose last character is a colon is turned away above unless + // the compression takes the last two, and the branch over the + // compression is taken before this one, so a colon reached here always + // has a group after it + assert(position < size); } } diff --git a/vendor/core/src/core/jose/jose_jwks_provider.cc b/vendor/core/src/core/jose/jose_jwks_provider.cc index 528d4051e..001e81555 100644 --- a/vendor/core/src/core/jose/jose_jwks_provider.cc +++ b/vendor/core/src/core/jose/jose_jwks_provider.cc @@ -200,7 +200,7 @@ auto JWKSProvider::verify_at( refreshed = this->refetch_for_unknown_kid_locked(now); } - if (refreshed == nullptr || refreshed == snapshot) { + if (refreshed == nullptr) { return error; } diff --git a/vendor/core/src/core/json/construct.h b/vendor/core/src/core/json/construct.h index 731d3485d..e70b1869b 100644 --- a/vendor/core/src/core/json/construct.h +++ b/vendor/core/src/core/json/construct.h @@ -14,15 +14,26 @@ #include // std::size_t #include // std::uint64_t, std::uint32_t, std::uint8_t #include // std::reference_wrapper -#include // std::invalid_argument #include // std::string_view -#include // std::move +#include // std::move, std::unreachable #include // std::vector namespace sourcemeta::core { namespace internal { +// The four hexadecimal digits of a `\u` escape, read from the position given +// and leaving it on whatever follows them +inline auto parse_hex4(const char *&position) -> unsigned long { + unsigned long value{0}; + for (std::size_t index = 0; index < 4; index++) { + const auto digit{hex_digit_value(*position++)}; + value = (value << 4) | static_cast(digit < 0 ? 0 : digit); + } + + return value; +} + inline auto unescape_string(const char *data, const std::uint32_t length, const bool has_escape) -> JSON::String { JSON::String result; @@ -80,16 +91,6 @@ inline auto unescape_string(const char *data, const std::uint32_t length, result.push_back('\t'); break; case 'u': { - auto parse_hex4 = [](const char *&position) -> unsigned long { - unsigned long value{0}; - for (std::size_t index = 0; index < 4; index++) { - const auto digit{hex_digit_value(*position++)}; - value = (value << 4) | - static_cast(digit < 0 ? 0 : digit); - } - return value; - }; - auto code_point{parse_hex4(cursor)}; if (code_point >= 0xD800 && code_point <= 0xDBFF) { assert(cursor + 6 <= string_end); @@ -102,51 +103,51 @@ inline auto unescape_string(const char *data, const std::uint32_t length, result); break; } + // The scan accepted the escape, so what stands after the reverse solidus + // is one of the characters the grammar names default: - break; + std::unreachable(); } } return result; } +// A number literal that the scanner accepts can still name a value outside the +// range this module represents, and where it does the failure belongs at the +// literal rather than at the start of the document +inline auto construct_decimal(const std::string_view value, + const std::uint64_t line, + const std::uint64_t column) -> JSON { + try { + return JSON{Decimal{value}}; + } catch (const DecimalParseError &) { + throw JSONParseError(line, column); + } +} + inline auto construct_number(const char *data, const std::uint32_t length, const std::uint8_t flags, - const std::uint32_t significant_digits) -> JSON { + const std::uint32_t significant_digits, + const std::uint64_t line, + const std::uint64_t column) -> JSON { if ((flags & TAPE_FLAG_NUMBER_EXPONENT) != 0) { - try { - return JSON{Decimal{std::string_view{data, length}}}; - } catch (const DecimalParseError &) { - throw JSONParseError(1, 1); - } catch (const std::invalid_argument &) { - throw JSONParseError(1, 1); - } + return construct_decimal(std::string_view{data, length}, line, column); } if ((flags & TAPE_FLAG_NUMBER_DOT) != 0) { constexpr std::uint32_t MAX_SAFE_SIGNIFICANT_DIGITS{15}; if (significant_digits > MAX_SAFE_SIGNIFICANT_DIGITS) { - try { - return JSON{Decimal{std::string_view{data, length}}}; - } catch (const DecimalParseError &) { - throw JSONParseError(1, 1); - } catch (const std::invalid_argument &) { - throw JSONParseError(1, 1); - } + return construct_decimal(std::string_view{data, length}, line, column); } const std::string_view value{data, length}; - const auto double_result{sourcemeta::core::to_double(value)}; + const auto double_result{sourcemeta::core::to_double_exact(value)}; if (double_result.has_value()) { return JSON{double_result.value()}; } - try { - return JSON{Decimal{value}}; - } catch (const DecimalParseError &) { - throw JSONParseError(1, 1); - } catch (const std::invalid_argument &) { - throw JSONParseError(1, 1); - } + + return construct_decimal(value, line, column); } auto digit_length = length; @@ -154,62 +155,71 @@ inline auto construct_number(const char *data, const std::uint32_t length, digit_length--; } + // A literal of digits alone stands for an exponent of zero, which is inside + // the range the storage holds, so reading one back cannot fail the way a + // literal that carries an exponent of its own can if (digit_length <= 19) { const std::string_view value{data, length}; const auto int_result{sourcemeta::core::to_int64_t(value)}; if (int_result.has_value()) { return JSON{int_result.value()}; } - try { - return JSON{Decimal{value}}; - } catch (const DecimalParseError &) { - throw JSONParseError(1, 1); - } catch (const std::invalid_argument &) { - throw JSONParseError(1, 1); - } - } - try { - return JSON{Decimal{std::string_view{data, length}}}; - } catch (const DecimalParseError &) { - throw JSONParseError(1, 1); - } catch (const std::invalid_argument &) { - throw JSONParseError(1, 1); + return JSON{Decimal{value}}; } + + return JSON{Decimal{std::string_view{data, length}}}; } -inline auto post_column_for(const TapeEntry &entry) -> std::uint64_t { +// Every value that is not a container is read the same way wherever it stands, +// and the kind a number literal names is settled by reading it rather than by +// what the scan recorded +inline auto construct_leaf(const char *buffer, const TapeEntry &entry) -> JSON { switch (entry.type) { case TapeType::True: - return entry.column + 3; + return JSON{true}; case TapeType::False: - return entry.column + 4; + return JSON{false}; case TapeType::Null: - return entry.column + 3; + return JSON{nullptr}; case TapeType::String: - case TapeType::Key: - return entry.column + entry.length + 1; - case TapeType::Number: - return entry.column + entry.length - 1; + return JSON{ + unescape_string(buffer + entry.offset, entry.length, + (entry.flags & TAPE_FLAG_STRING_ESCAPE) != 0)}; + // A container is read by the walk itself rather than here, so what is left + // for this to answer for is a number default: - return entry.column; + assert(entry.type == TapeType::Number); + return construct_number(buffer + entry.offset, entry.length, entry.flags, + entry.count, entry.line, entry.column); } } +inline auto post_column_for(const TapeEntry &entry) -> std::uint64_t { + // A string carries the length of what stands between its quotation marks, so + // the mark closing it stands one past that, while every other value carries + // the length of the whole literal and so ends on its last character + if (entry.type == TapeType::String || entry.type == TapeType::Key) { + return entry.column + entry.length + 1; + } + + return entry.column + entry.length - 1; +} + } // namespace internal // NOLINTBEGIN(cppcoreguidelines-avoid-goto,bugprone-use-after-move) #define CALLBACK_PRE(value_type, entry_ref, context, index, property) \ if (callback) { \ - callback(JSON::ParsePhase::Pre, JSON::Type::value_type, (entry_ref).line, \ + callback(JSON::ParsePhase::Pre, value_type, (entry_ref).line, \ (entry_ref).column, context, index, property); \ } #define CALLBACK_POST(value_type, post_line, post_column) \ if (callback) { \ - callback(JSON::ParsePhase::Post, JSON::Type::value_type, post_line, \ - post_column, JSON::ParseContext::Root, 0, empty_property); \ + callback(JSON::ParsePhase::Post, value_type, post_line, post_column, \ + JSON::ParseContext::Root, 0, empty_property); \ } inline auto construct_json(const char *buffer, @@ -229,64 +239,33 @@ inline auto construct_json(const char *buffer, std::size_t tape_index{0}; static const JSON::String empty_property; - if (tape.empty()) { - throw JSONParseError(1, 1); - } + assert(!tape.empty()); const auto &entry{tape[tape_index]}; - switch (entry.type) { - case TapeType::True: - CALLBACK_PRE(Boolean, entry, JSON::ParseContext::Root, 0, empty_property); - CALLBACK_POST(Boolean, entry.line, internal::post_column_for(entry)); - output = JSON{true}; - return; - case TapeType::False: - CALLBACK_PRE(Boolean, entry, JSON::ParseContext::Root, 0, empty_property); - CALLBACK_POST(Boolean, entry.line, internal::post_column_for(entry)); - output = JSON{false}; - return; - case TapeType::Null: - CALLBACK_PRE(Null, entry, JSON::ParseContext::Root, 0, empty_property); - CALLBACK_POST(Null, entry.line, internal::post_column_for(entry)); - output = JSON{nullptr}; - return; - case TapeType::String: { - CALLBACK_PRE(String, entry, JSON::ParseContext::Root, 0, empty_property); - auto value{Result{internal::unescape_string( - buffer + entry.offset, entry.length, - (entry.flags & TAPE_FLAG_STRING_ESCAPE) != 0)}}; - CALLBACK_POST(String, entry.line, internal::post_column_for(entry)); - output = std::move(value); - return; - } - case TapeType::Number: { - auto value = internal::construct_number( - buffer + entry.offset, entry.length, entry.flags, entry.count); - if (value.is_integer()) { - CALLBACK_PRE(Integer, entry, JSON::ParseContext::Root, 0, - empty_property); - CALLBACK_POST(Integer, entry.line, internal::post_column_for(entry)); - } else if (value.is_decimal()) { - CALLBACK_PRE(Decimal, entry, JSON::ParseContext::Root, 0, - empty_property); - CALLBACK_POST(Decimal, entry.line, internal::post_column_for(entry)); - } else { - CALLBACK_PRE(Real, entry, JSON::ParseContext::Root, 0, empty_property); - CALLBACK_POST(Real, entry.line, internal::post_column_for(entry)); - } - output = std::move(value); - return; - } - case TapeType::ArrayStart: - CALLBACK_PRE(Array, entry, JSON::ParseContext::Root, 0, empty_property); - goto do_construct_array; - case TapeType::ObjectStart: - CALLBACK_PRE(Object, entry, JSON::ParseContext::Root, 0, empty_property); - goto do_construct_object; - default: - throw JSONParseError(1, 1); + if (entry.type == TapeType::ArrayStart) { + CALLBACK_PRE(JSON::Type::Array, entry, JSON::ParseContext::Root, 0, + empty_property); + goto do_construct_array; } + if (entry.type == TapeType::ObjectStart) { + CALLBACK_PRE(JSON::Type::Object, entry, JSON::ParseContext::Root, 0, + empty_property); + goto do_construct_object; + } + + output = internal::construct_leaf(buffer, entry); + if (callback) { + const auto value_type{output.type()}; + callback(JSON::ParsePhase::Pre, value_type, entry.line, entry.column, + JSON::ParseContext::Root, 0, empty_property); + callback(JSON::ParsePhase::Post, value_type, entry.line, + internal::post_column_for(entry), JSON::ParseContext::Root, 0, + empty_property); + } + + return; + /* * Construct an array */ @@ -323,7 +302,7 @@ do_construct_array: { assert(tape[tape_index].type == TapeType::ArrayEnd); const auto &end_entry{tape[tape_index]}; tape_index++; - CALLBACK_POST(Array, end_entry.line, end_entry.column); + CALLBACK_POST(JSON::Type::Array, end_entry.line, end_entry.column); goto do_construct_container_end; } @@ -335,89 +314,35 @@ do_construct_array_item: { assert(levels.back() == Container::Array); const auto &item_entry{tape[tape_index]}; - switch (item_entry.type) { - case TapeType::ArrayStart: - CALLBACK_PRE(Array, item_entry, JSON::ParseContext::Index, - frames.back().get().size(), empty_property); - goto do_construct_array; - case TapeType::ObjectStart: - CALLBACK_PRE(Object, item_entry, JSON::ParseContext::Index, - frames.back().get().size(), empty_property); - goto do_construct_object; - case TapeType::True: - CALLBACK_PRE(Boolean, item_entry, JSON::ParseContext::Index, - frames.back().get().size(), empty_property); - frames.back().get().push_back(JSON{true}); - tape_index++; - CALLBACK_POST(Boolean, item_entry.line, - internal::post_column_for(item_entry)); - goto do_construct_array_item_separator; - case TapeType::False: - CALLBACK_PRE(Boolean, item_entry, JSON::ParseContext::Index, - frames.back().get().size(), empty_property); - frames.back().get().push_back(JSON{false}); - tape_index++; - CALLBACK_POST(Boolean, item_entry.line, - internal::post_column_for(item_entry)); - goto do_construct_array_item_separator; - case TapeType::Null: - CALLBACK_PRE(Null, item_entry, JSON::ParseContext::Index, - frames.back().get().size(), empty_property); - frames.back().get().push_back(JSON{nullptr}); - tape_index++; - CALLBACK_POST(Null, item_entry.line, - internal::post_column_for(item_entry)); - goto do_construct_array_item_separator; - case TapeType::String: - CALLBACK_PRE(String, item_entry, JSON::ParseContext::Index, - frames.back().get().size(), empty_property); - frames.back().get().push_back(Result{internal::unescape_string( - buffer + item_entry.offset, item_entry.length, - (item_entry.flags & TAPE_FLAG_STRING_ESCAPE) != 0)}); - tape_index++; - CALLBACK_POST(String, item_entry.line, - internal::post_column_for(item_entry)); - goto do_construct_array_item_separator; - case TapeType::Number: { - const auto current_index{frames.back().get().size()}; - auto value = internal::construct_number( - buffer + item_entry.offset, item_entry.length, item_entry.flags, - item_entry.count); - if (value.is_integer()) { - CALLBACK_PRE(Integer, item_entry, JSON::ParseContext::Index, - current_index, empty_property); - } else if (value.is_decimal()) { - CALLBACK_PRE(Decimal, item_entry, JSON::ParseContext::Index, - current_index, empty_property); - } else { - CALLBACK_PRE(Real, item_entry, JSON::ParseContext::Index, current_index, - empty_property); - } - const auto value_type{value.type()}; - frames.back().get().push_back(std::move(value)); - tape_index++; - if (value_type == JSON::Type::Integer) { - CALLBACK_POST(Integer, item_entry.line, - internal::post_column_for(item_entry)); - } else if (value_type == JSON::Type::Decimal) { - CALLBACK_POST(Decimal, item_entry.line, - internal::post_column_for(item_entry)); - } else { - CALLBACK_POST(Real, item_entry.line, - internal::post_column_for(item_entry)); - } - goto do_construct_array_item_separator; - } - default: - throw JSONParseError(1, 1); + if (item_entry.type == TapeType::ArrayStart) { + CALLBACK_PRE(JSON::Type::Array, item_entry, JSON::ParseContext::Index, + frames.back().get().size(), empty_property); + goto do_construct_array; } + + if (item_entry.type == TapeType::ObjectStart) { + CALLBACK_PRE(JSON::Type::Object, item_entry, JSON::ParseContext::Index, + frames.back().get().size(), empty_property); + goto do_construct_object; + } + + const auto item_index{frames.back().get().size()}; + auto item_value{internal::construct_leaf(buffer, item_entry)}; + const auto item_type{item_value.type()}; + CALLBACK_PRE(item_type, item_entry, JSON::ParseContext::Index, item_index, + empty_property); + frames.back().get().push_back(std::move(item_value)); + tape_index++; + CALLBACK_POST(item_type, item_entry.line, + internal::post_column_for(item_entry)); + goto do_construct_array_item_separator; } do_construct_array_item_separator: if (tape[tape_index].type == TapeType::ArrayEnd) { const auto &end_entry{tape[tape_index]}; tape_index++; - CALLBACK_POST(Array, end_entry.line, end_entry.column); + CALLBACK_POST(JSON::Type::Array, end_entry.line, end_entry.column); goto do_construct_container_end; } @@ -459,7 +384,7 @@ do_construct_object: { assert(tape[tape_index].type == TapeType::ObjectEnd); const auto &end_entry{tape[tape_index]}; tape_index++; - CALLBACK_POST(Object, end_entry.line, end_entry.column); + CALLBACK_POST(JSON::Type::Object, end_entry.line, end_entry.column); goto do_construct_container_end; } @@ -489,108 +414,34 @@ do_construct_object_key: { do_construct_object_value: { const auto &value_entry{tape[tape_index]}; - switch (value_entry.type) { - case TapeType::ArrayStart: - goto do_construct_array; - case TapeType::ObjectStart: - goto do_construct_object; - case TapeType::True: - frames.back().get().assign_assume_new(std::move(key), JSON{true}, - key_hash); - if (callback) { - callback(JSON::ParsePhase::Pre, JSON::Type::Boolean, key_line, - key_column, JSON::ParseContext::Property, 0, - frames.back().get().as_object().back_key()); - } - tape_index++; - CALLBACK_POST(Boolean, value_entry.line, - internal::post_column_for(value_entry)); - goto do_construct_object_property_end; - case TapeType::False: - frames.back().get().assign_assume_new(std::move(key), JSON{false}, - key_hash); - if (callback) { - callback(JSON::ParsePhase::Pre, JSON::Type::Boolean, key_line, - key_column, JSON::ParseContext::Property, 0, - frames.back().get().as_object().back_key()); - } - tape_index++; - CALLBACK_POST(Boolean, value_entry.line, - internal::post_column_for(value_entry)); - goto do_construct_object_property_end; - case TapeType::Null: - frames.back().get().assign_assume_new(std::move(key), JSON{nullptr}, - key_hash); - if (callback) { - callback(JSON::ParsePhase::Pre, JSON::Type::Null, key_line, key_column, - JSON::ParseContext::Property, 0, - frames.back().get().as_object().back_key()); - } - tape_index++; - CALLBACK_POST(Null, value_entry.line, - internal::post_column_for(value_entry)); - goto do_construct_object_property_end; - case TapeType::String: - frames.back().get().assign_assume_new( - std::move(key), - Result{internal::unescape_string( - buffer + value_entry.offset, value_entry.length, - (value_entry.flags & TAPE_FLAG_STRING_ESCAPE) != 0)}, - key_hash); - if (callback) { - callback(JSON::ParsePhase::Pre, JSON::Type::String, key_line, - key_column, JSON::ParseContext::Property, 0, - frames.back().get().as_object().back_key()); - } - tape_index++; - CALLBACK_POST(String, value_entry.line, - internal::post_column_for(value_entry)); - goto do_construct_object_property_end; - case TapeType::Number: { - auto value = internal::construct_number( - buffer + value_entry.offset, value_entry.length, value_entry.flags, - value_entry.count); - const auto value_type{value.type()}; - frames.back().get().assign_assume_new(std::move(key), std::move(value), - key_hash); - if (callback) { - if (value_type == JSON::Type::Integer) { - callback(JSON::ParsePhase::Pre, JSON::Type::Integer, key_line, - key_column, JSON::ParseContext::Property, 0, - frames.back().get().as_object().back_key()); - } else if (value_type == JSON::Type::Decimal) { - callback(JSON::ParsePhase::Pre, JSON::Type::Decimal, key_line, - key_column, JSON::ParseContext::Property, 0, - frames.back().get().as_object().back_key()); - } else { - callback(JSON::ParsePhase::Pre, JSON::Type::Real, key_line, - key_column, JSON::ParseContext::Property, 0, - frames.back().get().as_object().back_key()); - } - } - tape_index++; - if (value_type == JSON::Type::Integer) { - CALLBACK_POST(Integer, value_entry.line, - internal::post_column_for(value_entry)); - } else if (value_type == JSON::Type::Decimal) { - CALLBACK_POST(Decimal, value_entry.line, - internal::post_column_for(value_entry)); - } else { - CALLBACK_POST(Real, value_entry.line, - internal::post_column_for(value_entry)); - } - goto do_construct_object_property_end; - } - default: - throw JSONParseError(1, 1); + if (value_entry.type == TapeType::ArrayStart) { + goto do_construct_array; + } + + if (value_entry.type == TapeType::ObjectStart) { + goto do_construct_object; + } + + auto value{internal::construct_leaf(buffer, value_entry)}; + const auto value_type{value.type()}; + frames.back().get().assign_assume_new(std::move(key), std::move(value), + key_hash); + if (callback) { + callback(JSON::ParsePhase::Pre, value_type, key_line, key_column, + JSON::ParseContext::Property, 0, + frames.back().get().as_object().back_key()); } + tape_index++; + CALLBACK_POST(value_type, value_entry.line, + internal::post_column_for(value_entry)); + goto do_construct_object_property_end; } do_construct_object_property_end: if (tape[tape_index].type == TapeType::ObjectEnd) { const auto &end_entry{tape[tape_index]}; tape_index++; - CALLBACK_POST(Object, end_entry.line, end_entry.column); + CALLBACK_POST(JSON::Type::Object, end_entry.line, end_entry.column); goto do_construct_container_end; } diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_value.h b/vendor/core/src/core/json/include/sourcemeta/core/json_value.h index 9b94f87af..712db24c8 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_value.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_value.h @@ -517,6 +517,66 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { return this->is_integer() || this->is_real() || this->is_decimal(); } + /// Check whether the number this document holds is exactly a value of the + /// IEEE 754-2019 binary64 interchange format, the double precision format of + /// that standard. A number that no binary significand reaches is not one of + /// its values however closely it sits. A number taken in as a native + /// floating point value is one of them already, while a number read from a + /// document is judged on what its digits denote, so every way of writing one + /// number agrees. The result of this method is undefined unless the JSON + /// instance holds a number. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::JSON half{sourcemeta::core::Decimal{"0.5"}}; + /// const sourcemeta::core::JSON tenth{sourcemeta::core::Decimal{"0.1"}}; + /// assert(half.is_double()); + /// assert(!tenth.is_double()); + /// assert(!sourcemeta::core::parse_json("3.14").is_double()); + /// assert(!sourcemeta::core::parse_json("314e-2").is_double()); + /// ``` + [[nodiscard]] SOURCEMETA_FORCEINLINE auto is_double() const noexcept -> bool { + assert(this->is_number()); + if (this->is_real()) { + return true; + } + if (this->is_integer()) { + return is_representable_as(this->to_integer()); + } + return this->to_decimal().is_double(); + } + + /// Check whether the number this document holds is exactly a value of the + /// IEEE 754-2019 binary32 interchange format, the single precision format of + /// that standard. As with the double precision format, a number read from a + /// document is judged on what its digits denote, so every way of writing one + /// number agrees. The result of this method is undefined unless the JSON + /// instance holds a number. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::JSON half{sourcemeta::core::Decimal{"0.5"}}; + /// const sourcemeta::core::JSON pi{3.14}; + /// assert(half.is_float()); + /// assert(!pi.is_float()); + /// assert(!sourcemeta::core::parse_json("3.14").is_float()); + /// assert(!sourcemeta::core::parse_json("314e-2").is_float()); + /// ``` + [[nodiscard]] SOURCEMETA_FORCEINLINE auto is_float() const noexcept -> bool { + assert(this->is_number()); + if (this->is_real()) { + return is_representable_as(this->to_real()); + } + if (this->is_integer()) { + return is_representable_as(this->to_integer()); + } + return this->to_decimal().is_float(); + } + /// Check if the input JSON document is either a positive integer or a /// positive real number. Zero is considered to be positive. For example: /// diff --git a/vendor/core/src/core/json/json.cc b/vendor/core/src/core/json/json.cc index bbb9e2ad1..37f32513d 100644 --- a/vendor/core/src/core/json/json.cc +++ b/vendor/core/src/core/json/json.cc @@ -8,7 +8,6 @@ #include "parser.h" #include "stringify.h" -#include // assert #include // std::uint64_t #include // std::filesystem #include // std::basic_istream @@ -16,16 +15,16 @@ #include // std::optional, std::nullopt #include // std::basic_ostream #include // std::conditional_t -#include // std::cmp_greater, std::move +#include // std::cmp_greater, std::move, std::unreachable #include // std::vector namespace sourcemeta::core { template -static auto internal_parse_json(const char *&cursor, const char *end, - std::uint64_t &line, std::uint64_t &column, - const JSON::ParseCallback &callback, - const bool track_positions, JSON &output) +static auto +internal_parse_json(const char *&cursor, const char *end, std::uint64_t &line, + std::uint64_t &column, const JSON::ParseCallback &callback, + [[maybe_unused]] const bool track_positions, JSON &output) -> std::conditional_t { const char *buffer_start{cursor}; // Tape entries address the input with 32-bit offsets and lengths, so a larger @@ -45,28 +44,42 @@ static auto internal_parse_json(const char *&cursor, const char *end, if constexpr (ShouldThrow) { if (callback || track_positions) { scan_json(cursor, end, buffer_start, line, column, tape); + construct_json(buffer_start, tape, callback, output); } else { - // Re-scan with position tracking on failure for a precise error message + // Re-scan with position tracking on failure for a precise error message. + // Turning what was read into a value fails over a literal that naming it + // alone cannot settle, such as one past the range that can be held, so + // the retry has to cover that second phase as well. Leaving it out of + // the retry reports every such failure at the start of the document. + // + // Counting positions all along would spare the retry, but it would + // charge every document that parses for what only a document that fails + // needs, and this is the path everything else goes through. So a failure + // reads the input a second time and builds a second time, which is twice + // the work on a document that is about to be thrown away. What is + // counted never decides what is accepted, so the second attempt fails in + // the same place as the first and differs only in being able to say + // where that place is try { scan_json(cursor, end, buffer_start, line, column, tape); + construct_json(buffer_start, tape, callback, output); } catch (const JSONParseError &) { cursor = buffer_start; tape.clear(); line = 1; column = 0; scan_json(cursor, end, buffer_start, line, column, tape); + construct_json(buffer_start, tape, callback, output); } } - construct_json(buffer_start, tape, callback, output); } else { // Both the scanning and the construction phases signal failure by throwing, - // so a single boundary around them reports either as no value + // so a single boundary around them reports either as no value. Nothing + // asks the overload that answers this way to count positions or to + // announce what it reads, so the cheaper scan is the only one it needs + assert(!callback && !track_positions); try { - if (callback || track_positions) { - scan_json(cursor, end, buffer_start, line, column, tape); - } else { - scan_json(cursor, end, buffer_start, line, column, tape); - } + scan_json(cursor, end, buffer_start, line, column, tape); construct_json(buffer_start, tape, callback, output); } catch (const JSONParseError &) { return false; @@ -313,11 +326,13 @@ auto operator<<(std::basic_ostream &stream, return stream << "array"; case sourcemeta::core::JSON::Type::Object: return stream << "object"; - default: - // Should never happen, but some compilers are not happy without this - assert(false); - return stream; } + + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) + std::unreachable(); +#endif } } // namespace sourcemeta::core diff --git a/vendor/core/src/core/json/json_value.cc b/vendor/core/src/core/json/json_value.cc index cc037e03c..7670c8a72 100644 --- a/vendor/core/src/core/json/json_value.cc +++ b/vendor/core/src/core/json/json_value.cc @@ -16,7 +16,7 @@ #include // std::invalid_argument #include // std::to_string #include // std::basic_string_view -#include // std::exchange, std::move +#include // std::exchange, std::move, std::unreachable #include // std::vector // Searching a run of hashes for one value tests many lanes at once on any @@ -893,10 +893,13 @@ auto JSON::operator-=(const JSON &substractive) -> JSON & { return 5; } - default: - assert(false); - return 0; } + + // Clang settles that no other value arrives, so only GCC and MSVC + // need to be told that the end of this function is never reached +#if !defined(__clang__) + std::unreachable(); +#endif } [[nodiscard]] auto JSON::divisible_by(const JSON &divisor) const -> bool { diff --git a/vendor/core/src/core/json/parser.h b/vendor/core/src/core/json/parser.h index 26a3910b7..3a73c67ec 100644 --- a/vendor/core/src/core/json/parser.h +++ b/vendor/core/src/core/json/parser.h @@ -447,6 +447,56 @@ inline auto scan_number(const std::uint64_t line, std::uint64_t &column, return facts; } +// What a container needs remembered while the scan walks its children +struct ContainerFrame { + std::size_t tape_index; + std::uint32_t child_count; +}; + +// A key and a string value differ in nothing but the entry each leaves behind, +// and the three places a string value can stand differ in nothing but where +// the scan goes next, so what all four do with what they read is spelled out +// once here +template +inline auto push_string(const char *&cursor, const char *end, + const char *buffer_start, std::uint64_t &line, + std::uint64_t &column, std::vector &tape) + -> void { + const auto start{static_cast(cursor - buffer_start)}; + const auto value_line{line}; + const auto value_column{column}; + const auto has_escape{scan_string(line, column, cursor, end)}; + tape.push_back( + {.type = Type, + .flags = has_escape ? TAPE_FLAG_STRING_ESCAPE : std::uint8_t{0}, + .offset = start, + .length = static_cast(cursor - buffer_start - start - 1), + .count = 0, + .line = value_line, + .column = value_column}); +} + +// Closing an array and closing an object differ in nothing but the entry each +// leaves behind, as both settle the count of what the container held before +// giving up the frame that was counting +template +inline auto push_container_end(const std::uint64_t line, + const std::uint64_t column, + std::vector &container_stack, + std::vector &tape) -> void { + assert(!container_stack.empty()); + auto &frame{container_stack.back()}; + tape[frame.tape_index].count = frame.child_count; + tape.push_back({.type = Type, + .flags = 0, + .offset = 0, + .length = 0, + .count = 0, + .line = line, + .column = column}); + container_stack.pop_back(); +} + } // namespace internal // NOLINTBEGIN(cppcoreguidelines-avoid-goto) @@ -456,14 +506,9 @@ inline auto scan_json(const char *&cursor, const char *end, const char *buffer_start, std::uint64_t &line, std::uint64_t &column, std::vector &tape) -> void { - struct ContainerFrame { - std::size_t tape_index; - std::uint32_t child_count; - }; - using CharT = JSON::Char; char character = 0; - std::vector container_stack; + std::vector container_stack; container_stack.reserve(32); internal::skip_whitespace(cursor, end, line, column); @@ -484,47 +529,44 @@ inline auto scan_json(const char *&cursor, const char *end, switch (character) { case internal::TOKEN_TRUE: internal::scan_true(line, column, cursor, end); - tape.push_back({.type = TapeType::True, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); + tape.push_back( + {.type = TapeType::True, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_TRUE.size()), + .count = 0, + .line = value_line, + .column = value_column}); return; case internal::TOKEN_FALSE: internal::scan_false(line, column, cursor, end); - tape.push_back({.type = TapeType::False, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); + tape.push_back( + {.type = TapeType::False, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_FALSE.size()), + .count = 0, + .line = value_line, + .column = value_column}); return; case internal::TOKEN_NULL: internal::scan_null(line, column, cursor, end); - tape.push_back({.type = TapeType::Null, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); - return; - case internal::TOKEN_STRING_QUOTE: { - const auto string_start{ - static_cast(cursor - buffer_start)}; - const auto string_has_escape{ - internal::scan_string(line, column, cursor, end)}; - const auto string_length{static_cast( - cursor - buffer_start - string_start - 1)}; tape.push_back( - {TapeType::String, - string_has_escape ? TAPE_FLAG_STRING_ESCAPE : std::uint8_t{0}, - string_start, string_length, 0, value_line, value_column}); + {.type = TapeType::Null, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_NULL.size()), + .count = 0, + .line = value_line, + .column = value_column}); + return; + case internal::TOKEN_STRING_QUOTE: + internal::push_string( + cursor, end, buffer_start, line, column, tape); return; - } case internal::TOKEN_ARRAY_BEGIN: goto do_scan_array; case internal::TOKEN_OBJECT_BEGIN: @@ -569,7 +611,7 @@ do_scan_array: { .count = 0, .line = line, .column = column}); - container_stack.push_back({start_index, 0}); + container_stack.push_back({.tape_index = start_index, .child_count = 0}); internal::skip_whitespace(cursor, end, line, column); if (cursor >= end) [[unlikely]] { @@ -625,47 +667,44 @@ do_scan_array: { goto do_scan_object; case internal::TOKEN_TRUE: internal::scan_true(line, column, cursor, end); - tape.push_back({.type = TapeType::True, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); + tape.push_back( + {.type = TapeType::True, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_TRUE.size()), + .count = 0, + .line = value_line, + .column = value_column}); goto do_scan_array_item_separator; case internal::TOKEN_FALSE: internal::scan_false(line, column, cursor, end); - tape.push_back({.type = TapeType::False, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); + tape.push_back( + {.type = TapeType::False, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_FALSE.size()), + .count = 0, + .line = value_line, + .column = value_column}); goto do_scan_array_item_separator; case internal::TOKEN_NULL: internal::scan_null(line, column, cursor, end); - tape.push_back({.type = TapeType::Null, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); - goto do_scan_array_item_separator; - case internal::TOKEN_STRING_QUOTE: { - const auto string_start{ - static_cast(cursor - buffer_start)}; - const auto string_has_escape{ - internal::scan_string(line, column, cursor, end)}; - const auto string_length{static_cast( - cursor - buffer_start - string_start - 1)}; tape.push_back( - {TapeType::String, - string_has_escape ? TAPE_FLAG_STRING_ESCAPE : std::uint8_t{0}, - string_start, string_length, 0, value_line, value_column}); + {.type = TapeType::Null, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_NULL.size()), + .count = 0, + .line = value_line, + .column = value_column}); + goto do_scan_array_item_separator; + case internal::TOKEN_STRING_QUOTE: + internal::push_string( + cursor, end, buffer_start, line, column, tape); goto do_scan_array_item_separator; - } case internal::TOKEN_NUMBER_MINUS: case internal::TOKEN_NUMBER_ZERO: case internal::TOKEN_NUMBER_ONE: @@ -708,20 +747,10 @@ do_scan_array: { switch (character) { case internal::TOKEN_ARRAY_DELIMITER: goto do_scan_array_item; - case internal::TOKEN_ARRAY_END: { - assert(!container_stack.empty()); - auto &frame{container_stack.back()}; - tape[frame.tape_index].count = frame.child_count; - tape.push_back({.type = TapeType::ArrayEnd, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = line, - .column = column}); - container_stack.pop_back(); + case internal::TOKEN_ARRAY_END: + internal::push_container_end(line, column, + container_stack, tape); goto do_scan_container_end; - } default: [[unlikely]] throw JSONParseError(line, column); } @@ -739,7 +768,7 @@ do_scan_object: { .count = 0, .line = line, .column = column}); - container_stack.push_back({start_index, 0}); + container_stack.push_back({.tape_index = start_index, .child_count = 0}); internal::skip_whitespace(cursor, end, line, column); if (cursor >= end) [[unlikely]] { @@ -785,20 +814,10 @@ do_scan_object: { } character = *cursor++; switch (character) { - case internal::TOKEN_STRING_QUOTE: { - const auto key_start{static_cast(cursor - buffer_start)}; - const auto key_line{line}; - const auto key_column{column}; - const auto key_has_escape{ - internal::scan_string(line, column, cursor, end)}; - const auto key_length{ - static_cast(cursor - buffer_start - key_start - 1)}; - tape.push_back( - {TapeType::Key, - key_has_escape ? TAPE_FLAG_STRING_ESCAPE : std::uint8_t{0}, - key_start, key_length, 0, key_line, key_column}); + case internal::TOKEN_STRING_QUOTE: + internal::push_string( + cursor, end, buffer_start, line, column, tape); goto do_scan_object_separator; - } default: [[unlikely]] throw JSONParseError(line, column); } @@ -845,47 +864,44 @@ do_scan_object: { goto do_scan_object; case internal::TOKEN_TRUE: internal::scan_true(line, column, cursor, end); - tape.push_back({.type = TapeType::True, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); + tape.push_back( + {.type = TapeType::True, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_TRUE.size()), + .count = 0, + .line = value_line, + .column = value_column}); goto do_scan_object_property_end; case internal::TOKEN_FALSE: internal::scan_false(line, column, cursor, end); - tape.push_back({.type = TapeType::False, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); + tape.push_back( + {.type = TapeType::False, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_FALSE.size()), + .count = 0, + .line = value_line, + .column = value_column}); goto do_scan_object_property_end; case internal::TOKEN_NULL: internal::scan_null(line, column, cursor, end); - tape.push_back({.type = TapeType::Null, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = value_line, - .column = value_column}); - goto do_scan_object_property_end; - case internal::TOKEN_STRING_QUOTE: { - const auto string_start{ - static_cast(cursor - buffer_start)}; - const auto string_has_escape{ - internal::scan_string(line, column, cursor, end)}; - const auto string_length{static_cast( - cursor - buffer_start - string_start - 1)}; tape.push_back( - {TapeType::String, - string_has_escape ? TAPE_FLAG_STRING_ESCAPE : std::uint8_t{0}, - string_start, string_length, 0, value_line, value_column}); + {.type = TapeType::Null, + .flags = 0, + .offset = 0, + .length = static_cast( + internal::CONSTANT_NULL.size()), + .count = 0, + .line = value_line, + .column = value_column}); + goto do_scan_object_property_end; + case internal::TOKEN_STRING_QUOTE: + internal::push_string( + cursor, end, buffer_start, line, column, tape); goto do_scan_object_property_end; - } case internal::TOKEN_NUMBER_MINUS: case internal::TOKEN_NUMBER_ZERO: case internal::TOKEN_NUMBER_ONE: @@ -928,20 +944,10 @@ do_scan_object: { switch (character) { case internal::TOKEN_OBJECT_DELIMITER: goto do_scan_object_key; - case internal::TOKEN_OBJECT_END: { - assert(!container_stack.empty()); - auto &frame{container_stack.back()}; - tape[frame.tape_index].count = frame.child_count; - tape.push_back({.type = TapeType::ObjectEnd, - .flags = 0, - .offset = 0, - .length = 0, - .count = 0, - .line = line, - .column = column}); - container_stack.pop_back(); + case internal::TOKEN_OBJECT_END: + internal::push_container_end(line, column, + container_stack, tape); goto do_scan_container_end; - } default: [[unlikely]] throw JSONParseError(line, column); } diff --git a/vendor/core/src/core/json/stringify.h b/vendor/core/src/core/json/stringify.h index 97979da8d..06fd14866 100644 --- a/vendor/core/src/core/json/stringify.h +++ b/vendor/core/src/core/json/stringify.h @@ -16,7 +16,7 @@ #include // std::next, std::cbegin, std::cend, std::back_inserter #include // std::basic_ostream #include // std::ostringstream -#include // std::basic_string +#include // std::basic_string, std::string #include // std::vector namespace sourcemeta::core::internal { @@ -296,6 +296,27 @@ auto prettify(const typename JSON::Object &document, stream.put(internal::TOKEN_OBJECT_END); } +// A number the double precision format holds exactly would be read back as a +// double, so it keeps an exponent to be read back as a decimal instead. The +// rest are read back as decimals however they are written, so they take the +// plain form, which states them as they came in. Where that form needs an +// exponent of its own it groups the digits in threes, which states them +// further from how they came in than scientific notation does +inline auto +stringify_decimal(const Decimal &decimal, + std::basic_ostream &stream) + -> void { + if (!decimal.is_double()) { + const auto plain{decimal.to_string()}; + if (plain.find('e') == std::string::npos) { + stream << plain; + return; + } + } + + stream << decimal.to_scientific_string(); +} + template