diff --git a/DEPENDENCIES b/DEPENDENCIES index c876d94ec..350c15c94 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,4 +1,4 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core fe9ef27a95281775fe3a98b9fb8eb4c51837af0a -blaze https://github.com/sourcemeta/blaze fa1310cb51cd7d1afbd08dc47dab5307b0122526 +core https://github.com/sourcemeta/core f8a406692eb8080c84cd016ec4968e391461b1c1 +blaze https://github.com/sourcemeta/blaze 22d0e8a03d1de54c44599426d8537386fcf2a617 bootstrap https://github.com/twbs/bootstrap 1a6fdfae6be09b09eaced8f0e442ca6f7680a61e diff --git a/config.cmake.in b/config.cmake.in index 93c76fb20..5f976c0c5 100644 --- a/config.cmake.in +++ b/config.cmake.in @@ -10,7 +10,7 @@ endif() include(CMakeFindDependencyMacro) find_dependency(Core COMPONENTS json uri jsonpointer numeric regex io) -find_dependency(Blaze COMPONENTS foundation bundle alterschema) +find_dependency(Blaze COMPONENTS foundation bundle alterschema canonicalizer) foreach(component ${JSONBINPACK_COMPONENTS}) if(component STREQUAL "runtime") diff --git a/src/compiler/CMakeLists.txt b/src/compiler/CMakeLists.txt index 7fb220f4f..cfec64025 100644 --- a/src/compiler/CMakeLists.txt +++ b/src/compiler/CMakeLists.txt @@ -32,3 +32,5 @@ target_link_libraries(sourcemeta_jsonbinpack_compiler PUBLIC sourcemeta::blaze::foundation) target_link_libraries(sourcemeta_jsonbinpack_compiler PRIVATE sourcemeta::blaze::alterschema) +target_link_libraries(sourcemeta_jsonbinpack_compiler PRIVATE + sourcemeta::blaze::canonicalizer) diff --git a/src/compiler/compiler.cc b/src/compiler/compiler.cc index 62f6666a1..eb7993f79 100644 --- a/src/compiler/compiler.cc +++ b/src/compiler/compiler.cc @@ -3,6 +3,7 @@ #include #include +#include #include #include "encoding.h" @@ -24,13 +25,8 @@ auto canonicalize(sourcemeta::core::JSON &schema, const sourcemeta::blaze::SchemaWalker &walker, const sourcemeta::blaze::SchemaResolver &resolver, const std::string_view default_dialect) -> void { - sourcemeta::blaze::SchemaTransformer canonicalizer; - sourcemeta::blaze::add(canonicalizer, - sourcemeta::blaze::AlterSchemaMode::Canonicalizer); - [[maybe_unused]] const auto result = - canonicalizer.apply(schema, walker, make_resolver(resolver), - transformer_callback_noop, default_dialect); - assert(result.first); + sourcemeta::blaze::canonicalize(schema, walker, make_resolver(resolver), + default_dialect); } auto make_encoding(sourcemeta::core::JSON &document, @@ -62,7 +58,8 @@ auto compile(sourcemeta::core::JSON &schema, const sourcemeta::blaze::SchemaWalker &walker, const sourcemeta::blaze::SchemaResolver &resolver, const std::string_view default_dialect) -> void { - canonicalize(schema, walker, resolver, default_dialect); + sourcemeta::jsonbinpack::canonicalize(schema, walker, resolver, + default_dialect); sourcemeta::blaze::SchemaTransformer mapper; diff --git a/vendor/blaze/CMakeLists.txt b/vendor/blaze/CMakeLists.txt index aae7a5136..d4da10a5f 100644 --- a/vendor/blaze/CMakeLists.txt +++ b/vendor/blaze/CMakeLists.txt @@ -12,7 +12,7 @@ option(BLAZE_TEST "Build the Blaze test runner library" ON) option(BLAZE_CONFIGURATION "Build the Blaze configuration file library" ON) option(BLAZE_ALTERSCHEMA "Build the Blaze alterschema rule library" ON) option(BLAZE_CODEGEN "Build the Blaze codegen library" ON) -option(BLAZE_DOCUMENTATION "Build the Blaze documentation generator library" ON) +option(BLAZE_CANONICALIZER "Build the Blaze canonicalizer library" ON) option(BLAZE_EDITOR "Build the Blaze editor schema compatibility library" ON) option(BLAZE_FOUNDATION "Build the Blaze foundation JSON Schema library" ON) option(BLAZE_FRAME "Build the Blaze frame library" ON) @@ -94,8 +94,8 @@ if(BLAZE_CODEGEN) add_subdirectory(src/codegen) endif() -if(BLAZE_DOCUMENTATION) - add_subdirectory(src/documentation) +if(BLAZE_CANONICALIZER) + add_subdirectory(src/canonicalizer) endif() if(BLAZE_EDITOR) @@ -192,8 +192,8 @@ if(BLAZE_TESTS) add_subdirectory(test/codegen) endif() - if(BLAZE_DOCUMENTATION) - add_subdirectory(test/documentation) + if(BLAZE_CANONICALIZER) + add_subdirectory(test/canonicalizer) endif() if(BLAZE_EDITOR) diff --git a/vendor/blaze/DEPENDENCIES b/vendor/blaze/DEPENDENCIES index ff2a459b4..ca8a03483 100644 --- a/vendor/blaze/DEPENDENCIES +++ b/vendor/blaze/DEPENDENCIES @@ -1,5 +1,5 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core fe9ef27a95281775fe3a98b9fb8eb4c51837af0a +core https://github.com/sourcemeta/core f8a406692eb8080c84cd016ec4968e391461b1c1 jsonschema-test-suite https://github.com/json-schema-org/JSON-Schema-Test-Suite 1acd90e53554fa24d2529b49fd7d50bab18f8b7e jsonschema-2020-12 https://github.com/json-schema-org/json-schema-spec 769daad75a9553562333a8937a187741cb708c72 jsonschema-2019-09 https://github.com/json-schema-org/json-schema-spec 41014ea723120ce70b314d72f863c6929d9f3cfd diff --git a/vendor/blaze/config.cmake.in b/vendor/blaze/config.cmake.in index cd9163c56..74ecce022 100644 --- a/vendor/blaze/config.cmake.in +++ b/vendor/blaze/config.cmake.in @@ -13,7 +13,7 @@ if(NOT BLAZE_COMPONENTS) list(APPEND BLAZE_COMPONENTS configuration) list(APPEND BLAZE_COMPONENTS alterschema) list(APPEND BLAZE_COMPONENTS codegen) - list(APPEND BLAZE_COMPONENTS documentation) + list(APPEND BLAZE_COMPONENTS canonicalizer) list(APPEND BLAZE_COMPONENTS editor) list(APPEND BLAZE_COMPONENTS bundle) list(APPEND BLAZE_COMPONENTS format) @@ -22,7 +22,7 @@ endif() include(CMakeFindDependencyMacro) find_dependency(Core COMPONENTS unicode punycode idna regex uri uritemplate json - jsonpointer io yaml crypto html email ip dns time css) + jsonpointer jsonld io yaml crypto html email ip dns time css) foreach(component ${BLAZE_COMPONENTS}) if(component STREQUAL "foundation") @@ -70,12 +70,12 @@ foreach(component ${BLAZE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_evaluator.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_compiler.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_output.cmake") - include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_alterschema.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_canonicalizer.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_codegen.cmake") - elseif(component STREQUAL "documentation") + elseif(component STREQUAL "canonicalizer") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_foundation.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_frame.cmake") - include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_documentation.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_canonicalizer.cmake") elseif(component STREQUAL "editor") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_foundation.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_frame.cmake") diff --git a/vendor/blaze/ports/javascript/describe.mjs b/vendor/blaze/ports/javascript/describe.mjs index 621e03038..18d4d63dd 100644 --- a/vendor/blaze/ports/javascript/describe.mjs +++ b/vendor/blaze/ports/javascript/describe.mjs @@ -471,6 +471,12 @@ export function describe(valid, instruction, evaluatePath, if (keyword === '$ref') { return describeReference(target); } + // For the wrapper instruction we emit when hoisting `then` subschemas + // whose `if` condition compiles to nothing + if (keyword === 'then') { + return 'The ' + typeName(targetType) + + ' value was expected to validate against the given conditional'; + } return ''; } diff --git a/vendor/blaze/ports/javascript/index.mjs b/vendor/blaze/ports/javascript/index.mjs index f58f86bf5..46684e8c7 100644 --- a/vendor/blaze/ports/javascript/index.mjs +++ b/vendor/blaze/ports/javascript/index.mjs @@ -697,6 +697,22 @@ function evaluateInstructionFastCallback(instruction, instance, depth, template, return handler(instruction, instance, depth, template, evaluator); } +// Evaluate an entire subtree without emitting callbacks, while still +// maintaining the schema resources stack for dynamic anchor resolution, +// mirroring `evaluate_instruction_without_callback` from the C++ evaluator +function evaluateInstructionWithoutCallback(instruction, instance, depth, template, evaluator) { + const previousDispatch = evaluateInstruction; + const previousCallbackMode = evaluator.callbackMode; + evaluateInstruction = (evaluator.trackMode || evaluator.dynamicMode) + ? evaluateInstructionTracked + : evaluateInstructionFast; + evaluator.callbackMode = false; + const result = evaluateInstruction(instruction, instance, depth, template, evaluator); + evaluateInstruction = previousDispatch; + evaluator.callbackMode = previousCallbackMode; + return result; +} + function evaluateInstructionTrackedCallback(instruction, instance, depth, template, evaluator) { if (depth > DEPTH_LIMIT) { throw new Error('The evaluation path depth limit was reached likely due to infinite recursion'); @@ -1551,7 +1567,7 @@ function AssertionObjectPropertiesSimple(instruction, instance, depth, template, continue; } if (index < children.length) { - if (!evaluateInstructionFast(children[index], target[name], depth + 1, template, evaluator)) { + if (!evaluateInstructionWithoutCallback(children[index], target[name], depth + 1, template, evaluator)) { if (evaluator.callbackMode) evaluator.callbackPop(instruction, false); return false; } @@ -3455,7 +3471,10 @@ function AssertionObjectPropertiesSimple_fast(instruction, instance, depth, temp continue; } if (index < children.length) { - if (!evaluateInstructionFast(children[index], target[name], depth + 1, template, evaluator)) return false; + // Note that we don't hardcode the fast dispatcher here, as the fused + // children may require schema resource tracking for dynamic anchor + // resolution, which the current dispatcher takes care of + if (!evaluateInstruction(children[index], target[name], depth + 1, template, evaluator)) return false; } } return true; diff --git a/vendor/blaze/schemas/documentation.json b/vendor/blaze/schemas/documentation.json deleted file mode 100644 index f99b2aa7a..000000000 --- a/vendor/blaze/schemas/documentation.json +++ /dev/null @@ -1,368 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Blaze Documentation Format", - "description": "The JSON representation of a documentation table", - "examples": [ - { - "identifier": 0, - "rows": [ - { - "identifier": 1, - "path": [ - { - "type": "synthetic", - "value": "root" - } - ], - "type": { - "kind": "primitive", - "name": "string" - } - } - ] - } - ], - "$ref": "#/$defs/table", - "$defs": { - "table": { - "type": "object", - "required": [ "identifier", "rows" ], - "properties": { - "identifier": { - "description": "A unique monotonic identifier for this table", - "type": "integer", - "minimum": 0 - }, - "title": { - "description": "The title annotation from the source schema branch", - "type": "string", - "minLength": 1 - }, - "dynamicAnchor": { - "description": "The dynamic anchor value declared by this schema", - "type": "string", - "minLength": 1 - }, - "rows": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/$defs/row" - } - }, - "children": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { - "$ref": "#/$defs/section" - } - } - }, - "additionalProperties": false - }, - "row": { - "type": "object", - "required": [ "identifier", "path", "type" ], - "properties": { - "identifier": { - "description": "A unique monotonic identifier for this row", - "type": "integer", - "minimum": 0 - }, - "path": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/$defs/pathSegment" - } - }, - "modifiers": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { - "enum": [ "readOnly", "writeOnly", "deprecated" ] - } - }, - "type": { - "$ref": "#/$defs/typeExpression" - }, - "badges": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { - "$ref": "#/$defs/badge" - } - }, - "required": { - "type": "boolean" - }, - "constraints": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { - "type": "string", - "minLength": 1 - } - }, - "title": { - "description": "The title annotation for this property", - "type": "string", - "minLength": 1 - }, - "description": { - "description": "The description annotation for this property", - "type": "string", - "minLength": 1 - }, - "default": { - "description": "The default value annotation for this property. Any JSON value is valid, including null" - }, - "examples": { - "description": "Example values for this property", - "type": "array", - "minItems": 1, - "uniqueItems": true - }, - "children": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { - "$ref": "#/$defs/section" - } - } - }, - "additionalProperties": false - }, - "section": { - "type": "object", - "required": [ "label", "children" ], - "properties": { - "label": { - "anyOf": [ - { - "enum": [ - "Any of", - "All of", - "One of", - "Property names", - "Contains", - "Decoded content", - "Must NOT match", - "If", - "Then", - "Else" - ] - }, - { - "type": "string", - "pattern": "^Array item \\d+$" - } - ] - }, - "position": { - "description": "The JSON Pointer of the operator within the parent schema", - "type": "string", - "minLength": 1 - }, - "children": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { - "$ref": "#/$defs/table" - } - } - }, - "additionalProperties": false - }, - "pathSegment": { - "type": "object", - "required": [ "type", "value" ], - "properties": { - "type": { - "enum": [ "literal", "pattern", "wildcard", "synthetic" ] - }, - "value": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - }, - "badge": { - "type": "object", - "required": [ "kind", "value" ], - "properties": { - "kind": { - "enum": [ "format", "encoding", "mime" ] - }, - "value": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - }, - "typeExpression": { - "anyOf": [ - { - "type": "object", - "required": [ "kind" ], - "properties": { - "kind": { - "const": "object" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ "kind", "name" ], - "properties": { - "kind": { - "const": "primitive" - }, - "name": { - "enum": [ "string", "integer", "number" ] - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ "kind" ], - "properties": { - "kind": { - "const": "array" - }, - "items": { - "description": "The homogeneous item type. Absent when the array has no items constraint", - "$ref": "#/$defs/typeExpression" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ "kind", "items" ], - "properties": { - "kind": { - "const": "tuple" - }, - "items": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { - "$ref": "#/$defs/typeExpression" - } - }, - "additional": { - "description": "The open tail type. Absent when the tuple has no tail", - "$ref": "#/$defs/typeExpression" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ "kind", "values" ], - "properties": { - "kind": { - "const": "enum" - }, - "values": { - "description": "The visible enum values (first 10 by default)", - "type": "array", - "minItems": 1, - "uniqueItems": true - }, - "overflow": { - "description": "The remaining enum values beyond the visible portion", - "type": "array", - "minItems": 1, - "uniqueItems": true - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ "kind", "url" ], - "properties": { - "kind": { - "const": "externalRef" - }, - "url": { - "type": "string", - "format": "uri", - "minLength": 1 - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ "kind", "identifier", "path" ], - "properties": { - "kind": { - "const": "recursiveRef" - }, - "identifier": { - "description": "The identifier of the target table in the same documentation tree", - "type": "integer", - "minimum": 0 - }, - "path": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/$defs/pathSegment" - } - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ "kind", "anchor" ], - "properties": { - "kind": { - "const": "dynamicRef" - }, - "anchor": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ "kind" ], - "properties": { - "kind": { - "const": "any" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ "kind" ], - "properties": { - "kind": { - "const": "never" - } - }, - "additionalProperties": false - } - ] - } - } -} diff --git a/vendor/blaze/src/alterschema/CMakeLists.txt b/vendor/blaze/src/alterschema/CMakeLists.txt index 22da55e73..287517494 100644 --- a/vendor/blaze/src/alterschema/CMakeLists.txt +++ b/vendor/blaze/src/alterschema/CMakeLists.txt @@ -3,71 +3,6 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema PRIVATE_HEADERS error.h transformer.h SOURCES alterschema.cc schema_rule.cc transformer.cc wrap.cc # Canonicalizer - canonicalizer/additional_items_implicit.h - canonicalizer/comment_drop.h - canonicalizer/const_as_enum.h - canonicalizer/dependencies_to_any_of.h - canonicalizer/dependencies_to_extends_disallow.h - canonicalizer/dependent_required_to_any_of.h - canonicalizer/dependent_schemas_to_any_of.h - canonicalizer/deprecated_false_drop.h - canonicalizer/draft3_type_any.h - canonicalizer/disallow_array_to_extends.h - canonicalizer/disallow_double_negation.h - canonicalizer/disallow_extends_to_type.h - canonicalizer/disallow_to_array_of_schemas.h - canonicalizer/disallow_type_union_to_extends.h - canonicalizer/divisible_by_implicit.h - canonicalizer/duplicate_disallow_entries.h - canonicalizer/empty_definitions_drop.h - canonicalizer/empty_defs_drop.h - canonicalizer/empty_dependencies_drop.h - canonicalizer/empty_dependent_required_drop.h - canonicalizer/empty_dependent_schemas_drop.h - canonicalizer/empty_disallow_drop.h - canonicalizer/enum_drop_redundant_validation.h - canonicalizer/enum_filter_by_type.h - canonicalizer/enum_split_by_type.h - canonicalizer/exclusive_maximum_boolean_integer_fold.h - canonicalizer/exclusive_maximum_integer_to_maximum.h - canonicalizer/exclusive_minimum_boolean_integer_fold.h - canonicalizer/exclusive_minimum_integer_to_minimum.h - canonicalizer/extends_to_array.h - canonicalizer/if_then_else_implicit.h - canonicalizer/implicit_contains_keywords.h - canonicalizer/implicit_object_keywords.h - canonicalizer/items_implicit.h - canonicalizer/max_contains_covered_by_max_items.h - canonicalizer/max_decimal_implicit.h - canonicalizer/maximum_can_equal_integer_fold.h - canonicalizer/maximum_can_equal_true_drop.h - canonicalizer/min_items_given_min_contains.h - canonicalizer/min_length_implicit.h - canonicalizer/min_properties_covered_by_required.h - canonicalizer/minimum_can_equal_integer_fold.h - canonicalizer/minimum_can_equal_true_drop.h - canonicalizer/multiple_of_implicit.h - canonicalizer/optional_property_implicit.h - canonicalizer/recursive_anchor_false_drop.h - canonicalizer/required_property_implicit.h - canonicalizer/required_to_extends.h - canonicalizer/single_branch_allof.h - canonicalizer/single_branch_anyof.h - canonicalizer/single_branch_oneof.h - canonicalizer/type_array_to_any_of.h - canonicalizer/type_boolean_as_enum.h - canonicalizer/type_inherit_in_place.h - canonicalizer/type_null_as_enum.h - canonicalizer/type_union_distribute_keywords.h - canonicalizer/type_union_implicit.h - canonicalizer/type_union_to_schemas.h - canonicalizer/type_with_applicator_to_allof.h - canonicalizer/type_with_applicator_to_extends.h - canonicalizer/unevaluated_items_to_items.h - canonicalizer/unevaluated_properties_to_additional_properties.h - canonicalizer/unsatisfiable_can_equal_bounds.h - canonicalizer/unsatisfiable_exclusive_equal_bounds.h - canonicalizer/unsatisfiable_type_and_enum.h # Common common/allof_false_simplify.h diff --git a/vendor/blaze/src/alterschema/alterschema.cc b/vendor/blaze/src/alterschema/alterschema.cc index 9b32e78f2..17a5305cc 100644 --- a/vendor/blaze/src/alterschema/alterschema.cc +++ b/vendor/blaze/src/alterschema/alterschema.cc @@ -109,74 +109,6 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, return false; \ } -#include "canonicalizer/additional_items_implicit.h" -#include "canonicalizer/allof_merge_compatible_branches.h" -#include "canonicalizer/comment_drop.h" -#include "canonicalizer/const_as_enum.h" -#include "canonicalizer/dependencies_to_any_of.h" -#include "canonicalizer/dependencies_to_extends_disallow.h" -#include "canonicalizer/dependent_required_to_any_of.h" -#include "canonicalizer/dependent_schemas_to_any_of.h" -#include "canonicalizer/deprecated_false_drop.h" -#include "canonicalizer/disallow_array_to_extends.h" -#include "canonicalizer/disallow_double_negation.h" -#include "canonicalizer/disallow_extends_to_type.h" -#include "canonicalizer/disallow_to_array_of_schemas.h" -#include "canonicalizer/disallow_type_union_to_extends.h" -#include "canonicalizer/divisible_by_implicit.h" -#include "canonicalizer/draft3_type_any.h" -#include "canonicalizer/duplicate_disallow_entries.h" -#include "canonicalizer/empty_definitions_drop.h" -#include "canonicalizer/empty_defs_drop.h" -#include "canonicalizer/empty_dependencies_drop.h" -#include "canonicalizer/empty_dependent_required_drop.h" -#include "canonicalizer/empty_dependent_schemas_drop.h" -#include "canonicalizer/empty_disallow_drop.h" -#include "canonicalizer/enum_drop_redundant_validation.h" -#include "canonicalizer/enum_filter_by_type.h" -#include "canonicalizer/enum_split_by_type.h" -#include "canonicalizer/exclusive_maximum_boolean_integer_fold.h" -#include "canonicalizer/exclusive_maximum_integer_to_maximum.h" -#include "canonicalizer/exclusive_minimum_boolean_integer_fold.h" -#include "canonicalizer/exclusive_minimum_integer_to_minimum.h" -#include "canonicalizer/extends_to_array.h" -#include "canonicalizer/if_then_else_implicit.h" -#include "canonicalizer/implicit_contains_keywords.h" -#include "canonicalizer/implicit_object_keywords.h" -#include "canonicalizer/inline_single_use_ref.h" -#include "canonicalizer/items_implicit.h" -#include "canonicalizer/max_contains_covered_by_max_items.h" -#include "canonicalizer/max_decimal_implicit.h" -#include "canonicalizer/maximum_can_equal_integer_fold.h" -#include "canonicalizer/maximum_can_equal_true_drop.h" -#include "canonicalizer/min_items_given_min_contains.h" -#include "canonicalizer/min_length_implicit.h" -#include "canonicalizer/min_properties_covered_by_required.h" -#include "canonicalizer/minimum_can_equal_integer_fold.h" -#include "canonicalizer/minimum_can_equal_true_drop.h" -#include "canonicalizer/multiple_of_implicit.h" -#include "canonicalizer/optional_property_implicit.h" -#include "canonicalizer/recursive_anchor_false_drop.h" -#include "canonicalizer/required_property_implicit.h" -#include "canonicalizer/required_to_extends.h" -#include "canonicalizer/single_branch_allof.h" -#include "canonicalizer/single_branch_anyof.h" -#include "canonicalizer/single_branch_oneof.h" -#include "canonicalizer/type_array_to_any_of.h" -#include "canonicalizer/type_boolean_as_enum.h" -#include "canonicalizer/type_inherit_in_place.h" -#include "canonicalizer/type_null_as_enum.h" -#include "canonicalizer/type_union_distribute_keywords.h" -#include "canonicalizer/type_union_implicit.h" -#include "canonicalizer/type_union_to_schemas.h" -#include "canonicalizer/type_with_applicator_to_allof.h" -#include "canonicalizer/type_with_applicator_to_extends.h" -#include "canonicalizer/unevaluated_items_to_items.h" -#include "canonicalizer/unevaluated_properties_to_additional_properties.h" -#include "canonicalizer/unsatisfiable_can_equal_bounds.h" -#include "canonicalizer/unsatisfiable_exclusive_equal_bounds.h" -#include "canonicalizer/unsatisfiable_type_and_enum.h" - // Common #include "common/allof_false_simplify.h" #include "common/anyof_false_simplify.h" @@ -348,36 +280,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { return; } - if (mode == AlterSchemaMode::Canonicalizer) { - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - } - - if (mode == AlterSchemaMode::Canonicalizer) { - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - } - - if (mode == AlterSchemaMode::Linter || - mode == AlterSchemaMode::Canonicalizer) { + if (mode == AlterSchemaMode::Linter) { bundle.add(); } @@ -395,9 +298,6 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); - if (mode == AlterSchemaMode::Canonicalizer) { - bundle.add(); - } bundle.add(); bundle.add(); bundle.add(); @@ -411,10 +311,8 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); - if (mode != AlterSchemaMode::Canonicalizer) { - bundle.add(); - bundle.add(); - } + bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); @@ -441,23 +339,6 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); - if (mode == AlterSchemaMode::Canonicalizer) { - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - } - if (mode == AlterSchemaMode::Linter) { bundle.add(); bundle.add(); @@ -501,53 +382,15 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); } - if (mode != AlterSchemaMode::Canonicalizer) { - bundle.add(); - } + bundle.add(); bundle.add(); bundle.add(); - - if (mode != AlterSchemaMode::Canonicalizer) { - bundle.add(); - bundle.add(); - } + bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); - - if (mode == AlterSchemaMode::Canonicalizer) { - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - bundle.add(); - } } } // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_can_equal_bounds.h b/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_can_equal_bounds.h deleted file mode 100644 index c2c85dbd1..000000000 --- a/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_can_equal_bounds.h +++ /dev/null @@ -1,45 +0,0 @@ -class UnsatisfiableCanEqualBounds final : public SchemaTransformRule { -public: - using mutates = std::true_type; - using reframe_after_transform = std::false_type; - UnsatisfiableCanEqualBounds() - : SchemaTransformRule{"unsatisfiable_can_equal_bounds", ""} {}; - - [[nodiscard]] auto - condition(const sourcemeta::core::JSON &schema, - const sourcemeta::core::JSON &, - const sourcemeta::blaze::Vocabularies &vocabularies, - const sourcemeta::blaze::SchemaFrame &, - const sourcemeta::blaze::SchemaFrame::Location &, - const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { - ONLY_CONTINUE_IF( - vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_2) && - schema.is_object()); - - const auto *type{schema.try_at("type")}; - ONLY_CONTINUE_IF( - type && type->is_string() && - (type->to_string() == "number" || type->to_string() == "integer")); - const auto *minimum{schema.try_at("minimum")}; - ONLY_CONTINUE_IF(minimum && minimum->is_number()); - const auto *maximum{schema.try_at("maximum")}; - ONLY_CONTINUE_IF(maximum && maximum->is_number() && *minimum == *maximum); - - const auto *minimum_can_equal{schema.try_at("minimumCanEqual")}; - const bool min_exclusive{minimum_can_equal && - minimum_can_equal->is_boolean() && - !minimum_can_equal->to_boolean()}; - const auto *maximum_can_equal{schema.try_at("maximumCanEqual")}; - const bool max_exclusive{maximum_can_equal && - maximum_can_equal->is_boolean() && - !maximum_can_equal->to_boolean()}; - ONLY_CONTINUE_IF(min_exclusive || max_exclusive); - return true; - } - - auto transform(JSON &schema, const Result &) const -> void override { - schema.into(JSON{false}); - } -}; diff --git a/vendor/blaze/src/alterschema/include/sourcemeta/blaze/alterschema.h b/vendor/blaze/src/alterschema/include/sourcemeta/blaze/alterschema.h index fbe0d2c92..993b0c2c7 100644 --- a/vendor/blaze/src/alterschema/include/sourcemeta/blaze/alterschema.h +++ b/vendor/blaze/src/alterschema/include/sourcemeta/blaze/alterschema.h @@ -42,11 +42,6 @@ enum class AlterSchemaMode : std::uint8_t { /// performance Linter, - /// Rules that surface implicit constraints and simplifies keywords that - /// are syntax sugar to other keywords, potentially decreasing human - /// readability in favor of explicitness - Canonicalizer, - /// Rules that upgrade a JSON Schema document up to JSON Schema Draft 4 UpgradeDraft4, diff --git a/vendor/blaze/src/canonicalizer/CMakeLists.txt b/vendor/blaze/src/canonicalizer/CMakeLists.txt new file mode 100644 index 000000000..7eb309db5 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/CMakeLists.txt @@ -0,0 +1,144 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME canonicalizer + FOLDER "Blaze/Canonicalizer" + SOURCES canonicalize.cc + rule.h + helpers.h + + # Rules + rules/additional_items_implicit.h + rules/allof_false_simplify.h + rules/allof_merge_compatible_branches.h + rules/anyof_false_simplify.h + rules/anyof_remove_false_schemas.h + rules/anyof_true_simplify.h + rules/comment_drop.h + rules/const_as_enum.h + rules/const_in_enum.h + rules/const_with_type.h + rules/content_media_type_without_encoding.h + rules/content_schema_without_media_type.h + rules/definitions_to_defs.h + rules/dependencies_property_tautology.h + rules/dependencies_to_any_of.h + rules/dependencies_to_extends_disallow.h + rules/dependent_required_tautology.h + rules/dependent_required_to_any_of.h + rules/dependent_schemas_to_any_of.h + rules/deprecated_false_drop.h + rules/disallow_array_to_extends.h + rules/disallow_double_negation.h + rules/disallow_extends_to_type.h + rules/disallow_narrows_type.h + rules/disallow_to_array_of_schemas.h + rules/disallow_type_union_to_extends.h + rules/divisible_by_implicit.h + rules/double_negation_elimination.h + rules/draft_official_dialect_with_https.h + rules/draft_official_dialect_without_empty_fragment.h + rules/draft_ref_siblings.h + rules/draft3_type_any.h + rules/drop_allof_empty_schemas.h + rules/drop_extends_empty_schemas.h + rules/duplicate_allof_branches.h + rules/duplicate_anyof_branches.h + rules/duplicate_disallow_entries.h + rules/duplicate_enum_values.h + rules/duplicate_required_values.h + rules/dynamic_ref_to_static_ref.h + rules/else_without_if.h + rules/empty_definitions_drop.h + rules/empty_defs_drop.h + rules/empty_dependencies_drop.h + rules/empty_dependent_required_drop.h + rules/empty_dependent_schemas_drop.h + rules/empty_disallow_drop.h + rules/empty_object_as_true.h + rules/enum_drop_redundant_validation.h + rules/enum_filter_by_type.h + rules/enum_split_by_type.h + rules/enum_with_type.h + rules/equal_numeric_bounds_to_const.h + rules/equal_numeric_bounds_to_enum.h + rules/exclusive_bounds_false_drop.h + rules/exclusive_maximum_boolean_integer_fold.h + rules/exclusive_maximum_integer_to_maximum.h + rules/exclusive_maximum_number_and_maximum.h + rules/exclusive_minimum_boolean_integer_fold.h + rules/exclusive_minimum_integer_to_minimum.h + rules/exclusive_minimum_number_and_minimum.h + rules/extends_to_array.h + rules/flatten_nested_allof.h + rules/flatten_nested_anyof.h + rules/flatten_nested_extends.h + rules/if_then_else_implicit.h + rules/if_without_then_else.h + rules/ignored_metaschema.h + rules/implicit_contains_keywords.h + rules/implicit_object_keywords.h + rules/inline_single_use_ref.h + rules/items_implicit.h + rules/max_contains_covered_by_max_items.h + rules/max_contains_without_contains.h + rules/max_decimal_implicit.h + rules/maximum_can_equal_integer_fold.h + rules/maximum_can_equal_true_drop.h + rules/maximum_real_for_integer.h + rules/min_contains_without_contains.h + rules/min_items_given_min_contains.h + rules/min_length_implicit.h + rules/min_properties_covered_by_required.h + rules/minimum_can_equal_integer_fold.h + rules/minimum_can_equal_true_drop.h + rules/minimum_real_for_integer.h + rules/modern_official_dialect_with_empty_fragment.h + rules/modern_official_dialect_with_http.h + rules/multiple_of_implicit.h + rules/non_applicable_additional_items.h + rules/non_applicable_disallow_types.h + rules/non_applicable_enum_validation_keywords.h + rules/non_applicable_type_specific_keywords.h + rules/not_false.h + rules/oneof_false_simplify.h + rules/oneof_to_anyof_disjoint_types.h + rules/optional_property_implicit.h + rules/orphan_definitions.h + rules/recursive_anchor_false_drop.h + rules/required_properties_in_properties.h + rules/required_property_implicit.h + rules/required_to_extends.h + rules/single_branch_allof.h + rules/single_branch_anyof.h + rules/single_branch_oneof.h + rules/single_type_array.h + rules/then_without_if.h + rules/type_array_to_any_of.h + rules/type_boolean_as_enum.h + rules/type_inherit_in_place.h + rules/type_null_as_enum.h + rules/type_union_distribute_keywords.h + rules/type_union_implicit.h + rules/type_union_to_schemas.h + rules/type_with_applicator_to_allof.h + rules/type_with_applicator_to_extends.h + rules/unevaluated_items_to_items.h + rules/unevaluated_properties_to_additional_properties.h + rules/unknown_keywords_prefix.h + rules/unknown_local_ref.h + rules/unnecessary_allof_ref_wrapper_draft.h + rules/unnecessary_extends_ref_wrapper.h + rules/unsatisfiable_drop_validation.h + rules/unsatisfiable_exclusive_equal_bounds.h + rules/unsatisfiable_in_place_applicator_type.h + rules/unsatisfiable_type_and_enum.h + ) + +if(BLAZE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT blaze NAME canonicalizer) +endif() + +target_link_libraries(sourcemeta_blaze_canonicalizer PUBLIC + sourcemeta::blaze::foundation) +target_link_libraries(sourcemeta_blaze_canonicalizer PUBLIC + sourcemeta::blaze::frame) +target_link_libraries(sourcemeta_blaze_canonicalizer PRIVATE + sourcemeta::core::uri) diff --git a/vendor/blaze/src/canonicalizer/canonicalize.cc b/vendor/blaze/src/canonicalizer/canonicalize.cc new file mode 100644 index 000000000..95b425c48 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/canonicalize.cc @@ -0,0 +1,515 @@ +#include +#include +#include + +#include +#include +#include + +#include // std::sort, std::unique, std::ranges::none_of +#include // std::array +#include // std::popcount +#include // assert +#include // std::floor, std::ceil, std::isfinite +#include // std::derived_from +#include // std::size_t +#include // std::uint64_t +#include // std::hash, std::ref +#include // std::numeric_limits +#include // std::make_unique, std::unique_ptr +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::tuple +#include // std::is_same_v, std::true_type +#include // std::unordered_map +#include // std::unordered_set +#include // std::move, std::to_underlying +#include // std::vector + +namespace sourcemeta::blaze { + +namespace { + +#include "rule.h" + +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(), + std::is_same_v}; +} + +/// Re-analyse the frame, honouring any identifier already present in the schema +auto analyse_frame(sourcemeta::blaze::SchemaFrame &target, + sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &resolver, + const std::string_view default_dialect, + const std::string_view default_id) -> void { + if (!blaze::identify(schema, resolver, default_dialect).empty()) { + target.analyse(schema, walker, resolver, default_dialect); + } else { + target.analyse(schema, walker, resolver, default_dialect, default_id); + } +} + +/// Apply the given rules top-down to every subschema until none of them applies +auto apply(const std::vector &rules, sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &resolver, + const std::string_view default_dialect = "", + const std::string_view default_id = "") -> void { + assert(!rules.empty()); + + struct ProcessedRuleHasher { + auto operator()(const std::tuple &value) const noexcept + -> std::size_t { + return core::Pointer::Hasher{}(std::get<0>(value)) ^ + (std::hash{}(std::get<1>(value)) << 1) ^ + (std::hash{}(std::get<2>(value).fast_hash()) << 2); + } + }; + + std::unordered_set, + ProcessedRuleHasher> + processed_rules; + + blaze::SchemaFrame frame{blaze::SchemaFrame::Mode::References}; + + struct PotentiallyBrokenReference { + core::Pointer origin; + core::JSON::String original; + core::JSON::String destination; + core::JSON::String fragment; + core::Pointer target_pointer; + std::size_t target_relative_pointer; + }; + + std::vector potentially_broken_references; + + while (true) { + if (frame.empty()) { + if (schema.is_boolean()) { + break; + } + + analyse_frame(frame, schema, walker, resolver, default_dialect, + default_id); + } + + std::unordered_set visited; + bool applied{false}; + + for (const auto &entry : frame.locations()) { + if (entry.second.type != blaze::SchemaFrame::LocationType::Resource && + entry.second.type != blaze::SchemaFrame::LocationType::Subschema) { + continue; + } + + const auto [visited_iterator, inserted] = + visited.insert(core::to_pointer(entry.second.pointer)); + if (!inserted) { + continue; + } + const auto &entry_pointer{*visited_iterator}; + auto ¤t{core::get(schema, entry_pointer)}; + const auto current_vocabularies{ + frame.vocabularies(entry.second, resolver)}; + + for (const auto &[rule, reframe_after_transform] : rules) { + const auto outcome{rule->condition(current, schema, + current_vocabularies, frame, + entry.second, walker, resolver)}; + + if (!outcome) { + continue; + } + + potentially_broken_references.clear(); + for (const auto &reference : frame.references()) { + const auto destination{frame.traverse(reference.second.destination)}; + if (!destination.has_value() || + !reference.second.fragment.has_value() || + !reference.second.fragment.value().starts_with('/')) { + continue; + } + + const auto &target{destination.value().get()}; + potentially_broken_references.push_back( + {.origin = core::to_pointer(reference.first.second), + .original = core::JSON::String{reference.second.original}, + .destination = reference.second.destination, + .fragment = + core::JSON::String{reference.second.fragment.value()}, + .target_pointer = core::to_pointer(target.pointer), + .target_relative_pointer = target.relative_pointer}); + } + + rule->transform(current); + + applied = true; + + if (reframe_after_transform) { + analyse_frame(frame, schema, walker, resolver, default_dialect, + default_id); + } else if (current.is_boolean()) { + std::tuple mark{ + entry_pointer, rule->name(), current}; + assert(!processed_rules.contains(mark)); + processed_rules.emplace(std::move(mark)); + frame.reset(); + goto blaze_transformer_start_again; + } + + const auto new_location{ + frame.traverse(core::to_weak_pointer(entry_pointer))}; + assert(new_location.has_value()); + + // Fix broken references before re-checking the condition, + // as the re-check may mutate rule state that rereference needs + bool references_fixed{false}; + const auto resource_offset{new_location.value().get().relative_pointer}; + const auto current_slice{entry_pointer.slice(resource_offset)}; + for (const auto &saved_reference : potentially_broken_references) { + if (core::try_get(schema, saved_reference.target_pointer)) { + continue; + } + + // If the origin was also relocated, resolve its new location + auto effective_origin{saved_reference.origin}; + if (!core::try_get(schema, saved_reference.origin.initial())) { + const auto new_origin{rule->rereference( + saved_reference.destination, saved_reference.origin, + saved_reference.origin.slice(resource_offset), current_slice)}; + if (!new_origin.has_value()) { + continue; + } + effective_origin = saved_reference.origin.slice(0, resource_offset) + .concat(new_origin.value()); + if (!core::try_get(schema, effective_origin.initial())) { + continue; + } + } + + const auto new_relative{rule->rereference( + saved_reference.destination, saved_reference.origin, + saved_reference.target_pointer.slice( + saved_reference.target_relative_pointer), + current_slice)}; + if (!new_relative.has_value()) { + continue; + } + const auto new_fragment{ + saved_reference.fragment == + core::to_string(saved_reference.target_pointer) + ? saved_reference.target_pointer + .slice(0, saved_reference.target_relative_pointer) + .concat(new_relative.value()) + : new_relative.value()}; + + core::URI original{saved_reference.original}; + original.fragment(core::to_string(new_fragment)); + core::set(schema, effective_origin, core::JSON{original.recompose()}); + references_fixed = true; + } + + const auto new_vocabularies{ + frame.vocabularies(new_location.value().get(), resolver)}; + + assert(!rule->condition(current, schema, new_vocabularies, frame, + new_location.value().get(), walker, resolver)); + + std::tuple mark{ + entry_pointer, rule->name(), current}; + assert(!processed_rules.contains(mark)); + processed_rules.emplace(std::move(mark)); + + if (references_fixed) { + frame.reset(); + } + + if (references_fixed || reframe_after_transform) { + goto blaze_transformer_start_again; + } + } + } + + blaze_transformer_start_again: + if (!applied) { + break; + } + } +} + +#include "helpers.h" + +#include "rules/additional_items_implicit.h" +#include "rules/allof_false_simplify.h" +#include "rules/allof_merge_compatible_branches.h" +#include "rules/anyof_false_simplify.h" +#include "rules/anyof_remove_false_schemas.h" +#include "rules/anyof_true_simplify.h" +#include "rules/comment_drop.h" +#include "rules/const_as_enum.h" +#include "rules/const_in_enum.h" +#include "rules/const_with_type.h" +#include "rules/content_media_type_without_encoding.h" +#include "rules/content_schema_without_media_type.h" +#include "rules/definitions_to_defs.h" +#include "rules/dependencies_property_tautology.h" +#include "rules/dependencies_to_any_of.h" +#include "rules/dependencies_to_extends_disallow.h" +#include "rules/dependent_required_tautology.h" +#include "rules/dependent_required_to_any_of.h" +#include "rules/dependent_schemas_to_any_of.h" +#include "rules/deprecated_false_drop.h" +#include "rules/disallow_array_to_extends.h" +#include "rules/disallow_double_negation.h" +#include "rules/disallow_extends_to_type.h" +#include "rules/disallow_narrows_type.h" +#include "rules/disallow_to_array_of_schemas.h" +#include "rules/disallow_type_union_to_extends.h" +#include "rules/divisible_by_implicit.h" +#include "rules/double_negation_elimination.h" +#include "rules/draft3_type_any.h" +#include "rules/draft_official_dialect_with_https.h" +#include "rules/draft_official_dialect_without_empty_fragment.h" +#include "rules/draft_ref_siblings.h" +#include "rules/drop_allof_empty_schemas.h" +#include "rules/drop_extends_empty_schemas.h" +#include "rules/duplicate_allof_branches.h" +#include "rules/duplicate_anyof_branches.h" +#include "rules/duplicate_disallow_entries.h" +#include "rules/duplicate_enum_values.h" +#include "rules/duplicate_required_values.h" +#include "rules/dynamic_ref_to_static_ref.h" +#include "rules/else_without_if.h" +#include "rules/empty_definitions_drop.h" +#include "rules/empty_defs_drop.h" +#include "rules/empty_dependencies_drop.h" +#include "rules/empty_dependent_required_drop.h" +#include "rules/empty_dependent_schemas_drop.h" +#include "rules/empty_disallow_drop.h" +#include "rules/empty_object_as_true.h" +#include "rules/enum_drop_redundant_validation.h" +#include "rules/enum_filter_by_type.h" +#include "rules/enum_split_by_type.h" +#include "rules/enum_with_type.h" +#include "rules/equal_numeric_bounds_to_const.h" +#include "rules/equal_numeric_bounds_to_enum.h" +#include "rules/exclusive_bounds_false_drop.h" +#include "rules/exclusive_maximum_boolean_integer_fold.h" +#include "rules/exclusive_maximum_integer_to_maximum.h" +#include "rules/exclusive_maximum_number_and_maximum.h" +#include "rules/exclusive_minimum_boolean_integer_fold.h" +#include "rules/exclusive_minimum_integer_to_minimum.h" +#include "rules/exclusive_minimum_number_and_minimum.h" +#include "rules/extends_to_array.h" +#include "rules/flatten_nested_allof.h" +#include "rules/flatten_nested_anyof.h" +#include "rules/flatten_nested_extends.h" +#include "rules/if_then_else_implicit.h" +#include "rules/if_without_then_else.h" +#include "rules/ignored_metaschema.h" +#include "rules/implicit_contains_keywords.h" +#include "rules/implicit_object_keywords.h" +#include "rules/inline_single_use_ref.h" +#include "rules/items_implicit.h" +#include "rules/max_contains_covered_by_max_items.h" +#include "rules/max_contains_without_contains.h" +#include "rules/max_decimal_implicit.h" +#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/min_contains_without_contains.h" +#include "rules/min_items_given_min_contains.h" +#include "rules/min_length_implicit.h" +#include "rules/min_properties_covered_by_required.h" +#include "rules/minimum_can_equal_integer_fold.h" +#include "rules/minimum_can_equal_true_drop.h" +#include "rules/minimum_real_for_integer.h" +#include "rules/modern_official_dialect_with_empty_fragment.h" +#include "rules/modern_official_dialect_with_http.h" +#include "rules/multiple_of_implicit.h" +#include "rules/non_applicable_additional_items.h" +#include "rules/non_applicable_disallow_types.h" +#include "rules/non_applicable_enum_validation_keywords.h" +#include "rules/non_applicable_type_specific_keywords.h" +#include "rules/not_false.h" +#include "rules/oneof_false_simplify.h" +#include "rules/oneof_to_anyof_disjoint_types.h" +#include "rules/optional_property_implicit.h" +#include "rules/orphan_definitions.h" +#include "rules/recursive_anchor_false_drop.h" +#include "rules/required_properties_in_properties.h" +#include "rules/required_property_implicit.h" +#include "rules/required_to_extends.h" +#include "rules/single_branch_allof.h" +#include "rules/single_branch_anyof.h" +#include "rules/single_branch_oneof.h" +#include "rules/single_type_array.h" +#include "rules/then_without_if.h" +#include "rules/type_array_to_any_of.h" +#include "rules/type_boolean_as_enum.h" +#include "rules/type_inherit_in_place.h" +#include "rules/type_null_as_enum.h" +#include "rules/type_union_distribute_keywords.h" +#include "rules/type_union_implicit.h" +#include "rules/type_union_to_schemas.h" +#include "rules/type_with_applicator_to_allof.h" +#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_prefix.h" +#include "rules/unknown_local_ref.h" +#include "rules/unnecessary_allof_ref_wrapper_draft.h" +#include "rules/unnecessary_extends_ref_wrapper.h" +#include "rules/unsatisfiable_drop_validation.h" +#include "rules/unsatisfiable_exclusive_equal_bounds.h" +#include "rules/unsatisfiable_in_place_applicator_type.h" +#include "rules/unsatisfiable_type_and_enum.h" + +#undef ONLY_CONTINUE_IF + +} // namespace + +auto canonicalize(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &resolver, + const std::string_view default_dialect, + const std::string_view default_id) -> void { + std::vector rules; + rules.reserve(125); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + apply(rules, schema, walker, resolver, default_dialect, default_id); +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/canonicalizer/helpers.h b/vendor/blaze/src/canonicalizer/helpers.h new file mode 100644 index 000000000..d31752a95 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/helpers.h @@ -0,0 +1,77 @@ +#ifndef SOURCEMETA_BLAZE_CANONICALIZER_HELPERS_H_ +#define SOURCEMETA_BLAZE_CANONICALIZER_HELPERS_H_ + +// TODO: Move upstream +inline auto IS_IN_PLACE_APPLICATOR(const SchemaKeywordType type) -> bool { + return type == SchemaKeywordType::ApplicatorValueOrElementsInPlace || + type == SchemaKeywordType::ApplicatorMembersInPlaceSome || + type == SchemaKeywordType::ApplicatorElementsInPlace || + type == SchemaKeywordType::ApplicatorElementsInPlaceSome || + type == SchemaKeywordType::ApplicatorElementsInPlaceSomeNegate || + type == SchemaKeywordType::ApplicatorValueInPlaceMaybe || + type == SchemaKeywordType::ApplicatorValueInPlaceOther || + 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. +template +auto WALK_UP(const sourcemeta::core::JSON &root, const SchemaFrame &frame, + const SchemaFrame::Location &location, const SchemaWalker &walker, + const SchemaResolver &resolver, + const TraversePredicate &should_continue, + const MatchCallback &matches) + -> std::optional< + std::reference_wrapper> { + 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(frame.uri(parent_pointer).value().get())}; + assert(parent.has_value()); + const auto parent_vocabularies{ + frame.vocabularies(parent.value().get(), resolver)}; + const auto keyword_type{ + walker(relative_pointer.at(0).to_property(), parent_vocabularies).type}; + + if (!should_continue(keyword_type)) { + return std::nullopt; + } + + if (matches(sourcemeta::core::get(root, parent_pointer), + parent_vocabularies)) { + return std::cref(parent.value().get().pointer); + } + + current_pointer = parent_pointer; + current_parent = parent.value().get().parent; + } + + return std::nullopt; +} + +template +auto WALK_UP_IN_PLACE_APPLICATORS(const sourcemeta::core::JSON &root, + const SchemaFrame &frame, + const SchemaFrame::Location &location, + const SchemaWalker &walker, + const SchemaResolver &resolver, + const MatchCallback &matches) + -> std::optional< + std::reference_wrapper> { + return WALK_UP(root, frame, location, walker, resolver, + IS_IN_PLACE_APPLICATOR, matches); +} + +#define ONLY_CONTINUE_IF(condition) \ + if (!(condition)) { \ + return false; \ + } + +#endif diff --git a/vendor/blaze/src/canonicalizer/include/sourcemeta/blaze/canonicalizer.h b/vendor/blaze/src/canonicalizer/include/sourcemeta/blaze/canonicalizer.h new file mode 100644 index 000000000..0e237b202 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/include/sourcemeta/blaze/canonicalizer.h @@ -0,0 +1,51 @@ +#ifndef SOURCEMETA_BLAZE_CANONICALIZER_H_ +#define SOURCEMETA_BLAZE_CANONICALIZER_H_ + +/// @defgroup canonicalizer Canonicalizer +/// @brief Rewrite a JSON Schema into its analysis-canonical form. +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +#ifndef SOURCEMETA_BLAZE_CANONICALIZER_EXPORT +#include +#endif + +#include +#include + +#include + +#include // std::string_view + +namespace sourcemeta::blaze { + +/// @ingroup canonicalizer +/// Rewrite the given schema, in place, into its analysis-canonical form. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// auto schema = sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "http://json-schema.org/draft-03/schema#", +/// "type": "boolean" +/// })JSON"); +/// +/// sourcemeta::blaze::canonicalize(schema, sourcemeta::blaze::schema_walker, +/// sourcemeta::blaze::schema_resolver); +/// ``` +SOURCEMETA_BLAZE_CANONICALIZER_EXPORT +auto canonicalize(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &resolver, + const std::string_view default_dialect = "", + const std::string_view default_id = "") -> void; + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/canonicalizer/rule.h b/vendor/blaze/src/canonicalizer/rule.h new file mode 100644 index 000000000..ba9edc34d --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rule.h @@ -0,0 +1,43 @@ +#ifndef SOURCEMETA_BLAZE_CANONICALIZER_RULE_H_ +#define SOURCEMETA_BLAZE_CANONICALIZER_RULE_H_ + +class SchemaTransformRule { +public: + SchemaTransformRule(const std::string_view name) : name_{name} {} + + virtual ~SchemaTransformRule() = default; + + SchemaTransformRule(const SchemaTransformRule &) = delete; + SchemaTransformRule(SchemaTransformRule &&) = delete; + auto operator=(const SchemaTransformRule &) -> SchemaTransformRule & = delete; + auto operator=(SchemaTransformRule &&) -> SchemaTransformRule & = delete; + + [[nodiscard]] auto name() const noexcept -> std::string_view { + return this->name_; + } + + /// A method to optionally fix any reference location that was affected by the + /// transformation + [[nodiscard]] virtual auto + rereference(const std::string_view, const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &) const + -> std::optional { + return std::nullopt; + } + + [[nodiscard]] virtual auto condition( + const sourcemeta::core::JSON &schema, const sourcemeta::core::JSON &root, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &resolver) const -> bool = 0; + + virtual auto transform(sourcemeta::core::JSON &schema) const -> void = 0; + +private: + const std::string name_{}; +}; + +#endif diff --git a/vendor/blaze/src/alterschema/canonicalizer/additional_items_implicit.h b/vendor/blaze/src/canonicalizer/rules/additional_items_implicit.h similarity index 83% rename from vendor/blaze/src/alterschema/canonicalizer/additional_items_implicit.h rename to vendor/blaze/src/canonicalizer/rules/additional_items_implicit.h index 9badd20a7..824ab36eb 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/additional_items_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/additional_items_implicit.h @@ -1,9 +1,8 @@ class AdditionalItemsImplicit final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; AdditionalItemsImplicit() - : SchemaTransformRule{"additional_items_implicit", ""} {}; + : SchemaTransformRule{"additional_items_implicit"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class AdditionalItemsImplicit final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_4, @@ -31,7 +29,7 @@ class AdditionalItemsImplicit final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.assign("additionalItems", this->is_draft3_ ? sourcemeta::core::JSON::make_object() : sourcemeta::core::JSON{true}); diff --git a/vendor/blaze/src/canonicalizer/rules/allof_false_simplify.h b/vendor/blaze/src/canonicalizer/rules/allof_false_simplify.h new file mode 100644 index 000000000..6ee154c8e --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/allof_false_simplify.h @@ -0,0 +1,42 @@ +class AllOfFalseSimplify final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + AllOfFalseSimplify() : SchemaTransformRule{"allof_false_simplify"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"allOf"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6}) && + schema.is_object() && !schema.defines("not")); + + const auto *all_of{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(all_of && all_of->is_array()); + + for (std::size_t index = 0; index < all_of->size(); ++index) { + const auto &entry{all_of->at(index)}; + if (entry.is_boolean() && !entry.to_boolean()) { + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + return true; + } + } + + return false; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.at("allOf").into(sourcemeta::core::JSON{true}); + schema.rename("allOf", "not"); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/allof_merge_compatible_branches.h b/vendor/blaze/src/canonicalizer/rules/allof_merge_compatible_branches.h similarity index 69% rename from vendor/blaze/src/alterschema/canonicalizer/allof_merge_compatible_branches.h rename to vendor/blaze/src/canonicalizer/rules/allof_merge_compatible_branches.h index 2912f1e86..fd7fc1d59 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/allof_merge_compatible_branches.h +++ b/vendor/blaze/src/canonicalizer/rules/allof_merge_compatible_branches.h @@ -1,9 +1,8 @@ class AllOfMergeCompatibleBranches final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; AllOfMergeCompatibleBranches() - : SchemaTransformRule{"allof_merge_compatible_branches", ""} {}; + : SchemaTransformRule{"allof_merge_compatible_branches"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,9 +11,8 @@ class AllOfMergeCompatibleBranches final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { - static const JSON::String KEYWORD{"allOf"}; + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"allOf"}; ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Applicator, Vocabularies::Known::JSON_Schema_2019_09_Applicator, @@ -27,7 +25,8 @@ class AllOfMergeCompatibleBranches final : public SchemaTransformRule { ONLY_CONTINUE_IF(all_of && all_of->is_array() && all_of->size() >= 2); ONLY_CONTINUE_IF(!frame.has_references_through( - location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); const auto &branches{*all_of}; @@ -72,8 +71,8 @@ class AllOfMergeCompatibleBranches final : public SchemaTransformRule { return false; } - auto transform(JSON &schema, const Result &) const -> void override { - static const JSON::String KEYWORD{"allOf"}; + auto transform(sourcemeta::core::JSON &schema) const -> void override { + static const sourcemeta::core::JSON::String KEYWORD{"allOf"}; auto &target{schema.at(KEYWORD).at(this->merge_into_)}; const auto &source{schema.at(KEYWORD).at(this->merge_from_)}; target.merge(source.as_object()); @@ -82,11 +81,12 @@ class AllOfMergeCompatibleBranches final : public SchemaTransformRule { static_cast(this->merge_from_))); } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { - static const JSON::String KEYWORD{"allOf"}; + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + static const sourcemeta::core::JSON::String KEYWORD{"allOf"}; const auto relative{target.resolve_from(current)}; if (relative.size() < 2 || !relative.at(0).is_property() || relative.at(0).to_property() != KEYWORD || !relative.at(1).is_index()) { @@ -95,16 +95,18 @@ class AllOfMergeCompatibleBranches final : public SchemaTransformRule { const auto index{relative.at(1).to_index()}; if (index == this->merge_from_) { - const Pointer old_prefix{ - current.concat(Pointer{KEYWORD, this->merge_from_})}; - const Pointer new_prefix{ - current.concat(Pointer{KEYWORD, this->merge_into_})}; + const sourcemeta::core::Pointer old_prefix{current.concat( + sourcemeta::core::Pointer{KEYWORD, this->merge_from_})}; + const sourcemeta::core::Pointer new_prefix{current.concat( + sourcemeta::core::Pointer{KEYWORD, this->merge_into_})}; return target.rebase(old_prefix, new_prefix); } if (index > this->merge_from_) { - const Pointer old_prefix{current.concat(Pointer{KEYWORD, index})}; - const Pointer new_prefix{current.concat(Pointer{KEYWORD, index - 1})}; + const sourcemeta::core::Pointer old_prefix{ + current.concat(sourcemeta::core::Pointer{KEYWORD, index})}; + const sourcemeta::core::Pointer new_prefix{ + current.concat(sourcemeta::core::Pointer{KEYWORD, index - 1})}; return target.rebase(old_prefix, new_prefix); } @@ -112,17 +114,20 @@ class AllOfMergeCompatibleBranches final : public SchemaTransformRule { } private: - static auto is_type_only_branch(const JSON &branch) -> bool { + static auto is_type_only_branch(const sourcemeta::core::JSON &branch) + -> bool { return branch.size() == 1 && branch.defines("type"); } - static auto has_in_place_applicators(const JSON &branch) -> bool { + static auto has_in_place_applicators(const sourcemeta::core::JSON &branch) + -> bool { return branch.defines("anyOf") || branch.defines("oneOf") || branch.defines("allOf") || branch.defines("not") || branch.defines("if"); } - static auto is_mergeable_branch(const JSON &branch) -> bool { + static auto is_mergeable_branch(const sourcemeta::core::JSON &branch) + -> bool { if (!branch.is_object()) { return false; } @@ -133,8 +138,9 @@ class AllOfMergeCompatibleBranches final : public SchemaTransformRule { !branch.defines("$recursiveAnchor"); } - static auto has_overlapping_keywords(const JSON &branch_a, - const JSON &branch_b) -> bool { + static auto has_overlapping_keywords(const sourcemeta::core::JSON &branch_a, + const sourcemeta::core::JSON &branch_b) + -> bool { for (const auto &entry : branch_a.as_object()) { if (branch_b.defines(entry.first)) { return true; @@ -144,14 +150,15 @@ class AllOfMergeCompatibleBranches final : public SchemaTransformRule { } static auto - has_cross_dependencies(const JSON &branch_a, const JSON &branch_b, + has_cross_dependencies(const sourcemeta::core::JSON &branch_a, + const sourcemeta::core::JSON &branch_b, const sourcemeta::blaze::SchemaWalker &walker, const sourcemeta::blaze::Vocabularies &vocabularies) -> bool { for (const auto &entry_a : branch_a.as_object()) { const auto &metadata{walker(entry_a.first, vocabularies)}; for (const auto &dependency : metadata.dependencies) { - if (branch_b.defines(JSON::String{dependency})) { + if (branch_b.defines(sourcemeta::core::JSON::String{dependency})) { return true; } } @@ -160,7 +167,7 @@ class AllOfMergeCompatibleBranches final : public SchemaTransformRule { for (const auto &entry_b : branch_b.as_object()) { const auto &metadata{walker(entry_b.first, vocabularies)}; for (const auto &dependency : metadata.dependencies) { - if (branch_a.defines(JSON::String{dependency})) { + if (branch_a.defines(sourcemeta::core::JSON::String{dependency})) { return true; } } diff --git a/vendor/blaze/src/canonicalizer/rules/anyof_false_simplify.h b/vendor/blaze/src/canonicalizer/rules/anyof_false_simplify.h new file mode 100644 index 000000000..c7c117a3f --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/anyof_false_simplify.h @@ -0,0 +1,37 @@ +class AnyOfFalseSimplify final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + AnyOfFalseSimplify() : SchemaTransformRule{"anyof_false_simplify"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"anyOf"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6}) && + schema.is_object() && !schema.defines("not")); + + const auto *any_of{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(any_of && any_of->is_array() && any_of->size() == 1); + + const auto &entry{any_of->front()}; + ONLY_CONTINUE_IF(entry.is_boolean() && !entry.to_boolean()); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.at("anyOf").into(sourcemeta::core::JSON{true}); + schema.rename("anyOf", "not"); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/anyof_remove_false_schemas.h b/vendor/blaze/src/canonicalizer/rules/anyof_remove_false_schemas.h new file mode 100644 index 000000000..129df3577 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/anyof_remove_false_schemas.h @@ -0,0 +1,67 @@ +class AnyOfRemoveFalseSchemas final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + AnyOfRemoveFalseSchemas() + : SchemaTransformRule{"anyof_remove_false_schemas"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"anyOf"}; + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6}) && + schema.is_object() && schema.defines(KEYWORD) && + schema.at(KEYWORD).is_array() && + schema.at(KEYWORD).contains(sourcemeta::core::JSON{false})); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + + std::vector false_locations; + bool has_non_false{false}; + const auto &anyof{schema.at(KEYWORD)}; + for (std::size_t index = 0; index < anyof.size(); ++index) { + const auto &entry{anyof.at(index)}; + if (entry.is_boolean() && !entry.to_boolean()) { + false_locations.push_back(sourcemeta::core::Pointer{KEYWORD, index}); + } else { + has_non_false = true; + } + } + + ONLY_CONTINUE_IF(has_non_false); + this->locations_ = std::move(false_locations); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + static const sourcemeta::core::JSON::String KEYWORD{"anyOf"}; + + std::unordered_set indices_to_remove; + for (const auto &location : this->locations_) { + indices_to_remove.insert(location.at(1).to_index()); + } + + auto new_anyof{sourcemeta::core::JSON::make_array()}; + const auto &anyof{schema.at(KEYWORD)}; + for (std::size_t index = 0; index < anyof.size(); ++index) { + if (!indices_to_remove.contains(index)) { + new_anyof.push_back(anyof.at(index)); + } + } + + schema.assign(KEYWORD, std::move(new_anyof)); + } + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/anyof_true_simplify.h b/vendor/blaze/src/canonicalizer/rules/anyof_true_simplify.h new file mode 100644 index 000000000..ee92e734c --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/anyof_true_simplify.h @@ -0,0 +1,68 @@ +class AnyOfTrueSimplify final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + AnyOfTrueSimplify() : SchemaTransformRule{"anyof_true_simplify"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &root, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"anyOf"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object() && schema.defines(KEYWORD) && + schema.at(KEYWORD).is_array()); + + // When unevaluated keywords are present at this level or any + // ancestor, `anyOf` annotations are semantically meaningful even + // if one branch always succeeds + if (vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_2020_12_Applicator})) { + auto cursor{std::cref(location)}; + while (true) { + const auto ¤t_schema{ + sourcemeta::core::get(root, cursor.get().pointer)}; + if (current_schema.is_object() && + (current_schema.defines("unevaluatedItems") || + current_schema.defines("unevaluatedProperties"))) { + return false; + } + if (!cursor.get().parent.has_value()) { + break; + } + const auto parent_location{frame.traverse(cursor.get().parent.value())}; + if (!parent_location.has_value()) { + break; + } + cursor = parent_location.value(); + } + } + + const auto &anyof{schema.at(KEYWORD)}; + for (std::size_t index = 0; index < anyof.size(); ++index) { + const auto &entry{anyof.at(index)}; + if ((entry.is_boolean() && entry.to_boolean()) || + (entry.is_object() && entry.empty())) { + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + return true; + } + } + + return false; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase("anyOf"); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/comment_drop.h b/vendor/blaze/src/canonicalizer/rules/comment_drop.h similarity index 74% rename from vendor/blaze/src/alterschema/canonicalizer/comment_drop.h rename to vendor/blaze/src/canonicalizer/rules/comment_drop.h index 713eca547..bfe3c2d10 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/comment_drop.h +++ b/vendor/blaze/src/canonicalizer/rules/comment_drop.h @@ -1,8 +1,7 @@ class CommentDrop final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - CommentDrop() : SchemaTransformRule{"comment_drop", ""} {}; + CommentDrop() : SchemaTransformRule{"comment_drop"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,8 +10,7 @@ class CommentDrop final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_2019_09_Core, @@ -21,7 +19,7 @@ class CommentDrop final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.erase("$comment"); } }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/const_as_enum.h b/vendor/blaze/src/canonicalizer/rules/const_as_enum.h similarity index 70% rename from vendor/blaze/src/alterschema/canonicalizer/const_as_enum.h rename to vendor/blaze/src/canonicalizer/rules/const_as_enum.h index f4ec73653..d1fea3c82 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/const_as_enum.h +++ b/vendor/blaze/src/canonicalizer/rules/const_as_enum.h @@ -1,11 +1,7 @@ class ConstAsEnum final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - ConstAsEnum() - : SchemaTransformRule{"const_as_enum", - "Setting `const` is syntax sugar for an " - "enumeration of a single value"} {}; + ConstAsEnum() : SchemaTransformRule{"const_as_enum"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -14,8 +10,7 @@ class ConstAsEnum final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation, @@ -23,10 +18,10 @@ class ConstAsEnum final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6}) && schema.is_object() && schema.defines("const") && !schema.defines("enum")); - return APPLIES_TO_KEYWORDS("const"); + return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { auto values = sourcemeta::core::JSON::make_array(); values.push_back(schema.at("const")); schema.at("const").into(std::move(values)); diff --git a/vendor/blaze/src/canonicalizer/rules/const_in_enum.h b/vendor/blaze/src/canonicalizer/rules/const_in_enum.h new file mode 100644 index 000000000..eaffe08f6 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/const_in_enum.h @@ -0,0 +1,32 @@ +class ConstInEnum final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + ConstInEnum() : SchemaTransformRule{"const_in_enum"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6}) && + schema.is_object()); + + const auto *const_value{schema.try_at("const")}; + ONLY_CONTINUE_IF(const_value); + const auto *enum_value{schema.try_at("enum")}; + ONLY_CONTINUE_IF(enum_value && enum_value->is_array() && + enum_value->contains(*const_value)); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase("enum"); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/const_with_type.h b/vendor/blaze/src/canonicalizer/rules/const_with_type.h new file mode 100644 index 000000000..5428247dd --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/const_with_type.h @@ -0,0 +1,36 @@ +class ConstWithType final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + ConstWithType() : SchemaTransformRule{"const_with_type"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6}) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type); + const auto *const_value{schema.try_at("const")}; + ONLY_CONTINUE_IF(const_value); + + const auto current_types{parse_schema_type(*type)}; + ONLY_CONTINUE_IF(current_types.any()); + ONLY_CONTINUE_IF( + current_types.test(std::to_underlying(const_value->type()))); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase("type"); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/content_media_type_without_encoding.h b/vendor/blaze/src/canonicalizer/rules/content_media_type_without_encoding.h new file mode 100644 index 000000000..53974f89b --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/content_media_type_without_encoding.h @@ -0,0 +1,27 @@ +class ContentMediaTypeWithoutEncoding final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + ContentMediaTypeWithoutEncoding() + : SchemaTransformRule{"content_media_type_without_encoding"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Content, + Vocabularies::Known::JSON_Schema_2019_09_Content, + Vocabularies::Known::JSON_Schema_Draft_7}) && + schema.is_object() && schema.defines("contentMediaType") && + !schema.defines("contentEncoding")); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase("contentMediaType"); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/content_schema_without_media_type.h b/vendor/blaze/src/canonicalizer/rules/content_schema_without_media_type.h new file mode 100644 index 000000000..cc3f88756 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/content_schema_without_media_type.h @@ -0,0 +1,33 @@ +class ContentSchemaWithoutMediaType final : public SchemaTransformRule { +private: + // NOLINTNEXTLINE(bugprone-throwing-static-initialization) + static inline const std::string KEYWORD{"contentSchema"}; + +public: + using reframe_after_transform = std::true_type; + ContentSchemaWithoutMediaType() + : SchemaTransformRule{"content_schema_without_media_type"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Content, + Vocabularies::Known::JSON_Schema_2019_09_Content}) && + schema.is_object() && schema.defines(KEYWORD) && + !schema.defines("contentMediaType")); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase(KEYWORD); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/definitions_to_defs.h b/vendor/blaze/src/canonicalizer/rules/definitions_to_defs.h new file mode 100644 index 000000000..d5ca0914b --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/definitions_to_defs.h @@ -0,0 +1,34 @@ +class DefinitionsToDefs final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + DefinitionsToDefs() : SchemaTransformRule{"definitions_to_defs"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Core, + Vocabularies::Known::JSON_Schema_2019_09_Core}) && + schema.is_object() && schema.defines("definitions") && + !schema.defines("$defs")); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.rename("definitions", "$defs"); + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + return target.rebase(current.concat("definitions"), + current.concat("$defs")); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/dependencies_property_tautology.h b/vendor/blaze/src/canonicalizer/rules/dependencies_property_tautology.h new file mode 100644 index 000000000..efc4478a0 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/dependencies_property_tautology.h @@ -0,0 +1,183 @@ +class DependenciesPropertyTautology final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + DependenciesPropertyTautology() + : SchemaTransformRule{"dependencies_property_tautology"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *dependencies{schema.try_at("dependencies")}; + ONLY_CONTINUE_IF(dependencies && dependencies->is_object()); + + const bool is_draft_3{vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})}; + + if (is_draft_3) { + const auto *properties{schema.try_at("properties")}; + ONLY_CONTINUE_IF(properties && properties->is_object()); + + ONLY_CONTINUE_IF(std::ranges::any_of( + properties->as_object(), [dependencies](const auto &entry) -> auto { + if (!entry.second.is_object()) { + return false; + } + const auto *required{entry.second.try_at("required")}; + if (!required || !required->is_boolean() || + !required->to_boolean()) { + return false; + } + const auto *dependent{dependencies->try_at(entry.first)}; + return dependent && + (dependent->is_array() || dependent->is_string()); + })); + this->locations_ = {sourcemeta::core::Pointer{"dependencies"}, + sourcemeta::core::Pointer{"properties"}}; + return true; + } + + const auto *required{schema.try_at("required")}; + ONLY_CONTINUE_IF(required && required->is_array()); + + ONLY_CONTINUE_IF(std::ranges::any_of( + required->as_array(), [dependencies](const auto &element) -> auto { + if (!element.is_string()) { + return false; + } + const auto *dependent{dependencies->try_at(element.to_string())}; + return dependent && (dependent->is_array() || dependent->is_string()); + })); + this->locations_ = {sourcemeta::core::Pointer{"dependencies"}, + sourcemeta::core::Pointer{"required"}}; + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + const bool is_draft_3_path{ + std::ranges::any_of(this->locations_, [](const auto &pointer) -> auto { + return pointer.size() == 1 && pointer.at(0).is_property() && + pointer.at(0).to_property() == "properties"; + })}; + if (is_draft_3_path) { + this->transform_boolean(schema); + } else { + this->transform_array(schema); + } + } + +private: + static auto transform_array(sourcemeta::core::JSON &schema) -> void { + auto requirements{schema.at("required")}; + while (true) { + bool match{false}; + const auto copy{requirements}; + for (const auto &element : copy.as_array()) { + if (!element.is_string() || + !schema.at("dependencies").defines(element.to_string())) { + continue; + } + + const auto &dependents{ + schema.at("dependencies").at(element.to_string())}; + if (dependents.is_array()) { + for (const auto &dependent : dependents.as_array()) { + if (dependent.is_string()) { + match = true; + requirements.push_back(dependent); + } + } + + schema.at("dependencies").erase(element.to_string()); + } else if (dependents.is_string()) { + match = true; + requirements.push_back(dependents); + schema.at("dependencies").erase(element.to_string()); + } + } + + if (!match) { + break; + } + } + + schema.assign("required", requirements); + } + + static auto transform_boolean(sourcemeta::core::JSON &schema) -> void { + while (true) { + bool match{false}; + + std::vector snapshot; + for (const auto &entry : schema.at("properties").as_object()) { + if (!entry.second.is_object()) { + continue; + } + const auto *required{entry.second.try_at("required")}; + if (required && required->is_boolean() && required->to_boolean()) { + snapshot.push_back(entry.first); + } + } + + for (const auto &name : snapshot) { + if (!schema.at("dependencies").defines(name)) { + continue; + } + + const auto dependents_copy{schema.at("dependencies").at(name)}; + std::vector new_required; + if (dependents_copy.is_string()) { + new_required.push_back(dependents_copy.to_string()); + } else if (dependents_copy.is_array()) { + for (const auto &dependent : dependents_copy.as_array()) { + if (dependent.is_string()) { + new_required.push_back(dependent.to_string()); + } + } + } else { + continue; + } + + for (const auto &dependency_name : new_required) { + if (!schema.at("properties").defines(dependency_name)) { + auto new_property{sourcemeta::core::JSON::make_object()}; + new_property.assign("required", sourcemeta::core::JSON{true}); + schema.at("properties") + .assign(dependency_name, std::move(new_property)); + match = true; + } else if (schema.at("properties").at(dependency_name).is_object()) { + auto &existing{schema.at("properties").at(dependency_name)}; + const auto *current_required{existing.try_at("required")}; + if (!current_required || !current_required->is_boolean() || + !current_required->to_boolean()) { + existing.assign("required", sourcemeta::core::JSON{true}); + match = true; + } + } + } + + schema.at("dependencies").erase(name); + } + + if (!match) { + break; + } + } + } + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/dependencies_to_any_of.h b/vendor/blaze/src/canonicalizer/rules/dependencies_to_any_of.h similarity index 57% rename from vendor/blaze/src/alterschema/canonicalizer/dependencies_to_any_of.h rename to vendor/blaze/src/canonicalizer/rules/dependencies_to_any_of.h index 9ab137c0b..e950ad97a 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/dependencies_to_any_of.h +++ b/vendor/blaze/src/canonicalizer/rules/dependencies_to_any_of.h @@ -1,8 +1,7 @@ class DependenciesToAnyOf final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - DependenciesToAnyOf() : SchemaTransformRule{"dependencies_to_any_of", ""} {}; + DependenciesToAnyOf() : SchemaTransformRule{"dependencies_to_any_of"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,8 +10,7 @@ class DependenciesToAnyOf final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_6, @@ -29,61 +27,64 @@ class DependenciesToAnyOf final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { - auto result_branches{JSON::make_array()}; + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto result_branches{sourcemeta::core::JSON::make_array()}; - std::vector processed; + std::vector processed; for (const auto &entry : schema.at("dependencies").as_object()) { if (is_schema(entry.second)) { - auto not_required{JSON::make_object()}; - not_required.assign("type", JSON{"object"}); - not_required.assign("required", JSON::make_array()); - not_required.at("required").push_back(JSON{entry.first}); - auto not_branch{JSON::make_object()}; + auto not_required{sourcemeta::core::JSON::make_object()}; + not_required.assign("type", sourcemeta::core::JSON{"object"}); + not_required.assign("required", sourcemeta::core::JSON::make_array()); + not_required.at("required") + .push_back(sourcemeta::core::JSON{entry.first}); + auto not_branch{sourcemeta::core::JSON::make_object()}; not_branch.assign("not", std::move(not_required)); - auto required_obj{JSON::make_object()}; - required_obj.assign("type", JSON{"object"}); - required_obj.assign("required", JSON::make_array()); - required_obj.at("required").push_back(JSON{entry.first}); + auto required_obj{sourcemeta::core::JSON::make_object()}; + required_obj.assign("type", sourcemeta::core::JSON{"object"}); + required_obj.assign("required", sourcemeta::core::JSON::make_array()); + required_obj.at("required") + .push_back(sourcemeta::core::JSON{entry.first}); - auto all_of{JSON::make_array()}; + auto all_of{sourcemeta::core::JSON::make_array()}; all_of.push_back(std::move(required_obj)); all_of.push_back(entry.second); - auto allof_branch{JSON::make_object()}; + auto allof_branch{sourcemeta::core::JSON::make_object()}; allof_branch.assign("allOf", std::move(all_of)); - auto pair{JSON::make_array()}; + auto pair{sourcemeta::core::JSON::make_array()}; pair.push_back(std::move(not_branch)); pair.push_back(std::move(allof_branch)); - auto wrapper{JSON::make_object()}; + auto wrapper{sourcemeta::core::JSON::make_object()}; wrapper.assign("anyOf", std::move(pair)); result_branches.push_back(std::move(wrapper)); } else if (entry.second.is_array()) { - auto required_all{JSON::make_array()}; - required_all.push_back(JSON{entry.first}); + auto required_all{sourcemeta::core::JSON::make_array()}; + required_all.push_back(sourcemeta::core::JSON{entry.first}); for (const auto &dependent : entry.second.as_array()) { required_all.push_back(dependent); } - auto not_required{JSON::make_object()}; - not_required.assign("type", JSON{"object"}); - not_required.assign("required", JSON::make_array()); - not_required.at("required").push_back(JSON{entry.first}); - auto not_branch{JSON::make_object()}; + auto not_required{sourcemeta::core::JSON::make_object()}; + not_required.assign("type", sourcemeta::core::JSON{"object"}); + not_required.assign("required", sourcemeta::core::JSON::make_array()); + not_required.at("required") + .push_back(sourcemeta::core::JSON{entry.first}); + auto not_branch{sourcemeta::core::JSON::make_object()}; not_branch.assign("not", std::move(not_required)); - auto required_branch{JSON::make_object()}; - required_branch.assign("type", JSON{"object"}); + auto required_branch{sourcemeta::core::JSON::make_object()}; + required_branch.assign("type", sourcemeta::core::JSON{"object"}); required_branch.assign("required", std::move(required_all)); - auto pair{JSON::make_array()}; + auto pair{sourcemeta::core::JSON::make_array()}; pair.push_back(std::move(not_branch)); pair.push_back(std::move(required_branch)); - auto wrapper{JSON::make_object()}; + auto wrapper{sourcemeta::core::JSON::make_object()}; wrapper.assign("anyOf", std::move(pair)); result_branches.push_back(std::move(wrapper)); } else { diff --git a/vendor/blaze/src/alterschema/canonicalizer/dependencies_to_extends_disallow.h b/vendor/blaze/src/canonicalizer/rules/dependencies_to_extends_disallow.h similarity index 55% rename from vendor/blaze/src/alterschema/canonicalizer/dependencies_to_extends_disallow.h rename to vendor/blaze/src/canonicalizer/rules/dependencies_to_extends_disallow.h index 5db582376..49a462b34 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/dependencies_to_extends_disallow.h +++ b/vendor/blaze/src/canonicalizer/rules/dependencies_to_extends_disallow.h @@ -1,9 +1,8 @@ class DependenciesToExtendsDisallow final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; DependenciesToExtendsDisallow() - : SchemaTransformRule{"dependencies_to_extends_disallow", ""} {}; + : SchemaTransformRule{"dependencies_to_extends_disallow"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class DependenciesToExtendsDisallow final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_3) && schema.is_object()); @@ -29,48 +27,52 @@ class DependenciesToExtendsDisallow final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { - auto result_branches{JSON::make_array()}; - std::vector processed; + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto result_branches{sourcemeta::core::JSON::make_array()}; + std::vector processed; for (const auto &entry : schema.at("dependencies").as_object()) { - auto required_property{JSON::make_object()}; - required_property.assign("type", JSON{"object"}); - auto required_props{JSON::make_object()}; - auto required_prop_schema{JSON::make_object()}; - required_prop_schema.assign("required", JSON{true}); + auto required_property{sourcemeta::core::JSON::make_object()}; + required_property.assign("type", sourcemeta::core::JSON{"object"}); + auto required_props{sourcemeta::core::JSON::make_object()}; + auto required_prop_schema{sourcemeta::core::JSON::make_object()}; + required_prop_schema.assign("required", sourcemeta::core::JSON{true}); required_props.assign(entry.first, std::move(required_prop_schema)); required_property.assign("properties", std::move(required_props)); - required_property.assign("patternProperties", JSON::make_object()); - required_property.assign("additionalProperties", JSON::make_object()); + required_property.assign("patternProperties", + sourcemeta::core::JSON::make_object()); + required_property.assign("additionalProperties", + sourcemeta::core::JSON::make_object()); - auto not_required{JSON::make_object()}; - auto disallow_array{JSON::make_array()}; + auto not_required{sourcemeta::core::JSON::make_object()}; + auto disallow_array{sourcemeta::core::JSON::make_array()}; disallow_array.push_back(required_property); not_required.assign("disallow", std::move(disallow_array)); if (is_schema(entry.second)) { - auto extends_array{JSON::make_array()}; - auto required_copy{JSON::make_object()}; - required_copy.assign("type", JSON{"object"}); - auto req_props_copy{JSON::make_object()}; - auto req_prop_schema_copy{JSON::make_object()}; - req_prop_schema_copy.assign("required", JSON{true}); + auto extends_array{sourcemeta::core::JSON::make_array()}; + auto required_copy{sourcemeta::core::JSON::make_object()}; + required_copy.assign("type", sourcemeta::core::JSON{"object"}); + auto req_props_copy{sourcemeta::core::JSON::make_object()}; + auto req_prop_schema_copy{sourcemeta::core::JSON::make_object()}; + req_prop_schema_copy.assign("required", sourcemeta::core::JSON{true}); req_props_copy.assign(entry.first, std::move(req_prop_schema_copy)); required_copy.assign("properties", std::move(req_props_copy)); - required_copy.assign("patternProperties", JSON::make_object()); - required_copy.assign("additionalProperties", JSON::make_object()); + required_copy.assign("patternProperties", + sourcemeta::core::JSON::make_object()); + required_copy.assign("additionalProperties", + sourcemeta::core::JSON::make_object()); extends_array.push_back(std::move(required_copy)); extends_array.push_back(entry.second); - auto extends_branch{JSON::make_object()}; + auto extends_branch{sourcemeta::core::JSON::make_object()}; extends_branch.assign("extends", std::move(extends_array)); - auto type_array{JSON::make_array()}; + auto type_array{sourcemeta::core::JSON::make_array()}; type_array.push_back(std::move(not_required)); type_array.push_back(std::move(extends_branch)); - auto wrapper{JSON::make_object()}; + auto wrapper{sourcemeta::core::JSON::make_object()}; wrapper.assign("type", std::move(type_array)); result_branches.push_back(std::move(wrapper)); } else if (entry.second.is_string() || entry.second.is_array()) { @@ -85,26 +87,28 @@ class DependenciesToExtendsDisallow final : public SchemaTransformRule { } } - auto required_all{JSON::make_object()}; - required_all.assign("type", JSON{"object"}); - auto all_props{JSON::make_object()}; - auto trigger_schema{JSON::make_object()}; - trigger_schema.assign("required", JSON{true}); + auto required_all{sourcemeta::core::JSON::make_object()}; + required_all.assign("type", sourcemeta::core::JSON{"object"}); + auto all_props{sourcemeta::core::JSON::make_object()}; + auto trigger_schema{sourcemeta::core::JSON::make_object()}; + trigger_schema.assign("required", sourcemeta::core::JSON{true}); all_props.assign(entry.first, std::move(trigger_schema)); for (const auto &dependent_prop : dependent_props) { - auto dep_schema{JSON::make_object()}; - dep_schema.assign("required", JSON{true}); + auto dep_schema{sourcemeta::core::JSON::make_object()}; + dep_schema.assign("required", sourcemeta::core::JSON{true}); all_props.assign(dependent_prop, std::move(dep_schema)); } required_all.assign("properties", std::move(all_props)); - required_all.assign("patternProperties", JSON::make_object()); - required_all.assign("additionalProperties", JSON::make_object()); + required_all.assign("patternProperties", + sourcemeta::core::JSON::make_object()); + required_all.assign("additionalProperties", + sourcemeta::core::JSON::make_object()); - auto type_array{JSON::make_array()}; + auto type_array{sourcemeta::core::JSON::make_array()}; type_array.push_back(std::move(not_required)); type_array.push_back(std::move(required_all)); - auto wrapper{JSON::make_object()}; + auto wrapper{sourcemeta::core::JSON::make_object()}; wrapper.assign("type", std::move(type_array)); result_branches.push_back(std::move(wrapper)); } else { diff --git a/vendor/blaze/src/canonicalizer/rules/dependent_required_tautology.h b/vendor/blaze/src/canonicalizer/rules/dependent_required_tautology.h new file mode 100644 index 000000000..6fe8ef300 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/dependent_required_tautology.h @@ -0,0 +1,67 @@ +class DependentRequiredTautology final : public SchemaTransformRule { +public: + using reframe_after_transform = std::false_type; + DependentRequiredTautology() + : SchemaTransformRule{"dependent_required_tautology"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation}) && + schema.is_object()); + + const auto *dependent_required{schema.try_at("dependentRequired")}; + ONLY_CONTINUE_IF(dependent_required && dependent_required->is_object()); + const auto *required{schema.try_at("required")}; + ONLY_CONTINUE_IF(required && required->is_array()); + + ONLY_CONTINUE_IF( + std::any_of(required->as_array().cbegin(), required->as_array().cend(), + [dependent_required](const auto &element) -> auto { + return element.is_string() && + dependent_required->defines(element.to_string()); + })); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto requirements{schema.at("required")}; + while (true) { + bool match{false}; + const auto copy{requirements}; + for (const auto &element : copy.as_array()) { + if (!element.is_string() || + !schema.at("dependentRequired").defines(element.to_string())) { + continue; + } + + const auto &dependents{ + schema.at("dependentRequired").at(element.to_string())}; + if (dependents.is_array()) { + for (const auto &dependent : dependents.as_array()) { + if (dependent.is_string()) { + match = true; + requirements.push_back(dependent); + } + } + + schema.at("dependentRequired").erase(element.to_string()); + } + } + + if (!match) { + break; + } + } + + schema.assign("required", requirements); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h b/vendor/blaze/src/canonicalizer/rules/dependent_required_to_any_of.h similarity index 71% rename from vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h rename to vendor/blaze/src/canonicalizer/rules/dependent_required_to_any_of.h index 525859e92..4f21155aa 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h +++ b/vendor/blaze/src/canonicalizer/rules/dependent_required_to_any_of.h @@ -1,9 +1,8 @@ class DependentRequiredToAnyOf final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; DependentRequiredToAnyOf() - : SchemaTransformRule{"dependent_required_to_any_of", ""} {}; + : SchemaTransformRule{"dependent_required_to_any_of"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class DependentRequiredToAnyOf final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2019_09_Validation, @@ -39,34 +37,36 @@ class DependentRequiredToAnyOf final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { - auto result_branches{JSON::make_array()}; + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto result_branches{sourcemeta::core::JSON::make_array()}; - std::vector processed; + std::vector processed; for (const auto &entry : schema.at("dependentRequired").as_object()) { if (!entry.second.is_array()) { continue; } - auto required_all{JSON::make_array()}; - required_all.push_back(JSON{entry.first}); + auto required_all{sourcemeta::core::JSON::make_array()}; + required_all.push_back(sourcemeta::core::JSON{entry.first}); for (const auto &dependent : entry.second.as_array()) { required_all.push_back(dependent); } - auto absence_branch{JSON::make_object()}; - absence_branch.assign("properties", JSON::make_object()); - absence_branch.at("properties").assign(entry.first, JSON{false}); + auto absence_branch{sourcemeta::core::JSON::make_object()}; + absence_branch.assign("properties", + sourcemeta::core::JSON::make_object()); + absence_branch.at("properties") + .assign(entry.first, sourcemeta::core::JSON{false}); - auto required_branch{JSON::make_object()}; - required_branch.assign("type", JSON{"object"}); + auto required_branch{sourcemeta::core::JSON::make_object()}; + required_branch.assign("type", sourcemeta::core::JSON{"object"}); required_branch.assign("required", std::move(required_all)); - auto pair{JSON::make_array()}; + auto pair{sourcemeta::core::JSON::make_array()}; pair.push_back(std::move(absence_branch)); pair.push_back(std::move(required_branch)); - auto wrapper{JSON::make_object()}; + auto wrapper{sourcemeta::core::JSON::make_object()}; wrapper.assign("anyOf", std::move(pair)); result_branches.push_back(std::move(wrapper)); diff --git a/vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h b/vendor/blaze/src/canonicalizer/rules/dependent_schemas_to_any_of.h similarity index 64% rename from vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h rename to vendor/blaze/src/canonicalizer/rules/dependent_schemas_to_any_of.h index 989ecb2ca..e57b9d2ed 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h +++ b/vendor/blaze/src/canonicalizer/rules/dependent_schemas_to_any_of.h @@ -1,9 +1,8 @@ class DependentSchemasToAnyOf final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; DependentSchemasToAnyOf() - : SchemaTransformRule{"dependent_schemas_to_any_of", ""} {}; + : SchemaTransformRule{"dependent_schemas_to_any_of"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class DependentSchemasToAnyOf final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2019_09_Applicator, @@ -35,31 +33,34 @@ class DependentSchemasToAnyOf final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { - auto result_branches{JSON::make_array()}; + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto result_branches{sourcemeta::core::JSON::make_array()}; for (const auto &entry : schema.at("dependentSchemas").as_object()) { - auto absence_branch{JSON::make_object()}; - absence_branch.assign("properties", JSON::make_object()); - absence_branch.at("properties").assign(entry.first, JSON{false}); + auto absence_branch{sourcemeta::core::JSON::make_object()}; + absence_branch.assign("properties", + sourcemeta::core::JSON::make_object()); + absence_branch.at("properties") + .assign(entry.first, sourcemeta::core::JSON{false}); - auto required_obj{JSON::make_object()}; - required_obj.assign("type", JSON{"object"}); - required_obj.assign("required", JSON::make_array()); - required_obj.at("required").push_back(JSON{entry.first}); + auto required_obj{sourcemeta::core::JSON::make_object()}; + required_obj.assign("type", sourcemeta::core::JSON{"object"}); + required_obj.assign("required", sourcemeta::core::JSON::make_array()); + required_obj.at("required") + .push_back(sourcemeta::core::JSON{entry.first}); - auto all_of{JSON::make_array()}; + auto all_of{sourcemeta::core::JSON::make_array()}; all_of.push_back(std::move(required_obj)); all_of.push_back(entry.second); - auto allof_branch{JSON::make_object()}; + auto allof_branch{sourcemeta::core::JSON::make_object()}; allof_branch.assign("allOf", std::move(all_of)); - auto pair{JSON::make_array()}; + auto pair{sourcemeta::core::JSON::make_array()}; pair.push_back(std::move(absence_branch)); pair.push_back(std::move(allof_branch)); - auto wrapper{JSON::make_object()}; + auto wrapper{sourcemeta::core::JSON::make_object()}; wrapper.assign("anyOf", std::move(pair)); result_branches.push_back(std::move(wrapper)); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/deprecated_false_drop.h b/vendor/blaze/src/canonicalizer/rules/deprecated_false_drop.h similarity index 80% rename from vendor/blaze/src/alterschema/canonicalizer/deprecated_false_drop.h rename to vendor/blaze/src/canonicalizer/rules/deprecated_false_drop.h index d9218527b..6de3c1dee 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/deprecated_false_drop.h +++ b/vendor/blaze/src/canonicalizer/rules/deprecated_false_drop.h @@ -1,8 +1,7 @@ class DeprecatedFalseDrop final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - DeprecatedFalseDrop() : SchemaTransformRule{"deprecated_false_drop", ""} {}; + DeprecatedFalseDrop() : SchemaTransformRule{"deprecated_false_drop"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,8 +10,7 @@ class DeprecatedFalseDrop final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2019_09_Meta_Data, @@ -25,7 +23,7 @@ class DeprecatedFalseDrop final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.erase("deprecated"); } }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_array_to_extends.h b/vendor/blaze/src/canonicalizer/rules/disallow_array_to_extends.h similarity index 71% rename from vendor/blaze/src/alterschema/canonicalizer/disallow_array_to_extends.h rename to vendor/blaze/src/canonicalizer/rules/disallow_array_to_extends.h index 89be86db8..b15c6d937 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/disallow_array_to_extends.h +++ b/vendor/blaze/src/canonicalizer/rules/disallow_array_to_extends.h @@ -1,13 +1,8 @@ class DisallowArrayToExtends final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; DisallowArrayToExtends() - : SchemaTransformRule{ - "disallow_array_to_extends", - "A multi-way `disallow` is the conjunction of single negations: " - "each element becomes its own single-element `disallow` in an " - "`extends` branch"} {}; + : SchemaTransformRule{"disallow_array_to_extends"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -16,8 +11,7 @@ class DisallowArrayToExtends final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && @@ -28,12 +22,12 @@ class DisallowArrayToExtends final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { - auto branches{JSON::make_array()}; + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto branches{sourcemeta::core::JSON::make_array()}; for (const auto &element : schema.at("disallow").as_array()) { - auto negation{JSON::make_array()}; + auto negation{sourcemeta::core::JSON::make_array()}; negation.push_back(element); - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; branch.assign("disallow", std::move(negation)); branches.push_back(std::move(branch)); } @@ -46,7 +40,7 @@ class DisallowArrayToExtends final : public SchemaTransformRule { schema.at("extends").push_back(std::move(branch)); } } else if (schema.defines("extends")) { - auto extends{JSON::make_array()}; + auto extends{sourcemeta::core::JSON::make_array()}; extends.push_back(schema.at("extends")); this->extends_start_ = extends.size(); for (auto &branch : branches.as_array()) { @@ -59,10 +53,11 @@ class DisallowArrayToExtends final : public SchemaTransformRule { } } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { const auto disallow_prefix{current.concat("disallow")}; if (!target.starts_with(disallow_prefix)) { return target; @@ -75,7 +70,7 @@ class DisallowArrayToExtends final : public SchemaTransformRule { const auto index{relative.at(0).to_index()}; return target.rebase( - current.concat(Pointer{"disallow", index}), + current.concat(sourcemeta::core::Pointer{"disallow", index}), current.concat( {"extends", this->extends_start_ + index, "disallow", 0})); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_double_negation.h b/vendor/blaze/src/canonicalizer/rules/disallow_double_negation.h similarity index 82% rename from vendor/blaze/src/alterschema/canonicalizer/disallow_double_negation.h rename to vendor/blaze/src/canonicalizer/rules/disallow_double_negation.h index 4a6ffa816..2a3fb1531 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/disallow_double_negation.h +++ b/vendor/blaze/src/canonicalizer/rules/disallow_double_negation.h @@ -1,14 +1,7 @@ class DisallowDoubleNegation final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - DisallowDoubleNegation() - : SchemaTransformRule{ - "disallow_double_negation", - "A `disallow` whose single negated schema is itself a `disallow` " - "of " - "a single schema is a double negation equivalent to the inner " - "schema"} {}; + DisallowDoubleNegation() : SchemaTransformRule{"disallow_double_negation"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -17,9 +10,8 @@ class DisallowDoubleNegation final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { - static const JSON::String KEYWORD{"disallow"}; + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"disallow"}; ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && @@ -54,7 +46,7 @@ class DisallowDoubleNegation final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { auto inner{schema.at("disallow").at(0).at("disallow").at(0)}; schema.erase("disallow"); @@ -69,10 +61,11 @@ class DisallowDoubleNegation final : public SchemaTransformRule { } } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { auto old_prefix{current.concat({"disallow", 0, "disallow", 0})}; while ( target.starts_with(old_prefix.concat({"disallow", 0, "disallow", 0}))) { diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_extends_to_type.h b/vendor/blaze/src/canonicalizer/rules/disallow_extends_to_type.h similarity index 79% rename from vendor/blaze/src/alterschema/canonicalizer/disallow_extends_to_type.h rename to vendor/blaze/src/canonicalizer/rules/disallow_extends_to_type.h index 2f0791623..92f595245 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/disallow_extends_to_type.h +++ b/vendor/blaze/src/canonicalizer/rules/disallow_extends_to_type.h @@ -1,13 +1,7 @@ class DisallowExtendsToType final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - DisallowExtendsToType() - : SchemaTransformRule{ - "disallow_extends_to_type", - "Negating a conjunction is the disjunction of the negations: an " - "`extends` under `disallow` becomes a `type` union where each " - "branch is its own single negation"} {}; + DisallowExtendsToType() : SchemaTransformRule{"disallow_extends_to_type"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -16,8 +10,7 @@ class DisallowExtendsToType final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && @@ -42,7 +35,7 @@ class DisallowExtendsToType final : public SchemaTransformRule { // The conjuncts relocate to distinct `type` branches (handled by // `rereference`), but the wrapper schema itself is dissolved rather than // moved, so a reference straight at it has no new home: bail in that case - static const JSON::String DISALLOW{"disallow"}; + static const sourcemeta::core::JSON::String DISALLOW{"disallow"}; auto wrapper_pointer{location.pointer}; wrapper_pointer.push_back(std::cref(DISALLOW)); wrapper_pointer.push_back(static_cast(0)); @@ -51,12 +44,12 @@ class DisallowExtendsToType final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { - auto branches{JSON::make_array()}; + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto branches{sourcemeta::core::JSON::make_array()}; for (auto &branch : schema.at("disallow").at(0).at("extends").as_array()) { - auto negation{JSON::make_array()}; + auto negation{sourcemeta::core::JSON::make_array()}; negation.push_back(std::move(branch)); - auto element{JSON::make_object()}; + auto element{sourcemeta::core::JSON::make_object()}; element.assign("disallow", std::move(negation)); branches.push_back(std::move(element)); } @@ -65,10 +58,11 @@ class DisallowExtendsToType final : public SchemaTransformRule { schema.assign("type", std::move(branches)); } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { const auto extends_prefix{current.concat({"disallow", 0, "extends"})}; if (!target.starts_with(extends_prefix)) { return target; diff --git a/vendor/blaze/src/canonicalizer/rules/disallow_narrows_type.h b/vendor/blaze/src/canonicalizer/rules/disallow_narrows_type.h new file mode 100644 index 000000000..20fb047f3 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/disallow_narrows_type.h @@ -0,0 +1,135 @@ +class DisallowNarrowsType final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + DisallowNarrowsType() : SchemaTransformRule{"disallow_narrows_type"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"disallow"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && !disallow->empty()); + + const auto *parent_type{schema.try_at("type")}; + ONLY_CONTINUE_IF(parent_type && parent_type->is_array() && + parent_type->size() > 1); + + std::unordered_set parent_type_names; + for (const auto &entry : parent_type->as_array()) { + ONLY_CONTINUE_IF(entry.is_string() && entry.to_string() != "any"); + parent_type_names.insert(entry.to_string()); + } + + std::vector locations; + std::unordered_set narrowed_types; + for (std::size_t index = 0; index < disallow->size(); ++index) { + const auto entry_types{extract_type_names(disallow->at(index))}; + if (entry_types.empty()) { + continue; + } + + const bool all_in_parent{std::ranges::all_of( + entry_types, [&parent_type_names](const auto &type_name) -> auto { + return parent_type_names.contains(type_name); + })}; + if (!all_in_parent) { + continue; + } + + locations.push_back(sourcemeta::core::Pointer{KEYWORD, index}); + narrowed_types.insert(entry_types.cbegin(), entry_types.cend()); + } + + ONLY_CONTINUE_IF(!locations.empty()); + ONLY_CONTINUE_IF(narrowed_types.size() < parent_type_names.size()); + + auto keyword_pointer{location.pointer}; + keyword_pointer.push_back(std::cref(KEYWORD)); + ONLY_CONTINUE_IF(!frame.has_references_through(keyword_pointer)); + + this->locations_ = std::move(locations); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + std::unordered_set narrowed_types; + std::vector dead_indices; + dead_indices.reserve(this->locations_.size()); + const auto &disallow{schema.at("disallow")}; + for (const auto &location : this->locations_) { + assert(location.size() == 2); + const auto index{location.at(1).to_index()}; + dead_indices.push_back(index); + const auto entry_types{extract_type_names(disallow.at(index))}; + narrowed_types.insert(entry_types.cbegin(), entry_types.cend()); + } + + auto new_type{sourcemeta::core::JSON::make_array()}; + for (const auto &entry : schema.at("type").as_array()) { + if (entry.is_string() && !narrowed_types.contains(entry.to_string())) { + new_type.push_back(entry); + } + } + schema.assign("type", std::move(new_type)); + + auto new_disallow{sourcemeta::core::JSON::make_array()}; + for (std::size_t index = 0; index < disallow.size(); ++index) { + if (std::ranges::find(dead_indices, index) == dead_indices.end()) { + new_disallow.push_back(disallow.at(index)); + } + } + if (new_disallow.empty()) { + schema.erase("disallow"); + } else { + schema.assign("disallow", std::move(new_disallow)); + } + } + +private: + static auto extract_type_names(const sourcemeta::core::JSON &entry) + -> std::unordered_set { + std::unordered_set result; + if (entry.is_string()) { + if (entry.to_string() != "any") { + result.insert(entry.to_string()); + } + return result; + } + if (!entry.is_object() || entry.size() != 1) { + return result; + } + const auto *entry_type{entry.try_at("type")}; + if (!entry_type) { + return result; + } + if (entry_type->is_string()) { + if (entry_type->to_string() != "any") { + result.insert(entry_type->to_string()); + } + return result; + } + if (!entry_type->is_array()) { + return result; + } + for (const auto &type_entry : entry_type->as_array()) { + if (!type_entry.is_string() || type_entry.to_string() == "any") { + return std::unordered_set{}; + } + result.insert(type_entry.to_string()); + } + return result; + } + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_to_array_of_schemas.h b/vendor/blaze/src/canonicalizer/rules/disallow_to_array_of_schemas.h similarity index 62% rename from vendor/blaze/src/alterschema/canonicalizer/disallow_to_array_of_schemas.h rename to vendor/blaze/src/canonicalizer/rules/disallow_to_array_of_schemas.h index c50b213bb..5889c05fb 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/disallow_to_array_of_schemas.h +++ b/vendor/blaze/src/canonicalizer/rules/disallow_to_array_of_schemas.h @@ -1,9 +1,8 @@ class DisallowToArrayOfSchemas final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; DisallowToArrayOfSchemas() - : SchemaTransformRule{"disallow_to_array_of_schemas", ""} {}; + : SchemaTransformRule{"disallow_to_array_of_schemas"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class DisallowToArrayOfSchemas final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1, @@ -41,11 +39,11 @@ class DisallowToArrayOfSchemas final : public SchemaTransformRule { return false; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { const auto &disallow{schema.at("disallow")}; if (disallow.is_string()) { - auto array{JSON::make_array()}; + auto array{sourcemeta::core::JSON::make_array()}; if (this->convert_to_schemas_) { array.push_back(type_string_to_schema(disallow.to_string())); } else { @@ -55,7 +53,7 @@ class DisallowToArrayOfSchemas final : public SchemaTransformRule { return; } - auto new_array{JSON::make_array()}; + auto new_array{sourcemeta::core::JSON::make_array()}; for (const auto &element : disallow.as_array()) { if (element.is_string() && this->convert_to_schemas_) { new_array.push_back(type_string_to_schema(element.to_string())); @@ -66,39 +64,42 @@ class DisallowToArrayOfSchemas final : public SchemaTransformRule { schema.assign("disallow", std::move(new_array)); } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { - return target.rebase(current.concat("disallow"), - current.concat(Pointer{"disallow", 0})); + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + return target.rebase( + current.concat("disallow"), + current.concat(sourcemeta::core::Pointer{"disallow", 0})); } private: - static auto type_string_to_schema(const std::string &type_name) -> JSON { + static auto type_string_to_schema(const std::string &type_name) + -> sourcemeta::core::JSON { if (type_name == "null") { - auto result{JSON::make_object()}; - auto values{JSON::make_array()}; - values.push_back(JSON{nullptr}); + auto result{sourcemeta::core::JSON::make_object()}; + auto values{sourcemeta::core::JSON::make_array()}; + values.push_back(sourcemeta::core::JSON{nullptr}); result.assign("enum", std::move(values)); return result; } if (type_name == "boolean") { - auto result{JSON::make_object()}; - auto values{JSON::make_array()}; - values.push_back(JSON{false}); - values.push_back(JSON{true}); + auto result{sourcemeta::core::JSON::make_object()}; + auto values{sourcemeta::core::JSON::make_array()}; + values.push_back(sourcemeta::core::JSON{false}); + values.push_back(sourcemeta::core::JSON{true}); result.assign("enum", std::move(values)); return result; } if (type_name == "any") { - return JSON::make_object(); + return sourcemeta::core::JSON::make_object(); } - auto result{JSON::make_object()}; - result.assign("type", JSON{type_name}); + auto result{sourcemeta::core::JSON::make_object()}; + result.assign("type", sourcemeta::core::JSON{type_name}); return result; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_type_union_to_extends.h b/vendor/blaze/src/canonicalizer/rules/disallow_type_union_to_extends.h similarity index 79% rename from vendor/blaze/src/alterschema/canonicalizer/disallow_type_union_to_extends.h rename to vendor/blaze/src/canonicalizer/rules/disallow_type_union_to_extends.h index 518e40fdb..ef86637d1 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/disallow_type_union_to_extends.h +++ b/vendor/blaze/src/canonicalizer/rules/disallow_type_union_to_extends.h @@ -1,13 +1,8 @@ class DisallowTypeUnionToExtends final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; DisallowTypeUnionToExtends() - : SchemaTransformRule{ - "disallow_type_union_to_extends", - "Negating a disjunction is the conjunction of the negations: a " - "`type` union under `disallow` becomes an `extends` where each " - "branch is its own single negation"} {}; + : SchemaTransformRule{"disallow_type_union_to_extends"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -16,8 +11,7 @@ class DisallowTypeUnionToExtends final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && @@ -42,7 +36,7 @@ class DisallowTypeUnionToExtends final : public SchemaTransformRule { // The union members relocate to distinct `extends` branches (handled by // `rereference`), but the wrapper schema itself is dissolved rather than // moved, so a reference straight at it has no new home: bail in that case - static const JSON::String DISALLOW{"disallow"}; + static const sourcemeta::core::JSON::String DISALLOW{"disallow"}; auto wrapper_pointer{location.pointer}; wrapper_pointer.push_back(std::cref(DISALLOW)); wrapper_pointer.push_back(static_cast(0)); @@ -51,12 +45,12 @@ class DisallowTypeUnionToExtends final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { - auto branches{JSON::make_array()}; + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto branches{sourcemeta::core::JSON::make_array()}; for (auto &member : schema.at("disallow").at(0).at("type").as_array()) { - auto negation{JSON::make_array()}; + auto negation{sourcemeta::core::JSON::make_array()}; negation.push_back(std::move(member)); - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; branch.assign("disallow", std::move(negation)); branches.push_back(std::move(branch)); } @@ -65,10 +59,11 @@ class DisallowTypeUnionToExtends final : public SchemaTransformRule { schema.assign("extends", std::move(branches)); } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { const auto type_prefix{current.concat({"disallow", 0, "type"})}; if (!target.starts_with(type_prefix)) { return target; diff --git a/vendor/blaze/src/alterschema/canonicalizer/divisible_by_implicit.h b/vendor/blaze/src/canonicalizer/rules/divisible_by_implicit.h similarity index 80% rename from vendor/blaze/src/alterschema/canonicalizer/divisible_by_implicit.h rename to vendor/blaze/src/canonicalizer/rules/divisible_by_implicit.h index e6a0fdcd1..3c99b51ce 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/divisible_by_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/divisible_by_implicit.h @@ -1,8 +1,7 @@ class DivisibleByImplicit final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - DivisibleByImplicit() : SchemaTransformRule{"divisible_by_implicit", ""} {}; + DivisibleByImplicit() : SchemaTransformRule{"divisible_by_implicit"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,8 +10,7 @@ class DivisibleByImplicit final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_3}) && @@ -24,7 +22,7 @@ class DivisibleByImplicit final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.assign("divisibleBy", sourcemeta::core::JSON{1}); } }; diff --git a/vendor/blaze/src/canonicalizer/rules/double_negation_elimination.h b/vendor/blaze/src/canonicalizer/rules/double_negation_elimination.h new file mode 100644 index 000000000..a60b15280 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/double_negation_elimination.h @@ -0,0 +1,75 @@ +class DoubleNegationElimination final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + DoubleNegationElimination() + : SchemaTransformRule{"double_negation_elimination"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"not"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object()); + + const auto *outer_not{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(outer_not && outer_not->is_object() && + outer_not->size() == 1); + const auto *inner_not{outer_not->try_at(KEYWORD)}; + ONLY_CONTINUE_IF(inner_not && + !(inner_not->is_boolean() && !inner_not->to_boolean())); + ONLY_CONTINUE_IF( + !(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Unevaluated, + Vocabularies::Known::JSON_Schema_2019_09_Applicator}) && + (schema.defines("unevaluatedProperties") || + schema.defines("unevaluatedItems")))); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto inner{schema.at("not").at("not")}; + schema.erase("not"); + + while (inner.is_object() && inner.size() == 1 && inner.defines("not") && + inner.at("not").is_object() && inner.at("not").size() == 1 && + inner.at("not").defines("not") && + !(inner.at("not").at("not").is_boolean() && + !inner.at("not").at("not").to_boolean())) { + auto next{inner.at("not").at("not")}; + inner = std::move(next); + } + + if (inner.is_object()) { + schema.merge(inner.as_object()); + } + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + auto old_prefix{current.concat(sourcemeta::core::Pointer{"not", "not"})}; + while (target.starts_with( + old_prefix.concat(sourcemeta::core::Pointer{"not", "not"}))) { + old_prefix = old_prefix.concat(sourcemeta::core::Pointer{"not", "not"}); + } + if (!target.starts_with(old_prefix)) { + return target; + } + return target.rebase(old_prefix, current); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/draft3_type_any.h b/vendor/blaze/src/canonicalizer/rules/draft3_type_any.h similarity index 82% rename from vendor/blaze/src/alterschema/canonicalizer/draft3_type_any.h rename to vendor/blaze/src/canonicalizer/rules/draft3_type_any.h index d16c1b73b..84438b74d 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/draft3_type_any.h +++ b/vendor/blaze/src/canonicalizer/rules/draft3_type_any.h @@ -1,8 +1,7 @@ class Draft3TypeAny final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - Draft3TypeAny() : SchemaTransformRule{"draft3_type_any", ""} {}; + Draft3TypeAny() : SchemaTransformRule{"draft3_type_any"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,8 +10,7 @@ class Draft3TypeAny final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_3) && schema.is_object()); @@ -47,7 +45,7 @@ class Draft3TypeAny final : public SchemaTransformRule { return false; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.erase("type"); } }; diff --git a/vendor/blaze/src/canonicalizer/rules/draft_official_dialect_with_https.h b/vendor/blaze/src/canonicalizer/rules/draft_official_dialect_with_https.h new file mode 100644 index 000000000..c50fd1312 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/draft_official_dialect_with_https.h @@ -0,0 +1,71 @@ +class DraftOfficialDialectWithHttps final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + DraftOfficialDialectWithHttps() + : SchemaTransformRule{"draft_official_dialect_with_https"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + using sourcemeta::blaze::SchemaBaseDialect; + ONLY_CONTINUE_IF( + location.base_dialect == SchemaBaseDialect::JSON_Schema_Draft_7 || + location.base_dialect == SchemaBaseDialect::JSON_Schema_Draft_7_Hyper || + location.base_dialect == SchemaBaseDialect::JSON_Schema_Draft_6 || + location.base_dialect == SchemaBaseDialect::JSON_Schema_Draft_6_Hyper || + location.base_dialect == SchemaBaseDialect::JSON_Schema_Draft_4 || + location.base_dialect == SchemaBaseDialect::JSON_Schema_Draft_4_Hyper || + location.base_dialect == SchemaBaseDialect::JSON_Schema_Draft_3 || + location.base_dialect == SchemaBaseDialect::JSON_Schema_Draft_3_Hyper || + location.base_dialect == SchemaBaseDialect::JSON_Schema_Draft_2_Hyper || + location.base_dialect == SchemaBaseDialect::JSON_Schema_Draft_1_Hyper || + location.base_dialect == SchemaBaseDialect::JSON_Schema_Draft_0_Hyper); + ONLY_CONTINUE_IF(schema.is_object()); + const auto *schema_keyword{schema.try_at("$schema")}; + ONLY_CONTINUE_IF(schema_keyword && schema_keyword->is_string()); + const auto &dialect{schema_keyword->to_string()}; + ONLY_CONTINUE_IF(dialect.starts_with("https://json-schema.org/")); + ONLY_CONTINUE_IF( + dialect == "https://json-schema.org/draft-07/schema" || + dialect == "https://json-schema.org/draft-07/schema#" || + dialect == "https://json-schema.org/draft-07/hyper-schema" || + dialect == "https://json-schema.org/draft-07/hyper-schema#" || + dialect == "https://json-schema.org/draft-06/schema" || + dialect == "https://json-schema.org/draft-06/schema#" || + dialect == "https://json-schema.org/draft-06/hyper-schema" || + dialect == "https://json-schema.org/draft-06/hyper-schema#" || + dialect == "https://json-schema.org/draft-04/schema" || + dialect == "https://json-schema.org/draft-04/schema#" || + dialect == "https://json-schema.org/draft-04/hyper-schema" || + dialect == "https://json-schema.org/draft-04/hyper-schema#" || + dialect == "https://json-schema.org/draft-03/schema" || + dialect == "https://json-schema.org/draft-03/schema#" || + dialect == "https://json-schema.org/draft-03/hyper-schema" || + dialect == "https://json-schema.org/draft-03/hyper-schema#" || + dialect == "https://json-schema.org/draft-02/schema" || + dialect == "https://json-schema.org/draft-02/schema#" || + dialect == "https://json-schema.org/draft-02/hyper-schema" || + dialect == "https://json-schema.org/draft-02/hyper-schema#" || + dialect == "https://json-schema.org/draft-01/schema" || + dialect == "https://json-schema.org/draft-01/schema#" || + dialect == "https://json-schema.org/draft-01/hyper-schema" || + dialect == "https://json-schema.org/draft-01/hyper-schema#" || + dialect == "https://json-schema.org/draft-00/schema" || + dialect == "https://json-schema.org/draft-00/schema#" || + dialect == "https://json-schema.org/draft-00/hyper-schema" || + dialect == "https://json-schema.org/draft-00/hyper-schema#"); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + const auto &old_dialect{schema.at("$schema").to_string()}; + std::string new_dialect{"http://"}; + new_dialect += old_dialect.substr(8); + schema.at("$schema").into(sourcemeta::core::JSON{new_dialect}); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/draft_official_dialect_without_empty_fragment.h b/vendor/blaze/src/canonicalizer/rules/draft_official_dialect_without_empty_fragment.h new file mode 100644 index 000000000..c36f2dd4e --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/draft_official_dialect_without_empty_fragment.h @@ -0,0 +1,43 @@ +class DraftOfficialDialectWithoutEmptyFragment final + : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + DraftOfficialDialectWithoutEmptyFragment() + : SchemaTransformRule{"draft_official_dialect_without_empty_fragment"} {}; + + [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const + -> bool override { + ONLY_CONTINUE_IF(schema.is_object()); + const auto *schema_keyword{schema.try_at("$schema")}; + ONLY_CONTINUE_IF(schema_keyword && schema_keyword->is_string()); + const auto &dialect{schema_keyword->to_string()}; + ONLY_CONTINUE_IF( + dialect == "http://json-schema.org/draft-07/schema" || + dialect == "http://json-schema.org/draft-07/hyper-schema" || + dialect == "http://json-schema.org/draft-06/schema" || + dialect == "http://json-schema.org/draft-06/hyper-schema" || + dialect == "http://json-schema.org/draft-04/schema" || + dialect == "http://json-schema.org/draft-04/hyper-schema" || + dialect == "http://json-schema.org/draft-03/schema" || + dialect == "http://json-schema.org/draft-03/hyper-schema" || + dialect == "http://json-schema.org/draft-02/schema" || + dialect == "http://json-schema.org/draft-02/hyper-schema" || + dialect == "http://json-schema.org/draft-01/schema" || + dialect == "http://json-schema.org/draft-01/hyper-schema" || + dialect == "http://json-schema.org/draft-00/schema" || + dialect == "http://json-schema.org/draft-00/hyper-schema"); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto dialect{std::move(schema.at("$schema")).to_string()}; + dialect += "#"; + schema.at("$schema").into(sourcemeta::core::JSON{dialect}); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/draft_ref_siblings.h b/vendor/blaze/src/canonicalizer/rules/draft_ref_siblings.h new file mode 100644 index 000000000..a63bf2941 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/draft_ref_siblings.h @@ -0,0 +1,52 @@ +class DraftRefSiblings final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + DraftRefSiblings() : SchemaTransformRule{"draft_ref_siblings"} {} + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1, + Vocabularies::Known::JSON_Schema_Draft_0})); + ONLY_CONTINUE_IF(schema.is_object() && schema.defines("$ref")); + + std::vector locations; + for (const auto &entry : schema.as_object()) { + const auto &metadata{walker(entry.first, vocabularies)}; + if (metadata.type == sourcemeta::blaze::SchemaKeywordType::Reference || + metadata.type == sourcemeta::blaze::SchemaKeywordType::Comment || + // If we disallow this, we end up deleting it and the linter will fail + // with an error about not knowing the dialect + entry.first == "$schema") { + continue; + } else { + locations.push_back(sourcemeta::core::Pointer{entry.first}); + } + } + + ONLY_CONTINUE_IF(!locations.empty()); + this->locations_ = std::move(locations); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + for (const auto &location : this->locations_) { + schema.erase(location.at(0).to_property()); + } + } + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/drop_allof_empty_schemas.h b/vendor/blaze/src/canonicalizer/rules/drop_allof_empty_schemas.h new file mode 100644 index 000000000..cfe3f468d --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/drop_allof_empty_schemas.h @@ -0,0 +1,41 @@ +class DropAllOfEmptySchemas final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + DropAllOfEmptySchemas() : SchemaTransformRule{"drop_allof_empty_schemas"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, const Vocabularies &vocabularies, + const SchemaFrame &, const SchemaFrame::Location &, + const SchemaWalker &, const SchemaResolver &) const + -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4})); + 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()); + ONLY_CONTINUE_IF(std::ranges::any_of(all_of->as_array(), is_empty_schema)); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto new_allof{sourcemeta::core::JSON::make_array()}; + for (const auto &entry : schema.at("allOf").as_array()) { + if (!is_empty_schema(entry)) { + new_allof.push_back(entry); + } + } + + if (new_allof.empty()) { + schema.erase("allOf"); + } else { + // Re-assign instead of the deleting in place to invalid memory addresses + // and avoid confusing the transformer + schema.assign("allOf", std::move(new_allof)); + } + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/drop_extends_empty_schemas.h b/vendor/blaze/src/canonicalizer/rules/drop_extends_empty_schemas.h new file mode 100644 index 000000000..3fa45a2bb --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/drop_extends_empty_schemas.h @@ -0,0 +1,70 @@ +class DropExtendsEmptySchemas final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + DropExtendsEmptySchemas() + : SchemaTransformRule{"drop_extends_empty_schemas"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"extends"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *extends{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(extends); + + auto keyword_pointer{location.pointer}; + keyword_pointer.push_back(std::cref(KEYWORD)); + ONLY_CONTINUE_IF(!frame.has_references_through(keyword_pointer)); + + if (sourcemeta::blaze::is_empty_schema(*extends)) { + this->locations_ = {sourcemeta::core::Pointer{KEYWORD}}; + return true; + } + + if (extends->is_array() && !extends->empty()) { + std::vector locations; + for (std::size_t index = 0; index < extends->size(); ++index) { + if (sourcemeta::blaze::is_empty_schema(extends->at(index))) { + locations.push_back(sourcemeta::core::Pointer{KEYWORD, index}); + } + } + ONLY_CONTINUE_IF(!locations.empty()); + this->locations_ = std::move(locations); + return true; + } + + return false; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + if (this->locations_.size() == 1 && this->locations_.at(0).size() == 1) { + schema.erase("extends"); + return; + } + + auto new_extends{sourcemeta::core::JSON::make_array()}; + for (const auto &entry : schema.at("extends").as_array()) { + if (!sourcemeta::blaze::is_empty_schema(entry)) { + new_extends.push_back(entry); + } + } + + if (new_extends.empty()) { + schema.erase("extends"); + } else { + schema.assign("extends", std::move(new_extends)); + } + } + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/duplicate_allof_branches.h b/vendor/blaze/src/canonicalizer/rules/duplicate_allof_branches.h new file mode 100644 index 000000000..64a6ee478 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/duplicate_allof_branches.h @@ -0,0 +1,73 @@ +class DuplicateAllOfBranches final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + DuplicateAllOfBranches() : SchemaTransformRule{"duplicate_allof_branches"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object()); + + const auto *all_of{schema.try_at("allOf")}; + ONLY_CONTINUE_IF(all_of && all_of->is_array() && !all_of->unique()); + // TODO: Highlight which specific entries in `allOf` are duplicated + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + this->index_mapping_.clear(); + const auto &original{schema.at("allOf")}; + + std::unordered_map< + std::reference_wrapper, std::size_t, + sourcemeta::core::HashJSON< + std::reference_wrapper>, + sourcemeta::core::EqualJSON< + std::reference_wrapper>> + seen; + auto result{sourcemeta::core::JSON::make_array()}; + + for (std::size_t index = 0; index < original.size(); ++index) { + const auto &value{original.at(index)}; + const auto match{seen.find(std::cref(value))}; + + if (match == seen.end()) { + this->index_mapping_[index] = seen.size(); + seen.emplace(std::cref(value), seen.size()); + result.push_back(value); + } else { + this->index_mapping_[index] = match->second; + } + } + + schema.assign("allOf", std::move(result)); + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + const auto allof_prefix{current.concat("allOf")}; + const auto relative{target.resolve_from(allof_prefix)}; + const auto old_index{relative.at(0).to_index()}; + const auto new_index{this->index_mapping_.at(old_index)}; + const sourcemeta::core::Pointer old_prefix{allof_prefix.concat(old_index)}; + const sourcemeta::core::Pointer new_prefix{allof_prefix.concat(new_index)}; + return target.rebase(old_prefix, new_prefix); + } + +private: + mutable std::unordered_map index_mapping_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/duplicate_anyof_branches.h b/vendor/blaze/src/canonicalizer/rules/duplicate_anyof_branches.h new file mode 100644 index 000000000..a967bbbfe --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/duplicate_anyof_branches.h @@ -0,0 +1,73 @@ +class DuplicateAnyOfBranches final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + DuplicateAnyOfBranches() : SchemaTransformRule{"duplicate_anyof_branches"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object()); + + const auto *any_of{schema.try_at("anyOf")}; + ONLY_CONTINUE_IF(any_of && any_of->is_array() && !any_of->unique()); + // TODO: Highlight which specific entries in `anyOf` are duplicated + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + this->index_mapping_.clear(); + const auto &original{schema.at("anyOf")}; + + std::unordered_map< + std::reference_wrapper, std::size_t, + sourcemeta::core::HashJSON< + std::reference_wrapper>, + sourcemeta::core::EqualJSON< + std::reference_wrapper>> + seen; + auto result{sourcemeta::core::JSON::make_array()}; + + for (std::size_t index = 0; index < original.size(); ++index) { + const auto &value{original.at(index)}; + const auto match{seen.find(std::cref(value))}; + + if (match == seen.end()) { + this->index_mapping_[index] = seen.size(); + seen.emplace(std::cref(value), seen.size()); + result.push_back(value); + } else { + this->index_mapping_[index] = match->second; + } + } + + schema.assign("anyOf", std::move(result)); + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + const auto anyof_prefix{current.concat("anyOf")}; + const auto relative{target.resolve_from(anyof_prefix)}; + const auto old_index{relative.at(0).to_index()}; + const auto new_index{this->index_mapping_.at(old_index)}; + const sourcemeta::core::Pointer old_prefix{anyof_prefix.concat(old_index)}; + const sourcemeta::core::Pointer new_prefix{anyof_prefix.concat(new_index)}; + return target.rebase(old_prefix, new_prefix); + } + +private: + mutable std::unordered_map index_mapping_; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/duplicate_disallow_entries.h b/vendor/blaze/src/canonicalizer/rules/duplicate_disallow_entries.h similarity index 67% rename from vendor/blaze/src/alterschema/canonicalizer/duplicate_disallow_entries.h rename to vendor/blaze/src/canonicalizer/rules/duplicate_disallow_entries.h index aecdcda26..594b67940 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/duplicate_disallow_entries.h +++ b/vendor/blaze/src/canonicalizer/rules/duplicate_disallow_entries.h @@ -1,13 +1,8 @@ class DuplicateDisallowEntries final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; DuplicateDisallowEntries() - : SchemaTransformRule{ - "duplicate_disallow_entries", - "Setting duplicate subschemas in `disallow` is redundant, as " - "negating the same subschema more than once is guaranteed to not " - "affect the validation result"} {}; + : SchemaTransformRule{"duplicate_disallow_entries"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -16,8 +11,7 @@ class DuplicateDisallowEntries final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && @@ -33,19 +27,23 @@ class DuplicateDisallowEntries final : public SchemaTransformRule { // as its own `extends` branch const std::string keyword{"disallow"}; ONLY_CONTINUE_IF(!frame.has_references_through( - location.pointer, WeakPointer::Token{std::cref(keyword)})); + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(keyword)})); return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { const auto &original{schema.at("disallow")}; - std::unordered_set, - HashJSON>, - EqualJSON>> + std::unordered_set< + std::reference_wrapper, + sourcemeta::core::HashJSON< + std::reference_wrapper>, + sourcemeta::core::EqualJSON< + std::reference_wrapper>> seen; - auto result{JSON::make_array()}; + auto result{sourcemeta::core::JSON::make_array()}; for (const auto &element : original.as_array()) { if (seen.emplace(std::cref(element)).second) { diff --git a/vendor/blaze/src/canonicalizer/rules/duplicate_enum_values.h b/vendor/blaze/src/canonicalizer/rules/duplicate_enum_values.h new file mode 100644 index 000000000..6e73964a0 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/duplicate_enum_values.h @@ -0,0 +1,50 @@ +class DuplicateEnumValues final : public SchemaTransformRule { +public: + using reframe_after_transform = std::false_type; + DuplicateEnumValues() : SchemaTransformRule{"duplicate_enum_values"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1}) && + schema.is_object()); + + const auto *enum_value{schema.try_at("enum")}; + ONLY_CONTINUE_IF(enum_value && enum_value->is_array() && + !enum_value->unique()); + // TODO: Highlight which specific entries in `enum` are duplicated + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + // We want to be super careful to maintain the current ordering + // as we delete the duplicates + auto &enumeration{schema.at("enum")}; + std::unordered_set> + cache; + for (auto iterator = enumeration.as_array().cbegin(); + iterator != enumeration.as_array().cend();) { + if (cache.contains(*iterator)) { + iterator = enumeration.erase(iterator); + } else { + cache.emplace(*iterator); + iterator++; + } + } + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/duplicate_required_values.h b/vendor/blaze/src/canonicalizer/rules/duplicate_required_values.h new file mode 100644 index 000000000..3bcbbbe35 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/duplicate_required_values.h @@ -0,0 +1,37 @@ +class DuplicateRequiredValues final : public SchemaTransformRule { +public: + using reframe_after_transform = std::false_type; + DuplicateRequiredValues() + : SchemaTransformRule{"duplicate_required_values"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object()); + + const auto *required{schema.try_at("required")}; + ONLY_CONTINUE_IF(required && required->is_array() && !required->unique()); + // TODO: Highlight which specific entries in `required` are duplicated + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto collection = schema.at("required"); + std::sort(collection.as_array().begin(), collection.as_array().end()); + auto last = + std::unique(collection.as_array().begin(), collection.as_array().end()); + collection.erase(last, collection.as_array().end()); + schema.at("required").into(std::move(collection)); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/dynamic_ref_to_static_ref.h b/vendor/blaze/src/canonicalizer/rules/dynamic_ref_to_static_ref.h new file mode 100644 index 000000000..5ef592990 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/dynamic_ref_to_static_ref.h @@ -0,0 +1,114 @@ +class DynamicRefToStaticRef final : public SchemaTransformRule { +private: + // NOLINTNEXTLINE(bugprone-throwing-static-initialization) + static inline const std::string KEYWORD_DYNAMIC_REF{"$dynamicRef"}; + // NOLINTNEXTLINE(bugprone-throwing-static-initialization) + static inline const std::string KEYWORD_RECURSIVE_REF{"$recursiveRef"}; + +public: + using reframe_after_transform = std::true_type; + DynamicRefToStaticRef() : SchemaTransformRule{"dynamic_ref_to_static_ref"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &root, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(schema.is_object() && !schema.defines("$ref")); + + if (vocabularies.contains(Vocabularies::Known::JSON_Schema_2020_12_Core) && + schema.defines("$dynamicRef")) { + auto reference_pointer{location.pointer}; + reference_pointer.push_back(std::cref(KEYWORD_DYNAMIC_REF)); + + auto reference_entry{frame.reference( + sourcemeta::blaze::SchemaReferenceType::Static, reference_pointer)}; + if (!reference_entry.has_value()) { + reference_entry = frame.reference( + sourcemeta::blaze::SchemaReferenceType::Dynamic, reference_pointer); + } + if (!reference_entry.has_value()) { + return false; + } + + const auto destination{ + frame.traverse(reference_entry->get().destination)}; + if (!destination.has_value()) { + return false; + } + + if (destination->get().type == + sourcemeta::blaze::SchemaFrame::LocationType::Anchor) { + const auto &subschema{sourcemeta::core::get( + root, sourcemeta::core::to_pointer(destination->get().pointer))}; + if (subschema.is_object()) { + const auto *dynamic_anchor{subschema.try_at("$dynamicAnchor")}; + if (dynamic_anchor != nullptr && dynamic_anchor->is_string()) { + const auto &destination_uri{reference_entry->get().destination}; + const auto fragment_position{destination_uri.find('#')}; + const std::string_view fragment{ + fragment_position == std::string::npos + ? std::string_view{destination_uri} + : std::string_view{ + destination_uri.data() + fragment_position + 1, + destination_uri.size() - fragment_position - 1}}; + if (fragment == dynamic_anchor->to_string()) { + return false; + } + } + } + } + + this->keyword_ = &KEYWORD_DYNAMIC_REF; + return true; + } + + if (vocabularies.contains(Vocabularies::Known::JSON_Schema_2019_09_Core) && + schema.defines("$recursiveRef")) { + auto reference_pointer{location.pointer}; + reference_pointer.push_back(std::cref(KEYWORD_RECURSIVE_REF)); + + auto reference_entry{frame.reference( + sourcemeta::blaze::SchemaReferenceType::Static, reference_pointer)}; + if (!reference_entry.has_value()) { + reference_entry = frame.reference( + sourcemeta::blaze::SchemaReferenceType::Dynamic, reference_pointer); + } + if (!reference_entry.has_value()) { + return false; + } + + const auto destination{ + frame.traverse(reference_entry->get().destination)}; + if (!destination.has_value()) { + return false; + } + + const auto &subschema{sourcemeta::core::get( + root, sourcemeta::core::to_pointer(destination->get().pointer))}; + if (subschema.is_object()) { + const auto *recursive_anchor{subschema.try_at("$recursiveAnchor")}; + if (recursive_anchor != nullptr && recursive_anchor->is_boolean() && + recursive_anchor->to_boolean()) { + return false; + } + } + + this->keyword_ = &KEYWORD_RECURSIVE_REF; + return true; + } + + return false; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + assert(this->keyword_ != nullptr); + schema.rename(*this->keyword_, "$ref"); + } + +private: + mutable const std::string *keyword_{nullptr}; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/else_without_if.h b/vendor/blaze/src/canonicalizer/rules/else_without_if.h new file mode 100644 index 000000000..6e2a913a2 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/else_without_if.h @@ -0,0 +1,33 @@ +class ElseWithoutIf final : public SchemaTransformRule { +private: + // NOLINTNEXTLINE(bugprone-throwing-static-initialization) + static inline const std::string KEYWORD{"else"}; + +public: + using reframe_after_transform = std::true_type; + ElseWithoutIf() : SchemaTransformRule{"else_without_if"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7}) && + schema.is_object() && schema.defines(KEYWORD) && + !schema.defines("if")); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase(KEYWORD); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/empty_definitions_drop.h b/vendor/blaze/src/canonicalizer/rules/empty_definitions_drop.h similarity index 81% rename from vendor/blaze/src/alterschema/canonicalizer/empty_definitions_drop.h rename to vendor/blaze/src/canonicalizer/rules/empty_definitions_drop.h index a2fff0e0b..efb8fbfea 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/empty_definitions_drop.h +++ b/vendor/blaze/src/canonicalizer/rules/empty_definitions_drop.h @@ -1,8 +1,7 @@ class EmptyDefinitionsDrop final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - EmptyDefinitionsDrop() : SchemaTransformRule{"empty_definitions_drop", ""} {}; + EmptyDefinitionsDrop() : SchemaTransformRule{"empty_definitions_drop"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,8 +10,7 @@ class EmptyDefinitionsDrop final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_6, @@ -25,7 +23,7 @@ class EmptyDefinitionsDrop final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.erase("definitions"); } }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/empty_defs_drop.h b/vendor/blaze/src/canonicalizer/rules/empty_defs_drop.h similarity index 74% rename from vendor/blaze/src/alterschema/canonicalizer/empty_defs_drop.h rename to vendor/blaze/src/canonicalizer/rules/empty_defs_drop.h index ad0e80eb0..f6da47a11 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/empty_defs_drop.h +++ b/vendor/blaze/src/canonicalizer/rules/empty_defs_drop.h @@ -1,8 +1,7 @@ class EmptyDefsDrop final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - EmptyDefsDrop() : SchemaTransformRule{"empty_defs_drop", ""} {}; + EmptyDefsDrop() : SchemaTransformRule{"empty_defs_drop"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,8 +10,7 @@ class EmptyDefsDrop final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2019_09_Core, Vocabularies::Known::JSON_Schema_2020_12_Core}) && @@ -23,7 +21,7 @@ class EmptyDefsDrop final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.erase("$defs"); } }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/empty_dependencies_drop.h b/vendor/blaze/src/canonicalizer/rules/empty_dependencies_drop.h similarity index 77% rename from vendor/blaze/src/alterschema/canonicalizer/empty_dependencies_drop.h rename to vendor/blaze/src/canonicalizer/rules/empty_dependencies_drop.h index 249bdb7b5..0cbe43085 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/empty_dependencies_drop.h +++ b/vendor/blaze/src/canonicalizer/rules/empty_dependencies_drop.h @@ -1,9 +1,7 @@ class EmptyDependenciesDrop final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - EmptyDependenciesDrop() - : SchemaTransformRule{"empty_dependencies_drop", ""} {}; + EmptyDependenciesDrop() : SchemaTransformRule{"empty_dependencies_drop"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +10,7 @@ class EmptyDependenciesDrop final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_4, @@ -27,7 +24,7 @@ class EmptyDependenciesDrop final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.erase("dependencies"); } }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/empty_dependent_required_drop.h b/vendor/blaze/src/canonicalizer/rules/empty_dependent_required_drop.h similarity index 77% rename from vendor/blaze/src/alterschema/canonicalizer/empty_dependent_required_drop.h rename to vendor/blaze/src/canonicalizer/rules/empty_dependent_required_drop.h index 6e04d1fa8..55671c60a 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/empty_dependent_required_drop.h +++ b/vendor/blaze/src/canonicalizer/rules/empty_dependent_required_drop.h @@ -1,9 +1,8 @@ class EmptyDependentRequiredDrop final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; EmptyDependentRequiredDrop() - : SchemaTransformRule{"empty_dependent_required_drop", ""} {}; + : SchemaTransformRule{"empty_dependent_required_drop"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class EmptyDependentRequiredDrop final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2019_09_Validation, @@ -26,7 +24,7 @@ class EmptyDependentRequiredDrop final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.erase("dependentRequired"); } }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/empty_dependent_schemas_drop.h b/vendor/blaze/src/canonicalizer/rules/empty_dependent_schemas_drop.h similarity index 77% rename from vendor/blaze/src/alterschema/canonicalizer/empty_dependent_schemas_drop.h rename to vendor/blaze/src/canonicalizer/rules/empty_dependent_schemas_drop.h index 794fb530b..302bac234 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/empty_dependent_schemas_drop.h +++ b/vendor/blaze/src/canonicalizer/rules/empty_dependent_schemas_drop.h @@ -1,9 +1,8 @@ class EmptyDependentSchemasDrop final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; EmptyDependentSchemasDrop() - : SchemaTransformRule{"empty_dependent_schemas_drop", ""} {}; + : SchemaTransformRule{"empty_dependent_schemas_drop"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class EmptyDependentSchemasDrop final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2019_09_Applicator, @@ -26,7 +24,7 @@ class EmptyDependentSchemasDrop final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.erase("dependentSchemas"); } }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/empty_disallow_drop.h b/vendor/blaze/src/canonicalizer/rules/empty_disallow_drop.h similarity index 80% rename from vendor/blaze/src/alterschema/canonicalizer/empty_disallow_drop.h rename to vendor/blaze/src/canonicalizer/rules/empty_disallow_drop.h index 429e85c7f..6aad634ad 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/empty_disallow_drop.h +++ b/vendor/blaze/src/canonicalizer/rules/empty_disallow_drop.h @@ -1,8 +1,7 @@ class EmptyDisallowDrop final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - EmptyDisallowDrop() : SchemaTransformRule{"empty_disallow_drop", ""} {}; + EmptyDisallowDrop() : SchemaTransformRule{"empty_disallow_drop"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,8 +10,7 @@ class EmptyDisallowDrop final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && @@ -23,7 +21,7 @@ class EmptyDisallowDrop final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.erase("disallow"); } }; diff --git a/vendor/blaze/src/canonicalizer/rules/empty_object_as_true.h b/vendor/blaze/src/canonicalizer/rules/empty_object_as_true.h new file mode 100644 index 000000000..d3e929cfb --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/empty_object_as_true.h @@ -0,0 +1,27 @@ +class EmptyObjectAsTrue final : public SchemaTransformRule { +public: + using reframe_after_transform = std::false_type; + EmptyObjectAsTrue() : SchemaTransformRule{"empty_object_as_true"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Core, + Vocabularies::Known::JSON_Schema_2019_09_Core, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object() && schema.empty()); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.into(sourcemeta::core::JSON{true}); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h b/vendor/blaze/src/canonicalizer/rules/enum_drop_redundant_validation.h similarity index 88% rename from vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h rename to vendor/blaze/src/canonicalizer/rules/enum_drop_redundant_validation.h index b92335042..b2a40ac51 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h +++ b/vendor/blaze/src/canonicalizer/rules/enum_drop_redundant_validation.h @@ -1,9 +1,8 @@ class EnumDropRedundantValidation final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; EnumDropRedundantValidation() - : SchemaTransformRule{"enum_drop_redundant_validation", ""} {}; + : SchemaTransformRule{"enum_drop_redundant_validation"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class EnumDropRedundantValidation final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_0, @@ -86,7 +84,8 @@ class EnumDropRedundantValidation final : public SchemaTransformRule { if (entry.second.is_boolean() && entry.second.to_boolean()) { if (!frame.has_references_through( - location.pointer, WeakPointer::Token{std::cref(entry.first)})) { + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(entry.first)})) { this->keywords_.emplace_back(entry.first); } continue; @@ -98,7 +97,8 @@ class EnumDropRedundantValidation final : public SchemaTransformRule { } if (!frame.has_references_through( - location.pointer, WeakPointer::Token{std::cref(entry.first)})) { + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(entry.first)})) { this->wrap_keywords_.emplace_back(entry.first); } } @@ -107,7 +107,7 @@ class EnumDropRedundantValidation final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { for (const auto &keyword : this->keywords_) { schema.erase(keyword); } @@ -116,9 +116,9 @@ class EnumDropRedundantValidation final : public SchemaTransformRule { return; } - auto new_allof{JSON::make_array()}; + auto new_allof{sourcemeta::core::JSON::make_array()}; for (const auto &keyword : this->wrap_keywords_) { - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; branch.assign(keyword, schema.at(keyword)); if (keyword == "if" && this->has_if_group_) { if (schema.defines("then")) { @@ -140,7 +140,7 @@ class EnumDropRedundantValidation final : public SchemaTransformRule { } } - auto enum_branch{JSON::make_object()}; + auto enum_branch{sourcemeta::core::JSON::make_object()}; enum_branch.assign("enum", schema.at("enum")); schema.erase("enum"); new_allof.push_back(std::move(enum_branch)); @@ -150,8 +150,8 @@ class EnumDropRedundantValidation final : public SchemaTransformRule { } private: - mutable std::vector keywords_; - mutable std::vector wrap_keywords_; + mutable std::vector keywords_; + mutable std::vector wrap_keywords_; mutable bool has_if_group_{false}; mutable bool is_pre_draft4_{false}; }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/enum_filter_by_type.h b/vendor/blaze/src/canonicalizer/rules/enum_filter_by_type.h similarity index 88% rename from vendor/blaze/src/alterschema/canonicalizer/enum_filter_by_type.h rename to vendor/blaze/src/canonicalizer/rules/enum_filter_by_type.h index 685ccc1ad..1ed2326ec 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/enum_filter_by_type.h +++ b/vendor/blaze/src/canonicalizer/rules/enum_filter_by_type.h @@ -1,8 +1,7 @@ class EnumFilterByType final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - EnumFilterByType() : SchemaTransformRule{"enum_filter_by_type", ""} {}; + EnumFilterByType() : SchemaTransformRule{"enum_filter_by_type"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,8 +10,7 @@ class EnumFilterByType final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_0, @@ -40,7 +38,8 @@ class EnumFilterByType final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_2020_12_Validation}) && - declared_types.test(std::to_underlying(JSON::Type::Integer))}; + declared_types.test( + std::to_underlying(sourcemeta::core::JSON::Type::Integer))}; this->matching_indices_.clear(); bool has_mismatch{false}; @@ -61,8 +60,8 @@ class EnumFilterByType final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { - auto filtered{JSON::make_array()}; + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto filtered{sourcemeta::core::JSON::make_array()}; for (const auto &index : this->matching_indices_) { filtered.push_back(schema.at("enum").at(index)); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/enum_split_by_type.h b/vendor/blaze/src/canonicalizer/rules/enum_split_by_type.h similarity index 86% rename from vendor/blaze/src/alterschema/canonicalizer/enum_split_by_type.h rename to vendor/blaze/src/canonicalizer/rules/enum_split_by_type.h index 67c3213aa..997872c42 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/enum_split_by_type.h +++ b/vendor/blaze/src/canonicalizer/rules/enum_split_by_type.h @@ -1,13 +1,7 @@ class EnumSplitByType final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - EnumSplitByType() - : SchemaTransformRule{ - "enum_split_by_type", - "An `enum` whose values span more than one type is the disjunction " - "of its single-type subsets, so it splits into a union of " - "single-type enums"} {}; + EnumSplitByType() : SchemaTransformRule{"enum_split_by_type"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -16,8 +10,7 @@ class EnumSplitByType final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { const bool any_of_dialect{ vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_6, @@ -42,7 +35,7 @@ class EnumSplitByType final : public SchemaTransformRule { ONLY_CONTINUE_IF(enumeration && enumeration->is_array() && !enumeration->empty()); - JSON::TypeSet kinds; + sourcemeta::core::JSON::TypeSet kinds; for (const auto &value : enumeration->as_array()) { kinds.set(static_cast(kind_of(value))); } @@ -60,8 +53,8 @@ class EnumSplitByType final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { - auto branches{JSON::make_array()}; + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto branches{sourcemeta::core::JSON::make_array()}; for (const auto &value : schema.at("enum").as_array()) { const auto kind{kind_of(value)}; bool merged{false}; @@ -74,9 +67,9 @@ class EnumSplitByType final : public SchemaTransformRule { } if (!merged) { - auto values{JSON::make_array()}; + auto values{sourcemeta::core::JSON::make_array()}; values.push_back(value); - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; branch.assign("enum", std::move(values)); branches.push_back(std::move(branch)); } diff --git a/vendor/blaze/src/canonicalizer/rules/enum_with_type.h b/vendor/blaze/src/canonicalizer/rules/enum_with_type.h new file mode 100644 index 000000000..09d2b3556 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/enum_with_type.h @@ -0,0 +1,84 @@ +class EnumWithType final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + EnumWithType() : SchemaTransformRule{"enum_with_type"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1})); + ONLY_CONTINUE_IF(schema.is_object()); + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type); + const auto *enum_value{schema.try_at("enum")}; + ONLY_CONTINUE_IF(enum_value && enum_value->is_array()); + + if (vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::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({Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::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()); + })); + + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase("type"); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/equal_numeric_bounds_to_const.h b/vendor/blaze/src/canonicalizer/rules/equal_numeric_bounds_to_const.h new file mode 100644 index 000000000..3de7bcfcc --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/equal_numeric_bounds_to_const.h @@ -0,0 +1,47 @@ +class EqualNumericBoundsToConst final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + EqualNumericBoundsToConst() + : SchemaTransformRule{"equal_numeric_bounds_to_const"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any({ + Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + }) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF( + type && type->is_string() && + (type->to_string() == "integer" || type->to_string() == "number")); + const auto *minimum{schema.try_at("minimum")}; + ONLY_CONTINUE_IF(minimum && minimum->is_number()); + const auto *maximum{schema.try_at("maximum")}; + ONLY_CONTINUE_IF(maximum && maximum->is_number() && *minimum == *maximum); + + const auto *exclusive_minimum{schema.try_at("exclusiveMinimum")}; + ONLY_CONTINUE_IF(!(exclusive_minimum && exclusive_minimum->is_number() && + *exclusive_minimum >= *minimum)); + const auto *exclusive_maximum{schema.try_at("exclusiveMaximum")}; + ONLY_CONTINUE_IF(!(exclusive_maximum && exclusive_maximum->is_number() && + *exclusive_maximum <= *maximum)); + + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.rename("minimum", "const"); + schema.erase("type"); + schema.erase("maximum"); + } +}; 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 new file mode 100644 index 000000000..245aacbfd --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/equal_numeric_bounds_to_enum.h @@ -0,0 +1,56 @@ +class EqualNumericBoundsToEnum final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + EqualNumericBoundsToEnum() + : SchemaTransformRule{"equal_numeric_bounds_to_enum"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1, + Vocabularies::Known::JSON_Schema_Draft_0}) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF( + type && type->is_string() && + (type->to_string() == "integer" || type->to_string() == "number")); + const auto *minimum{schema.try_at("minimum")}; + ONLY_CONTINUE_IF(minimum && minimum->is_number()); + const auto *maximum{schema.try_at("maximum")}; + ONLY_CONTINUE_IF(maximum && maximum->is_number() && *minimum == *maximum); + + const auto *exclusive_minimum{schema.try_at("exclusiveMinimum")}; + ONLY_CONTINUE_IF(!(exclusive_minimum && exclusive_minimum->is_boolean() && + exclusive_minimum->to_boolean())); + const auto *exclusive_maximum{schema.try_at("exclusiveMaximum")}; + ONLY_CONTINUE_IF(!(exclusive_maximum && exclusive_maximum->is_boolean() && + exclusive_maximum->to_boolean())); + const auto *minimum_can_equal{schema.try_at("minimumCanEqual")}; + ONLY_CONTINUE_IF(!(minimum_can_equal && minimum_can_equal->is_boolean() && + !minimum_can_equal->to_boolean())); + const auto *maximum_can_equal{schema.try_at("maximumCanEqual")}; + ONLY_CONTINUE_IF(!(maximum_can_equal && maximum_can_equal->is_boolean() && + !maximum_can_equal->to_boolean())); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + sourcemeta::core::JSON values = sourcemeta::core::JSON::make_array(); + values.push_back(schema.at("minimum")); + schema.assign("enum", std::move(values)); + schema.erase("type"); + schema.erase("minimum"); + schema.erase("maximum"); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/exclusive_bounds_false_drop.h b/vendor/blaze/src/canonicalizer/rules/exclusive_bounds_false_drop.h new file mode 100644 index 000000000..1309b4cf8 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/exclusive_bounds_false_drop.h @@ -0,0 +1,51 @@ +class ExclusiveBoundsFalseDrop final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + ExclusiveBoundsFalseDrop() + : SchemaTransformRule{"exclusive_bounds_false_drop"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF( + type && type->is_string() && + (type->to_string() == "integer" || type->to_string() == "number")); + + std::vector locations; + const auto *exclusive_min{schema.try_at("exclusiveMinimum")}; + if (exclusive_min && exclusive_min->is_boolean() && + !exclusive_min->to_boolean()) { + locations.push_back(sourcemeta::core::Pointer{"exclusiveMinimum"}); + } + const auto *exclusive_max{schema.try_at("exclusiveMaximum")}; + if (exclusive_max && exclusive_max->is_boolean() && + !exclusive_max->to_boolean()) { + locations.push_back(sourcemeta::core::Pointer{"exclusiveMaximum"}); + } + + ONLY_CONTINUE_IF(!locations.empty()); + this->locations_ = std::move(locations); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + for (const auto &location : this->locations_) { + schema.erase(location.at(0).to_property()); + } + } + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/exclusive_maximum_boolean_integer_fold.h b/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_boolean_integer_fold.h similarity index 94% rename from vendor/blaze/src/alterschema/canonicalizer/exclusive_maximum_boolean_integer_fold.h rename to vendor/blaze/src/canonicalizer/rules/exclusive_maximum_boolean_integer_fold.h index 585e8dd28..fdf36990d 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/exclusive_maximum_boolean_integer_fold.h +++ b/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_boolean_integer_fold.h @@ -1,9 +1,8 @@ class ExclusiveMaximumBooleanIntegerFold final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; ExclusiveMaximumBooleanIntegerFold() - : SchemaTransformRule{"exclusive_maximum_boolean_integer_fold", ""} {}; + : SchemaTransformRule{"exclusive_maximum_boolean_integer_fold"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class ExclusiveMaximumBooleanIntegerFold final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_4}) && @@ -30,7 +28,7 @@ class ExclusiveMaximumBooleanIntegerFold final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { const auto &maximum{schema.at("maximum")}; if (maximum.is_integer()) { if (maximum.to_integer() > std::numeric_limits::min()) { diff --git a/vendor/blaze/src/alterschema/canonicalizer/exclusive_maximum_integer_to_maximum.h b/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_integer_to_maximum.h similarity index 89% rename from vendor/blaze/src/alterschema/canonicalizer/exclusive_maximum_integer_to_maximum.h rename to vendor/blaze/src/canonicalizer/rules/exclusive_maximum_integer_to_maximum.h index 02bd609d5..1092beb08 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/exclusive_maximum_integer_to_maximum.h +++ b/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_integer_to_maximum.h @@ -1,12 +1,8 @@ class ExclusiveMaximumIntegerToMaximum final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; ExclusiveMaximumIntegerToMaximum() - : SchemaTransformRule{ - "exclusive_maximum_integer_to_maximum", - "Setting `exclusiveMaximum` when `type` is `integer` is syntax " - "sugar for `maximum`"} {}; + : SchemaTransformRule{"exclusive_maximum_integer_to_maximum"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -15,8 +11,7 @@ class ExclusiveMaximumIntegerToMaximum final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation, @@ -29,10 +24,10 @@ class ExclusiveMaximumIntegerToMaximum final : public SchemaTransformRule { type->to_string() == "integer"); const auto *exclusive_maximum{schema.try_at("exclusiveMaximum")}; ONLY_CONTINUE_IF(exclusive_maximum && exclusive_maximum->is_number()); - return APPLIES_TO_KEYWORDS("exclusiveMaximum", "type"); + return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { if (schema.at("exclusiveMaximum").is_integer()) { if (schema.at("exclusiveMaximum").to_integer() > std::numeric_limits::min()) { diff --git a/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_number_and_maximum.h b/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_number_and_maximum.h new file mode 100644 index 000000000..d288427d9 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/exclusive_maximum_number_and_maximum.h @@ -0,0 +1,36 @@ +class ExclusiveMaximumNumberAndMaximum final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + ExclusiveMaximumNumberAndMaximum() + : SchemaTransformRule{"exclusive_maximum_number_and_maximum"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6}) && + schema.is_object()); + + const auto *maximum{schema.try_at("maximum")}; + ONLY_CONTINUE_IF(maximum && maximum->is_number()); + const auto *exclusive_maximum{schema.try_at("exclusiveMaximum")}; + ONLY_CONTINUE_IF(exclusive_maximum && exclusive_maximum->is_number()); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + if (schema.at("maximum") < schema.at("exclusiveMaximum")) { + schema.erase("exclusiveMaximum"); + } else { + schema.erase("maximum"); + } + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/exclusive_minimum_boolean_integer_fold.h b/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_boolean_integer_fold.h similarity index 94% rename from vendor/blaze/src/alterschema/canonicalizer/exclusive_minimum_boolean_integer_fold.h rename to vendor/blaze/src/canonicalizer/rules/exclusive_minimum_boolean_integer_fold.h index b16a78d93..af96a4f9b 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/exclusive_minimum_boolean_integer_fold.h +++ b/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_boolean_integer_fold.h @@ -1,9 +1,8 @@ class ExclusiveMinimumBooleanIntegerFold final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; ExclusiveMinimumBooleanIntegerFold() - : SchemaTransformRule{"exclusive_minimum_boolean_integer_fold", ""} {}; + : SchemaTransformRule{"exclusive_minimum_boolean_integer_fold"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class ExclusiveMinimumBooleanIntegerFold final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_4}) && @@ -30,7 +28,7 @@ class ExclusiveMinimumBooleanIntegerFold final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { const auto &minimum{schema.at("minimum")}; if (minimum.is_integer()) { if (minimum.to_integer() < std::numeric_limits::max()) { diff --git a/vendor/blaze/src/alterschema/canonicalizer/exclusive_minimum_integer_to_minimum.h b/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_integer_to_minimum.h similarity index 89% rename from vendor/blaze/src/alterschema/canonicalizer/exclusive_minimum_integer_to_minimum.h rename to vendor/blaze/src/canonicalizer/rules/exclusive_minimum_integer_to_minimum.h index 9adaae611..9c5aa8220 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/exclusive_minimum_integer_to_minimum.h +++ b/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_integer_to_minimum.h @@ -1,12 +1,8 @@ class ExclusiveMinimumIntegerToMinimum final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; ExclusiveMinimumIntegerToMinimum() - : SchemaTransformRule{ - "exclusive_minimum_integer_to_minimum", - "Setting `exclusiveMinimum` when `type` is `integer` is syntax " - "sugar for `minimum`"} {}; + : SchemaTransformRule{"exclusive_minimum_integer_to_minimum"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -15,8 +11,7 @@ class ExclusiveMinimumIntegerToMinimum final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation, @@ -29,10 +24,10 @@ class ExclusiveMinimumIntegerToMinimum final : public SchemaTransformRule { type->to_string() == "integer"); const auto *exclusive_minimum{schema.try_at("exclusiveMinimum")}; ONLY_CONTINUE_IF(exclusive_minimum && exclusive_minimum->is_number()); - return APPLIES_TO_KEYWORDS("exclusiveMinimum", "type"); + return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { if (schema.at("exclusiveMinimum").is_integer()) { if (schema.at("exclusiveMinimum").to_integer() < std::numeric_limits::max()) { diff --git a/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_number_and_minimum.h b/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_number_and_minimum.h new file mode 100644 index 000000000..b8c47952f --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/exclusive_minimum_number_and_minimum.h @@ -0,0 +1,36 @@ +class ExclusiveMinimumNumberAndMinimum final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + ExclusiveMinimumNumberAndMinimum() + : SchemaTransformRule{"exclusive_minimum_number_and_minimum"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6}) && + schema.is_object()); + + const auto *minimum{schema.try_at("minimum")}; + ONLY_CONTINUE_IF(minimum && minimum->is_number()); + const auto *exclusive_minimum{schema.try_at("exclusiveMinimum")}; + ONLY_CONTINUE_IF(exclusive_minimum && exclusive_minimum->is_number()); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + if (schema.at("exclusiveMinimum") < schema.at("minimum")) { + schema.erase("exclusiveMinimum"); + } else { + schema.erase("minimum"); + } + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/extends_to_array.h b/vendor/blaze/src/canonicalizer/rules/extends_to_array.h similarity index 61% rename from vendor/blaze/src/alterschema/canonicalizer/extends_to_array.h rename to vendor/blaze/src/canonicalizer/rules/extends_to_array.h index a6b2b951e..ff5a26b40 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/extends_to_array.h +++ b/vendor/blaze/src/canonicalizer/rules/extends_to_array.h @@ -1,8 +1,7 @@ class ExtendsToArray final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - ExtendsToArray() : SchemaTransformRule{"extends_to_array", ""} {}; + ExtendsToArray() : SchemaTransformRule{"extends_to_array"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,8 +10,7 @@ class ExtendsToArray final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1, @@ -25,17 +23,19 @@ class ExtendsToArray final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { auto array{sourcemeta::core::JSON::make_array()}; array.push_back(schema.at("extends")); schema.assign("extends", std::move(array)); } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { - return target.rebase(current.concat("extends"), - current.concat(Pointer{"extends", 0})); + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + return target.rebase( + current.concat("extends"), + current.concat(sourcemeta::core::Pointer{"extends", 0})); } }; diff --git a/vendor/blaze/src/canonicalizer/rules/flatten_nested_allof.h b/vendor/blaze/src/canonicalizer/rules/flatten_nested_allof.h new file mode 100644 index 000000000..74f0c2bbe --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/flatten_nested_allof.h @@ -0,0 +1,121 @@ +class FlattenNestedAllOf final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + FlattenNestedAllOf() : SchemaTransformRule{"flatten_nested_allof"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"allOf"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object() && schema.defines(KEYWORD) && + schema.at(KEYWORD).is_array()); + + this->flatten_indices_.clear(); + const auto &branches{schema.at(KEYWORD)}; + for (std::size_t index = 0; index < branches.size(); ++index) { + const auto &branch{branches.at(index)}; + if (branch.is_object() && branch.size() == 1 && branch.defines(KEYWORD) && + branch.at(KEYWORD).is_array()) { + this->flatten_indices_.push_back(index); + } + } + + ONLY_CONTINUE_IF(!this->flatten_indices_.empty()); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + static const sourcemeta::core::JSON::String KEYWORD{"allOf"}; + this->index_mapping_.clear(); + const auto &original{schema.at(KEYWORD)}; + auto result{sourcemeta::core::JSON::make_array()}; + std::size_t new_index{0}; + std::size_t flatten_cursor{0}; + + for (std::size_t index = 0; index < original.size(); ++index) { + if (flatten_cursor < this->flatten_indices_.size() && + this->flatten_indices_[flatten_cursor] == index) { + this->collect_leaves_(original.at(index), KEYWORD, index, result, + new_index); + ++flatten_cursor; + } else { + this->index_mapping_.emplace_back(index, std::nullopt, new_index); + result.push_back(original.at(index)); + ++new_index; + } + } + + schema.assign(KEYWORD, std::move(result)); + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + static const sourcemeta::core::JSON::String KEYWORD{"allOf"}; + const auto prefix{current.concat(KEYWORD)}; + if (!target.starts_with(prefix)) { + return target; + } + const auto relative{target.resolve_from(prefix)}; + if (relative.empty() || !relative.at(0).is_index()) { + return target; + } + const auto old_index{relative.at(0).to_index()}; + for (const auto &[outer, inner, mapped] : this->index_mapping_) { + if (outer == old_index && inner.has_value()) { + const sourcemeta::core::Pointer old_prefix{ + prefix.concat({old_index, KEYWORD, inner.value()})}; + if (target.starts_with(old_prefix)) { + const sourcemeta::core::Pointer new_prefix{prefix.concat(mapped)}; + return target.rebase(old_prefix, new_prefix); + } + } else if (outer == old_index) { + const sourcemeta::core::Pointer old_prefix{prefix.concat(old_index)}; + const sourcemeta::core::Pointer new_prefix{prefix.concat(mapped)}; + return target.rebase(old_prefix, new_prefix); + } + } + return target; + } + +private: + auto collect_leaves_(const sourcemeta::core::JSON &node, + const sourcemeta::core::JSON::String &keyword, + std::size_t outer_index, sourcemeta::core::JSON &result, + std::size_t &new_index) const -> void { + const auto &inner{node.at(keyword)}; + for (std::size_t inner_index = 0; inner_index < inner.size(); + ++inner_index) { + const auto &child{inner.at(inner_index)}; + if (child.is_object() && child.size() == 1 && child.defines(keyword) && + child.at(keyword).is_array()) { + this->collect_leaves_(child, keyword, outer_index, result, new_index); + } else { + this->index_mapping_.emplace_back(outer_index, inner_index, new_index); + result.push_back(child); + ++new_index; + } + } + } + + mutable std::vector flatten_indices_; + mutable std::vector< + std::tuple, std::size_t>> + index_mapping_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/flatten_nested_anyof.h b/vendor/blaze/src/canonicalizer/rules/flatten_nested_anyof.h new file mode 100644 index 000000000..207ba1054 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/flatten_nested_anyof.h @@ -0,0 +1,121 @@ +class FlattenNestedAnyOf final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + FlattenNestedAnyOf() : SchemaTransformRule{"flatten_nested_anyof"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"anyOf"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object() && schema.defines(KEYWORD) && + schema.at(KEYWORD).is_array()); + + this->flatten_indices_.clear(); + const auto &branches{schema.at(KEYWORD)}; + for (std::size_t index = 0; index < branches.size(); ++index) { + const auto &branch{branches.at(index)}; + if (branch.is_object() && branch.size() == 1 && branch.defines(KEYWORD) && + branch.at(KEYWORD).is_array()) { + this->flatten_indices_.push_back(index); + } + } + + ONLY_CONTINUE_IF(!this->flatten_indices_.empty()); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + static const sourcemeta::core::JSON::String KEYWORD{"anyOf"}; + this->index_mapping_.clear(); + const auto &original{schema.at(KEYWORD)}; + auto result{sourcemeta::core::JSON::make_array()}; + std::size_t new_index{0}; + std::size_t flatten_cursor{0}; + + for (std::size_t index = 0; index < original.size(); ++index) { + if (flatten_cursor < this->flatten_indices_.size() && + this->flatten_indices_[flatten_cursor] == index) { + this->collect_leaves_(original.at(index), KEYWORD, index, result, + new_index); + ++flatten_cursor; + } else { + this->index_mapping_.emplace_back(index, std::nullopt, new_index); + result.push_back(original.at(index)); + ++new_index; + } + } + + schema.assign(KEYWORD, std::move(result)); + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + static const sourcemeta::core::JSON::String KEYWORD{"anyOf"}; + const auto prefix{current.concat(KEYWORD)}; + if (!target.starts_with(prefix)) { + return target; + } + const auto relative{target.resolve_from(prefix)}; + if (relative.empty() || !relative.at(0).is_index()) { + return target; + } + const auto old_index{relative.at(0).to_index()}; + for (const auto &[outer, inner, mapped] : this->index_mapping_) { + if (outer == old_index && inner.has_value()) { + const sourcemeta::core::Pointer old_prefix{ + prefix.concat({old_index, KEYWORD, inner.value()})}; + if (target.starts_with(old_prefix)) { + const sourcemeta::core::Pointer new_prefix{prefix.concat(mapped)}; + return target.rebase(old_prefix, new_prefix); + } + } else if (outer == old_index) { + const sourcemeta::core::Pointer old_prefix{prefix.concat(old_index)}; + const sourcemeta::core::Pointer new_prefix{prefix.concat(mapped)}; + return target.rebase(old_prefix, new_prefix); + } + } + return target; + } + +private: + auto collect_leaves_(const sourcemeta::core::JSON &node, + const sourcemeta::core::JSON::String &keyword, + std::size_t outer_index, sourcemeta::core::JSON &result, + std::size_t &new_index) const -> void { + const auto &inner{node.at(keyword)}; + for (std::size_t inner_index = 0; inner_index < inner.size(); + ++inner_index) { + const auto &child{inner.at(inner_index)}; + if (child.is_object() && child.size() == 1 && child.defines(keyword) && + child.at(keyword).is_array()) { + this->collect_leaves_(child, keyword, outer_index, result, new_index); + } else { + this->index_mapping_.emplace_back(outer_index, inner_index, new_index); + result.push_back(child); + ++new_index; + } + } + } + + mutable std::vector flatten_indices_; + mutable std::vector< + std::tuple, std::size_t>> + index_mapping_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/flatten_nested_extends.h b/vendor/blaze/src/canonicalizer/rules/flatten_nested_extends.h new file mode 100644 index 000000000..f71d314c3 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/flatten_nested_extends.h @@ -0,0 +1,118 @@ +class FlattenNestedExtends final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + FlattenNestedExtends() : SchemaTransformRule{"flatten_nested_extends"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"extends"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object() && schema.defines(KEYWORD) && + schema.at(KEYWORD).is_array()); + + this->flatten_indices_.clear(); + const auto &branches{schema.at(KEYWORD)}; + for (std::size_t index = 0; index < branches.size(); ++index) { + const auto &branch{branches.at(index)}; + if (branch.is_object() && branch.size() == 1 && branch.defines(KEYWORD) && + branch.at(KEYWORD).is_array()) { + this->flatten_indices_.push_back(index); + } + } + + ONLY_CONTINUE_IF(!this->flatten_indices_.empty()); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + static const sourcemeta::core::JSON::String KEYWORD{"extends"}; + this->index_mapping_.clear(); + const auto &original{schema.at(KEYWORD)}; + auto result{sourcemeta::core::JSON::make_array()}; + std::size_t new_index{0}; + std::size_t flatten_cursor{0}; + + for (std::size_t index = 0; index < original.size(); ++index) { + if (flatten_cursor < this->flatten_indices_.size() && + this->flatten_indices_[flatten_cursor] == index) { + this->collect_leaves_(original.at(index), KEYWORD, index, result, + new_index); + ++flatten_cursor; + } else { + this->index_mapping_.emplace_back(index, std::nullopt, new_index); + result.push_back(original.at(index)); + ++new_index; + } + } + + schema.assign(KEYWORD, std::move(result)); + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + static const sourcemeta::core::JSON::String KEYWORD{"extends"}; + const auto prefix{current.concat(KEYWORD)}; + if (!target.starts_with(prefix)) { + return target; + } + const auto relative{target.resolve_from(prefix)}; + if (relative.empty() || !relative.at(0).is_index()) { + return target; + } + const auto old_index{relative.at(0).to_index()}; + for (const auto &[outer, inner, mapped] : this->index_mapping_) { + if (outer == old_index && inner.has_value()) { + const sourcemeta::core::Pointer old_prefix{ + prefix.concat({old_index, KEYWORD, inner.value()})}; + if (target.starts_with(old_prefix)) { + const sourcemeta::core::Pointer new_prefix{prefix.concat(mapped)}; + return target.rebase(old_prefix, new_prefix); + } + } else if (outer == old_index) { + const sourcemeta::core::Pointer old_prefix{prefix.concat(old_index)}; + const sourcemeta::core::Pointer new_prefix{prefix.concat(mapped)}; + return target.rebase(old_prefix, new_prefix); + } + } + return target; + } + +private: + auto collect_leaves_(const sourcemeta::core::JSON &node, + const sourcemeta::core::JSON::String &keyword, + std::size_t outer_index, sourcemeta::core::JSON &result, + std::size_t &new_index) const -> void { + const auto &inner{node.at(keyword)}; + for (std::size_t inner_index = 0; inner_index < inner.size(); + ++inner_index) { + const auto &child{inner.at(inner_index)}; + if (child.is_object() && child.size() == 1 && child.defines(keyword) && + child.at(keyword).is_array()) { + this->collect_leaves_(child, keyword, outer_index, result, new_index); + } else { + this->index_mapping_.emplace_back(outer_index, inner_index, new_index); + result.push_back(child); + ++new_index; + } + } + } + + mutable std::vector flatten_indices_; + mutable std::vector< + std::tuple, std::size_t>> + index_mapping_; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/if_then_else_implicit.h b/vendor/blaze/src/canonicalizer/rules/if_then_else_implicit.h similarity index 76% rename from vendor/blaze/src/alterschema/canonicalizer/if_then_else_implicit.h rename to vendor/blaze/src/canonicalizer/rules/if_then_else_implicit.h index 0a2658adc..8398c42d0 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/if_then_else_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/if_then_else_implicit.h @@ -1,8 +1,7 @@ class IfThenElseImplicit final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - IfThenElseImplicit() : SchemaTransformRule{"if_then_else_implicit", ""} {}; + IfThenElseImplicit() : SchemaTransformRule{"if_then_else_implicit"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,8 +10,7 @@ class IfThenElseImplicit final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_7, @@ -24,12 +22,12 @@ class IfThenElseImplicit final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { if (!schema.defines("then")) { - schema.assign("then", JSON{true}); + schema.assign("then", sourcemeta::core::JSON{true}); } if (!schema.defines("else")) { - schema.assign("else", JSON{true}); + schema.assign("else", sourcemeta::core::JSON{true}); } } }; diff --git a/vendor/blaze/src/canonicalizer/rules/if_without_then_else.h b/vendor/blaze/src/canonicalizer/rules/if_without_then_else.h new file mode 100644 index 000000000..7c273fc1f --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/if_without_then_else.h @@ -0,0 +1,33 @@ +class IfWithoutThenElse final : public SchemaTransformRule { +private: + // NOLINTNEXTLINE(bugprone-throwing-static-initialization) + static inline const std::string KEYWORD{"if"}; + +public: + using reframe_after_transform = std::true_type; + IfWithoutThenElse() : SchemaTransformRule{"if_without_then_else"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7}) && + schema.is_object() && schema.defines(KEYWORD) && + !schema.defines("then") && !schema.defines("else")); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase(KEYWORD); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/ignored_metaschema.h b/vendor/blaze/src/canonicalizer/rules/ignored_metaschema.h new file mode 100644 index 000000000..6b84c8fa2 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/ignored_metaschema.h @@ -0,0 +1,26 @@ +class IgnoredMetaschema final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + IgnoredMetaschema() : SchemaTransformRule{"ignored_metaschema"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(schema.is_object()); + const auto *schema_keyword{schema.try_at("$schema")}; + ONLY_CONTINUE_IF(schema_keyword && schema_keyword->is_string()); + const auto dialect{sourcemeta::blaze::dialect(schema)}; + ONLY_CONTINUE_IF(!dialect.empty()); + ONLY_CONTINUE_IF(dialect != location.dialect); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase("$schema"); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/implicit_contains_keywords.h b/vendor/blaze/src/canonicalizer/rules/implicit_contains_keywords.h similarity index 84% rename from vendor/blaze/src/alterschema/canonicalizer/implicit_contains_keywords.h rename to vendor/blaze/src/canonicalizer/rules/implicit_contains_keywords.h index 5c633cedc..5078c4a5b 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/implicit_contains_keywords.h +++ b/vendor/blaze/src/canonicalizer/rules/implicit_contains_keywords.h @@ -1,9 +1,8 @@ class ImplicitContainsKeywords final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; ImplicitContainsKeywords() - : SchemaTransformRule{"implicit_contains_keywords", ""} {}; + : SchemaTransformRule{"implicit_contains_keywords"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,8 @@ class ImplicitContainsKeywords final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &resolver, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &resolver) const + -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2019_09_Applicator, @@ -31,7 +30,7 @@ class ImplicitContainsKeywords final : public SchemaTransformRule { ONLY_CONTINUE_IF( !WALK_UP_IN_PLACE_APPLICATORS( root, frame, location, walker, resolver, - [](const JSON &ancestor, + [](const sourcemeta::core::JSON &ancestor, const Vocabularies &ancestor_vocabularies) -> bool { return ancestor.defines("unevaluatedItems") && ancestor_vocabularies.contains( @@ -44,7 +43,7 @@ class ImplicitContainsKeywords final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { if (!schema.defines("contains")) { schema.assign("contains", sourcemeta::core::JSON{true}); schema.assign("minContains", sourcemeta::core::JSON{0}); diff --git a/vendor/blaze/src/alterschema/canonicalizer/implicit_object_keywords.h b/vendor/blaze/src/canonicalizer/rules/implicit_object_keywords.h similarity index 96% rename from vendor/blaze/src/alterschema/canonicalizer/implicit_object_keywords.h rename to vendor/blaze/src/canonicalizer/rules/implicit_object_keywords.h index b0bf42d83..d34f0aba2 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/implicit_object_keywords.h +++ b/vendor/blaze/src/canonicalizer/rules/implicit_object_keywords.h @@ -1,9 +1,7 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - ImplicitObjectKeywords() - : SchemaTransformRule{"implicit_object_keywords", ""} {}; + ImplicitObjectKeywords() : SchemaTransformRule{"implicit_object_keywords"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +10,7 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(schema.is_object()); const auto *type{schema.try_at("type")}; @@ -32,7 +29,7 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { // Object keywords if (this->add_pattern_properties_) { schema.assign("patternProperties", sourcemeta::core::JSON::make_object()); diff --git a/vendor/blaze/src/alterschema/canonicalizer/inline_single_use_ref.h b/vendor/blaze/src/canonicalizer/rules/inline_single_use_ref.h similarity index 87% rename from vendor/blaze/src/alterschema/canonicalizer/inline_single_use_ref.h rename to vendor/blaze/src/canonicalizer/rules/inline_single_use_ref.h index 2ea6bf44b..b4de771ff 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/inline_single_use_ref.h +++ b/vendor/blaze/src/canonicalizer/rules/inline_single_use_ref.h @@ -1,8 +1,7 @@ class InlineSingleUseRef final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - InlineSingleUseRef() : SchemaTransformRule{"inline_single_use_ref", ""} {}; + InlineSingleUseRef() : SchemaTransformRule{"inline_single_use_ref"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,8 +10,7 @@ class InlineSingleUseRef final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(schema.is_object() && schema.size() == 1); const auto *ref{schema.try_at("$ref")}; @@ -94,14 +92,15 @@ class InlineSingleUseRef final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.into(std::move(this->target_copy_)); } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { if (target.starts_with(this->target_pointer_)) { const auto relative{target.resolve_from(this->target_pointer_)}; return current.concat(relative); @@ -110,6 +109,6 @@ class InlineSingleUseRef final : public SchemaTransformRule { } private: - mutable Pointer target_pointer_; + mutable sourcemeta::core::Pointer target_pointer_; mutable sourcemeta::core::JSON target_copy_{sourcemeta::core::JSON{nullptr}}; }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/items_implicit.h b/vendor/blaze/src/canonicalizer/rules/items_implicit.h similarity index 79% rename from vendor/blaze/src/alterschema/canonicalizer/items_implicit.h rename to vendor/blaze/src/canonicalizer/rules/items_implicit.h index d4e88e279..c574c40d7 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/items_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/items_implicit.h @@ -1,11 +1,7 @@ class ItemsImplicit final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - ItemsImplicit() - : SchemaTransformRule{"items_implicit", - "Every array has an implicit `items` " - "that consists of the boolean schema `true`"} {}; + ItemsImplicit() : SchemaTransformRule{"items_implicit"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -14,8 +10,8 @@ class ItemsImplicit final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &resolver, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &resolver) const + -> bool override { ONLY_CONTINUE_IF( ((vocabularies.contains( Vocabularies::Known::JSON_Schema_2020_12_Validation) && @@ -40,7 +36,7 @@ class ItemsImplicit final : public SchemaTransformRule { ONLY_CONTINUE_IF( !WALK_UP_IN_PLACE_APPLICATORS( root, frame, location, walker, resolver, - [](const JSON &ancestor, + [](const sourcemeta::core::JSON &ancestor, const Vocabularies &ancestor_vocabularies) -> bool { return ancestor.defines("unevaluatedItems") && ancestor_vocabularies.contains_any( @@ -52,7 +48,7 @@ class ItemsImplicit final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { - schema.assign("items", JSON{true}); + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.assign("items", sourcemeta::core::JSON{true}); } }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/max_contains_covered_by_max_items.h b/vendor/blaze/src/canonicalizer/rules/max_contains_covered_by_max_items.h similarity index 67% rename from vendor/blaze/src/alterschema/canonicalizer/max_contains_covered_by_max_items.h rename to vendor/blaze/src/canonicalizer/rules/max_contains_covered_by_max_items.h index 30a964036..20c773c2d 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/max_contains_covered_by_max_items.h +++ b/vendor/blaze/src/canonicalizer/rules/max_contains_covered_by_max_items.h @@ -1,13 +1,8 @@ class MaxContainsCoveredByMaxItems final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::false_type; MaxContainsCoveredByMaxItems() - : SchemaTransformRule{ - "max_contains_covered_by_max_items", - "Setting the `maxContains` keyword to a number greater than or " - "equal to the array upper bound does not add any further " - "constraint"} {}; + : SchemaTransformRule{"max_contains_covered_by_max_items"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -16,8 +11,7 @@ class MaxContainsCoveredByMaxItems final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, @@ -29,10 +23,10 @@ class MaxContainsCoveredByMaxItems final : public SchemaTransformRule { const auto *max_items{schema.try_at("maxItems")}; ONLY_CONTINUE_IF(max_items && max_items->is_integer() && max_contains->to_integer() > max_items->to_integer()); - return APPLIES_TO_KEYWORDS("maxContains", "maxItems"); + return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.assign("maxContains", schema.at("maxItems")); } }; diff --git a/vendor/blaze/src/canonicalizer/rules/max_contains_without_contains.h b/vendor/blaze/src/canonicalizer/rules/max_contains_without_contains.h new file mode 100644 index 000000000..8516b11ee --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/max_contains_without_contains.h @@ -0,0 +1,27 @@ +class MaxContainsWithoutContains final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + MaxContainsWithoutContains() + : SchemaTransformRule{"max_contains_without_contains"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation}) && + schema.is_object() && schema.defines("maxContains") && + !schema.defines("contains")); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase("maxContains"); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/max_decimal_implicit.h b/vendor/blaze/src/canonicalizer/rules/max_decimal_implicit.h similarity index 80% rename from vendor/blaze/src/alterschema/canonicalizer/max_decimal_implicit.h rename to vendor/blaze/src/canonicalizer/rules/max_decimal_implicit.h index d90985420..d56c1d2fd 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/max_decimal_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/max_decimal_implicit.h @@ -1,8 +1,7 @@ class MaxDecimalImplicit final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - MaxDecimalImplicit() : SchemaTransformRule{"max_decimal_implicit", ""} {}; + MaxDecimalImplicit() : SchemaTransformRule{"max_decimal_implicit"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,8 +10,7 @@ class MaxDecimalImplicit final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1}) && @@ -24,7 +22,7 @@ class MaxDecimalImplicit final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.assign("maxDecimal", sourcemeta::core::JSON{0}); } }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/maximum_can_equal_integer_fold.h b/vendor/blaze/src/canonicalizer/rules/maximum_can_equal_integer_fold.h similarity index 93% rename from vendor/blaze/src/alterschema/canonicalizer/maximum_can_equal_integer_fold.h rename to vendor/blaze/src/canonicalizer/rules/maximum_can_equal_integer_fold.h index c98f20f15..936e620db 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/maximum_can_equal_integer_fold.h +++ b/vendor/blaze/src/canonicalizer/rules/maximum_can_equal_integer_fold.h @@ -1,9 +1,8 @@ class MaximumCanEqualIntegerFold final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; MaximumCanEqualIntegerFold() - : SchemaTransformRule{"maximum_can_equal_integer_fold", ""} {}; + : SchemaTransformRule{"maximum_can_equal_integer_fold"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class MaximumCanEqualIntegerFold final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1, @@ -31,7 +29,7 @@ class MaximumCanEqualIntegerFold final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { const auto &maximum{schema.at("maximum")}; if (maximum.is_integer()) { if (maximum.to_integer() > std::numeric_limits::min()) { diff --git a/vendor/blaze/src/alterschema/canonicalizer/maximum_can_equal_true_drop.h b/vendor/blaze/src/canonicalizer/rules/maximum_can_equal_true_drop.h similarity index 80% rename from vendor/blaze/src/alterschema/canonicalizer/maximum_can_equal_true_drop.h rename to vendor/blaze/src/canonicalizer/rules/maximum_can_equal_true_drop.h index ee2faaa90..932c6a99e 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/maximum_can_equal_true_drop.h +++ b/vendor/blaze/src/canonicalizer/rules/maximum_can_equal_true_drop.h @@ -1,9 +1,8 @@ class MaximumCanEqualTrueDrop final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; MaximumCanEqualTrueDrop() - : SchemaTransformRule{"maximum_can_equal_true_drop", ""} {}; + : SchemaTransformRule{"maximum_can_equal_true_drop"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class MaximumCanEqualTrueDrop final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1, @@ -30,7 +28,7 @@ class MaximumCanEqualTrueDrop final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.erase("maximumCanEqual"); } }; diff --git a/vendor/blaze/src/canonicalizer/rules/maximum_real_for_integer.h b/vendor/blaze/src/canonicalizer/rules/maximum_real_for_integer.h new file mode 100644 index 000000000..cbc3fc76d --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/maximum_real_for_integer.h @@ -0,0 +1,54 @@ +class MaximumRealForInteger final : public SchemaTransformRule { +public: + using reframe_after_transform = std::false_type; + MaximumRealForInteger() : SchemaTransformRule{"maximum_real_for_integer"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1}) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); + const auto *maximum{schema.try_at("maximum")}; + ONLY_CONTINUE_IF(maximum && maximum->is_number() && + !maximum->is_integral()); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + if (schema.at("maximum").is_decimal()) { + auto current{schema.at("maximum").to_decimal()}; + auto new_value{current.to_integral()}; + if (new_value > current) { + new_value -= sourcemeta::core::Decimal{1}; + } + + if (new_value.is_int64()) { + schema.assign("maximum", sourcemeta::core::JSON{new_value.to_int64()}); + } else { + schema.assign("maximum", sourcemeta::core::JSON{std::move(new_value)}); + } + } else { + const auto current{schema.at("maximum").to_real()}; + const auto new_value{static_cast(std::floor(current))}; + schema.assign("maximum", sourcemeta::core::JSON{new_value}); + } + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/min_contains_without_contains.h b/vendor/blaze/src/canonicalizer/rules/min_contains_without_contains.h new file mode 100644 index 000000000..6594f2b1d --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/min_contains_without_contains.h @@ -0,0 +1,27 @@ +class MinContainsWithoutContains final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + MinContainsWithoutContains() + : SchemaTransformRule{"min_contains_without_contains"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF( + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation}) && + schema.is_object() && schema.defines("minContains") && + !schema.defines("contains")); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase("minContains"); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/min_items_given_min_contains.h b/vendor/blaze/src/canonicalizer/rules/min_items_given_min_contains.h similarity index 74% rename from vendor/blaze/src/alterschema/canonicalizer/min_items_given_min_contains.h rename to vendor/blaze/src/canonicalizer/rules/min_items_given_min_contains.h index 26bcb141b..3dc24be82 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/min_items_given_min_contains.h +++ b/vendor/blaze/src/canonicalizer/rules/min_items_given_min_contains.h @@ -1,12 +1,8 @@ class MinItemsGivenMinContains final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; MinItemsGivenMinContains() - : SchemaTransformRule{ - "min_items_given_min_contains", - "Every array has a minimum size of zero items but may be affected " - "by `minContains`"} {}; + : SchemaTransformRule{"min_items_given_min_contains"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -15,8 +11,7 @@ class MinItemsGivenMinContains final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, @@ -28,7 +23,7 @@ class MinItemsGivenMinContains final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { if (schema.defines("contains") && schema.defines("minContains") && schema.at("minContains").is_integer()) { schema.assign("minItems", sourcemeta::core::JSON{ diff --git a/vendor/blaze/src/alterschema/canonicalizer/min_length_implicit.h b/vendor/blaze/src/canonicalizer/rules/min_length_implicit.h similarity index 79% rename from vendor/blaze/src/alterschema/canonicalizer/min_length_implicit.h rename to vendor/blaze/src/canonicalizer/rules/min_length_implicit.h index 931671832..b48921edb 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/min_length_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/min_length_implicit.h @@ -1,11 +1,7 @@ class MinLengthImplicit final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - MinLengthImplicit() - : SchemaTransformRule{ - "min_length_implicit", - "Every string has a minimum length of zero characters"} {}; + MinLengthImplicit() : SchemaTransformRule{"min_length_implicit"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -14,8 +10,7 @@ class MinLengthImplicit final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation, @@ -34,7 +29,7 @@ class MinLengthImplicit final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.assign("minLength", sourcemeta::core::JSON{0}); } }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/min_properties_covered_by_required.h b/vendor/blaze/src/canonicalizer/rules/min_properties_covered_by_required.h similarity index 75% rename from vendor/blaze/src/alterschema/canonicalizer/min_properties_covered_by_required.h rename to vendor/blaze/src/canonicalizer/rules/min_properties_covered_by_required.h index e74625955..a80bca96d 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/min_properties_covered_by_required.h +++ b/vendor/blaze/src/canonicalizer/rules/min_properties_covered_by_required.h @@ -1,12 +1,8 @@ class MinPropertiesCoveredByRequired final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::false_type; MinPropertiesCoveredByRequired() - : SchemaTransformRule{ - "min_properties_covered_by_required", - "Setting `minProperties` to a number less than `required` does " - "not add any further constraint"} {}; + : SchemaTransformRule{"min_properties_covered_by_required"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -15,8 +11,7 @@ class MinPropertiesCoveredByRequired final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation, @@ -32,10 +27,10 @@ class MinPropertiesCoveredByRequired final : public SchemaTransformRule { required && required->is_array() && required->unique() && std::cmp_greater(required->size(), static_cast( min_properties->to_integer()))); - return APPLIES_TO_KEYWORDS("minProperties", "required"); + return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.assign("minProperties", sourcemeta::core::JSON{schema.at("required").size()}); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/minimum_can_equal_integer_fold.h b/vendor/blaze/src/canonicalizer/rules/minimum_can_equal_integer_fold.h similarity index 93% rename from vendor/blaze/src/alterschema/canonicalizer/minimum_can_equal_integer_fold.h rename to vendor/blaze/src/canonicalizer/rules/minimum_can_equal_integer_fold.h index 48460e63e..de89dd461 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/minimum_can_equal_integer_fold.h +++ b/vendor/blaze/src/canonicalizer/rules/minimum_can_equal_integer_fold.h @@ -1,9 +1,8 @@ class MinimumCanEqualIntegerFold final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; MinimumCanEqualIntegerFold() - : SchemaTransformRule{"minimum_can_equal_integer_fold", ""} {}; + : SchemaTransformRule{"minimum_can_equal_integer_fold"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class MinimumCanEqualIntegerFold final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1, @@ -31,7 +29,7 @@ class MinimumCanEqualIntegerFold final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { const auto &minimum{schema.at("minimum")}; if (minimum.is_integer()) { if (minimum.to_integer() < std::numeric_limits::max()) { diff --git a/vendor/blaze/src/alterschema/canonicalizer/minimum_can_equal_true_drop.h b/vendor/blaze/src/canonicalizer/rules/minimum_can_equal_true_drop.h similarity index 80% rename from vendor/blaze/src/alterschema/canonicalizer/minimum_can_equal_true_drop.h rename to vendor/blaze/src/canonicalizer/rules/minimum_can_equal_true_drop.h index 4085eb0db..c1e98e736 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/minimum_can_equal_true_drop.h +++ b/vendor/blaze/src/canonicalizer/rules/minimum_can_equal_true_drop.h @@ -1,9 +1,8 @@ class MinimumCanEqualTrueDrop final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; MinimumCanEqualTrueDrop() - : SchemaTransformRule{"minimum_can_equal_true_drop", ""} {}; + : SchemaTransformRule{"minimum_can_equal_true_drop"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class MinimumCanEqualTrueDrop final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1, @@ -30,7 +28,7 @@ class MinimumCanEqualTrueDrop final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.erase("minimumCanEqual"); } }; diff --git a/vendor/blaze/src/canonicalizer/rules/minimum_real_for_integer.h b/vendor/blaze/src/canonicalizer/rules/minimum_real_for_integer.h new file mode 100644 index 000000000..566d0b9b2 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/minimum_real_for_integer.h @@ -0,0 +1,54 @@ +class MinimumRealForInteger final : public SchemaTransformRule { +public: + using reframe_after_transform = std::false_type; + MinimumRealForInteger() : SchemaTransformRule{"minimum_real_for_integer"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1}) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); + const auto *minimum{schema.try_at("minimum")}; + ONLY_CONTINUE_IF(minimum && minimum->is_number() && + !minimum->is_integral()); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + if (schema.at("minimum").is_decimal()) { + const auto current{schema.at("minimum").to_decimal()}; + auto new_value{current.to_integral()}; + if (new_value < current) { + new_value += sourcemeta::core::Decimal{1}; + } + + if (new_value.is_int64()) { + schema.assign("minimum", sourcemeta::core::JSON{new_value.to_int64()}); + } else { + schema.assign("minimum", sourcemeta::core::JSON{new_value}); + } + } else { + const auto current{schema.at("minimum").to_real()}; + const auto new_value{static_cast(std::ceil(current))}; + schema.assign("minimum", sourcemeta::core::JSON{new_value}); + } + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/modern_official_dialect_with_empty_fragment.h b/vendor/blaze/src/canonicalizer/rules/modern_official_dialect_with_empty_fragment.h new file mode 100644 index 000000000..3226034a0 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/modern_official_dialect_with_empty_fragment.h @@ -0,0 +1,33 @@ +class ModernOfficialDialectWithEmptyFragment final + : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + ModernOfficialDialectWithEmptyFragment() + : SchemaTransformRule{"modern_official_dialect_with_empty_fragment"} {}; + + [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const + -> bool override { + ONLY_CONTINUE_IF(schema.is_object()); + const auto *schema_keyword{schema.try_at("$schema")}; + ONLY_CONTINUE_IF(schema_keyword && schema_keyword->is_string()); + const auto &dialect{schema_keyword->to_string()}; + ONLY_CONTINUE_IF( + dialect == "https://json-schema.org/draft/2019-09/schema#" || + dialect == "https://json-schema.org/draft/2019-09/hyper-schema#" || + dialect == "https://json-schema.org/draft/2020-12/schema#" || + dialect == "https://json-schema.org/draft/2020-12/hyper-schema#"); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto dialect{std::move(schema.at("$schema")).to_string()}; + dialect.pop_back(); + schema.at("$schema").into(sourcemeta::core::JSON{dialect}); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/modern_official_dialect_with_http.h b/vendor/blaze/src/canonicalizer/rules/modern_official_dialect_with_http.h new file mode 100644 index 000000000..4176a4baf --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/modern_official_dialect_with_http.h @@ -0,0 +1,44 @@ +class ModernOfficialDialectWithHttp final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + ModernOfficialDialectWithHttp() + : SchemaTransformRule{"modern_official_dialect_with_http"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + using sourcemeta::blaze::SchemaBaseDialect; + ONLY_CONTINUE_IF( + location.base_dialect == SchemaBaseDialect::JSON_Schema_2020_12 || + location.base_dialect == SchemaBaseDialect::JSON_Schema_2020_12_Hyper || + location.base_dialect == SchemaBaseDialect::JSON_Schema_2019_09 || + location.base_dialect == SchemaBaseDialect::JSON_Schema_2019_09_Hyper); + ONLY_CONTINUE_IF(schema.is_object()); + const auto *schema_keyword{schema.try_at("$schema")}; + ONLY_CONTINUE_IF(schema_keyword && schema_keyword->is_string()); + const auto &dialect{schema_keyword->to_string()}; + ONLY_CONTINUE_IF(dialect.starts_with("http://json-schema.org/")); + ONLY_CONTINUE_IF( + dialect == "http://json-schema.org/draft/2020-12/schema" || + dialect == "http://json-schema.org/draft/2020-12/schema#" || + dialect == "http://json-schema.org/draft/2020-12/hyper-schema" || + dialect == "http://json-schema.org/draft/2020-12/hyper-schema#" || + dialect == "http://json-schema.org/draft/2019-09/schema" || + dialect == "http://json-schema.org/draft/2019-09/schema#" || + dialect == "http://json-schema.org/draft/2019-09/hyper-schema" || + dialect == "http://json-schema.org/draft/2019-09/hyper-schema#"); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + const auto &old_dialect{schema.at("$schema").to_string()}; + std::string new_dialect{"https://"}; + new_dialect += old_dialect.substr(7); + schema.at("$schema").into(sourcemeta::core::JSON{new_dialect}); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/multiple_of_implicit.h b/vendor/blaze/src/canonicalizer/rules/multiple_of_implicit.h similarity index 76% rename from vendor/blaze/src/alterschema/canonicalizer/multiple_of_implicit.h rename to vendor/blaze/src/canonicalizer/rules/multiple_of_implicit.h index aa59e3401..72f04c0ff 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/multiple_of_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/multiple_of_implicit.h @@ -1,10 +1,7 @@ class MultipleOfImplicit final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - MultipleOfImplicit() - : SchemaTransformRule{"multiple_of_implicit", - "The unit of `multipleOf` is the integer 1"} {}; + MultipleOfImplicit() : SchemaTransformRule{"multiple_of_implicit"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -13,8 +10,7 @@ class MultipleOfImplicit final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation, @@ -30,7 +26,7 @@ class MultipleOfImplicit final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.assign("multipleOf", sourcemeta::core::JSON{1}); } }; diff --git a/vendor/blaze/src/canonicalizer/rules/non_applicable_additional_items.h b/vendor/blaze/src/canonicalizer/rules/non_applicable_additional_items.h new file mode 100644 index 000000000..eb7c983cb --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/non_applicable_additional_items.h @@ -0,0 +1,38 @@ +class NonApplicableAdditionalItems final : public SchemaTransformRule { +private: + // NOLINTNEXTLINE(bugprone-throwing-static-initialization) + static inline const std::string KEYWORD{"additionalItems"}; + +public: + using reframe_after_transform = std::true_type; + NonApplicableAdditionalItems() + : SchemaTransformRule{"non_applicable_additional_items"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object() && schema.defines(KEYWORD)); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + + const auto *items{schema.try_at("items")}; + return (items && is_schema(*items)) || !items; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase(KEYWORD); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/non_applicable_disallow_types.h b/vendor/blaze/src/canonicalizer/rules/non_applicable_disallow_types.h new file mode 100644 index 000000000..2554c065f --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/non_applicable_disallow_types.h @@ -0,0 +1,97 @@ +class NonApplicableDisallowTypes final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + NonApplicableDisallowTypes() + : SchemaTransformRule{"non_applicable_disallow_types"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"disallow"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && !disallow->empty()); + + const auto *parent_type_value{schema.try_at("type")}; + ONLY_CONTINUE_IF(parent_type_value && + is_known_type_form(*parent_type_value)); + + const auto parent_types{parse_schema_type(*parent_type_value)}; + ONLY_CONTINUE_IF(parent_types.any()); + + std::vector locations; + for (std::size_t index = 0; index < disallow->size(); ++index) { + const auto &entry{disallow->at(index)}; + sourcemeta::core::JSON::TypeSet entry_types; + if (entry.is_string() && entry.to_string() != "any") { + entry_types = parse_schema_type(entry); + } else if (entry.is_object()) { + const auto *entry_type{entry.try_at("type")}; + if (entry_type && is_known_type_form(*entry_type)) { + entry_types = parse_schema_type(*entry_type); + } + } + + if (entry_types.any() && (parent_types & entry_types).none()) { + locations.push_back(sourcemeta::core::Pointer{KEYWORD, index}); + } + } + + ONLY_CONTINUE_IF(!locations.empty()); + + auto keyword_pointer{location.pointer}; + keyword_pointer.push_back(std::cref(KEYWORD)); + ONLY_CONTINUE_IF(!frame.has_references_through(keyword_pointer)); + + this->locations_ = std::move(locations); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + std::vector dead_indices; + dead_indices.reserve(this->locations_.size()); + for (const auto &location : this->locations_) { + assert(location.size() == 2); + dead_indices.push_back(location.at(1).to_index()); + } + + auto new_disallow{sourcemeta::core::JSON::make_array()}; + const auto &disallow{schema.at("disallow")}; + for (std::size_t index = 0; index < disallow.size(); ++index) { + if (std::ranges::find(dead_indices, index) == dead_indices.end()) { + new_disallow.push_back(disallow.at(index)); + } + } + + if (new_disallow.empty()) { + schema.erase("disallow"); + } else { + schema.assign("disallow", std::move(new_disallow)); + } + } + +private: + static auto is_known_type_form(const sourcemeta::core::JSON &type) -> bool { + if (type.is_string()) { + return type.to_string() != "any"; + } + if (!type.is_array()) { + return false; + } + return std::ranges::all_of(type.as_array(), [](const auto &entry) -> auto { + return entry.is_string() && entry.to_string() != "any"; + }); + } + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/non_applicable_enum_validation_keywords.h b/vendor/blaze/src/canonicalizer/rules/non_applicable_enum_validation_keywords.h new file mode 100644 index 000000000..0b9ef9c61 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/non_applicable_enum_validation_keywords.h @@ -0,0 +1,76 @@ +class NonApplicableEnumValidationKeywords final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + NonApplicableEnumValidationKeywords() + : SchemaTransformRule{"non_applicable_enum_validation_keywords"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_2_Hyper, + Vocabularies::Known::JSON_Schema_Draft_1, + Vocabularies::Known::JSON_Schema_Draft_1_Hyper}) && + schema.is_object() && !schema.defines("type")); + + const auto *enum_value{schema.try_at("enum")}; + ONLY_CONTINUE_IF(enum_value && enum_value->is_array()); + + sourcemeta::core::JSON::TypeSet enum_types; + for (const auto &value : enum_value->as_array()) { + enum_types.set(std::to_underlying(value.type())); + } + + ONLY_CONTINUE_IF(enum_types.any()); + + const bool is_draft3{vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})}; + + std::vector positions; + for (const auto &entry : schema.as_object()) { + const auto &metadata = walker(entry.first, vocabularies); + + // If instances is empty (none set), the keyword applies to all types + if (metadata.instances.none()) { + continue; + } + + if (is_draft3 && entry.first == "required" && entry.second.is_boolean()) { + continue; + } + + // Check if there's any overlap between keyword's applicable types and + // enum types + if ((metadata.instances & enum_types).none()) { + positions.push_back(sourcemeta::core::Pointer{entry.first}); + } + } + + ONLY_CONTINUE_IF(!positions.empty()); + + this->locations_ = std::move(positions); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + for (const auto &location : this->locations_) { + schema.erase(location.at(0).to_property()); + } + } + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/non_applicable_type_specific_keywords.h b/vendor/blaze/src/canonicalizer/rules/non_applicable_type_specific_keywords.h new file mode 100644 index 000000000..182ec587f --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/non_applicable_type_specific_keywords.h @@ -0,0 +1,130 @@ +class NonApplicableTypeSpecificKeywords final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + NonApplicableTypeSpecificKeywords() + : SchemaTransformRule{"non_applicable_type_specific_keywords"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(schema.is_object()); + + const auto *type_value{schema.try_at("type")}; + auto current_types{vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_2_Hyper, + Vocabularies::Known::JSON_Schema_Draft_1, + Vocabularies::Known::JSON_Schema_Draft_1_Hyper, + Vocabularies::Known::JSON_Schema_Draft_0, + Vocabularies::Known::JSON_Schema_Draft_0_Hyper}) && + type_value + ? parse_schema_type(*type_value) + : sourcemeta::core::JSON::TypeSet{}}; + + if (vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1})) { + const auto *enum_value{schema.try_at("enum")}; + if (enum_value && enum_value->is_array()) { + for (const auto &entry : enum_value->as_array()) { + current_types.set(std::to_underlying(entry.type())); + } + } + } + + if (vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6})) { + const auto *const_value{schema.try_at("const")}; + if (const_value) { + current_types.set(std::to_underlying(const_value->type())); + } + } + + // This means that the schema has no explicit type constraints, + // so we cannot remove anything from it. + ONLY_CONTINUE_IF(current_types.any()); + + std::vector positions; + for (const auto &entry : schema.as_object()) { + const auto &metadata{walker(entry.first, vocabularies)}; + + // The keyword applies to any type, so it cannot be removed + if (metadata.instances.none()) { + continue; + } + + if (entry.first == "required" && + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})) { + continue; + } + + if (entry.first == "maxDecimal" && + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_0, + Vocabularies::Known::JSON_Schema_Draft_0_Hyper, + Vocabularies::Known::JSON_Schema_Draft_1, + Vocabularies::Known::JSON_Schema_Draft_1_Hyper})) { + continue; + } + + if (entry.first == "optional" && + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_0, + Vocabularies::Known::JSON_Schema_Draft_0_Hyper, + Vocabularies::Known::JSON_Schema_Draft_1, + Vocabularies::Known::JSON_Schema_Draft_1_Hyper, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_2_Hyper})) { + continue; + } + + // If none of the types that the keyword applies to is a valid + // type for the current schema, then by definition we can remove it + if ((metadata.instances & current_types).none()) { + // Skip keywords that have references pointing to them + if (frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(entry.first)})) { + continue; + } + + positions.push_back(sourcemeta::core::Pointer{entry.first}); + } + } + + ONLY_CONTINUE_IF(!positions.empty()); + this->locations_ = std::move(positions); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + for (const auto &location : this->locations_) { + schema.erase(location.at(0).to_property()); + } + } + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/not_false.h b/vendor/blaze/src/canonicalizer/rules/not_false.h new file mode 100644 index 000000000..4d085f087 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/not_false.h @@ -0,0 +1,34 @@ +class NotFalse final : public SchemaTransformRule { +private: + // NOLINTNEXTLINE(bugprone-throwing-static-initialization) + static inline const std::string KEYWORD{"not"}; + +public: + using reframe_after_transform = std::true_type; + NotFalse() : SchemaTransformRule{"not_false"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, const Vocabularies &vocabularies, + const SchemaFrame &frame, const SchemaFrame::Location &location, + const SchemaWalker &, const SchemaResolver &) const + -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object() && schema.defines(KEYWORD) && + schema.at(KEYWORD).is_boolean() && + !schema.at(KEYWORD).to_boolean()); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase(KEYWORD); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/oneof_false_simplify.h b/vendor/blaze/src/canonicalizer/rules/oneof_false_simplify.h new file mode 100644 index 000000000..48b587246 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/oneof_false_simplify.h @@ -0,0 +1,37 @@ +class OneOfFalseSimplify final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + OneOfFalseSimplify() : SchemaTransformRule{"oneof_false_simplify"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"oneOf"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6}) && + schema.is_object() && !schema.defines("not")); + + const auto *one_of{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(one_of && one_of->is_array() && one_of->size() == 1); + + const auto &entry{one_of->front()}; + ONLY_CONTINUE_IF(entry.is_boolean() && !entry.to_boolean()); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.at("oneOf").into(sourcemeta::core::JSON{true}); + schema.rename("oneOf", "not"); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/oneof_to_anyof_disjoint_types.h b/vendor/blaze/src/canonicalizer/rules/oneof_to_anyof_disjoint_types.h new file mode 100644 index 000000000..abcd97439 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/oneof_to_anyof_disjoint_types.h @@ -0,0 +1,98 @@ +class OneOfToAnyOfDisjointTypes final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + OneOfToAnyOfDisjointTypes() + : SchemaTransformRule{"oneof_to_anyof_disjoint_types"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"oneOf"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object()); + + const auto *oneof_value{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(oneof_value && oneof_value->is_array() && + oneof_value->size() > 1); + + const auto has_validation_vocabulary{vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1})}; + + const auto has_const_vocabulary{vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6})}; + + std::vector type_sets; + type_sets.reserve(oneof_value->size()); + + for (const auto &branch : oneof_value->as_array()) { + ONLY_CONTINUE_IF(branch.is_object()); + + const auto *type_value{branch.try_at("type")}; + const auto *const_value{has_const_vocabulary ? branch.try_at("const") + : nullptr}; + const auto *enum_value{has_validation_vocabulary ? branch.try_at("enum") + : nullptr}; + const auto has_enum{enum_value && enum_value->is_array()}; + + if (type_value) { + const auto branch_types{parse_schema_type(*type_value)}; + ONLY_CONTINUE_IF(branch_types.any()); + type_sets.push_back(branch_types); + } else if (const_value && !has_enum) { + sourcemeta::core::JSON::TypeSet branch_types; + branch_types.set(std::to_underlying(const_value->type())); + type_sets.push_back(branch_types); + } else if (has_enum && !const_value) { + sourcemeta::core::JSON::TypeSet branch_types; + for (const auto &item : enum_value->as_array()) { + branch_types.set(std::to_underlying(item.type())); + } + type_sets.push_back(branch_types); + } else { + return false; + } + } + + for (std::size_t index = 0; index < type_sets.size(); ++index) { + for (std::size_t other = index + 1; other < type_sets.size(); ++other) { + ONLY_CONTINUE_IF((type_sets[index] & type_sets[other]).none()); + } + } + + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.rename("oneOf", "anyOf"); + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &origin + [[maybe_unused]], + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + const sourcemeta::core::Pointer oneof_prefix{current.concat("oneOf")}; + const sourcemeta::core::Pointer anyof_prefix{current.concat("anyOf")}; + return target.rebase(oneof_prefix, anyof_prefix); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/optional_property_implicit.h b/vendor/blaze/src/canonicalizer/rules/optional_property_implicit.h similarity index 83% rename from vendor/blaze/src/alterschema/canonicalizer/optional_property_implicit.h rename to vendor/blaze/src/canonicalizer/rules/optional_property_implicit.h index bf9efb626..2d5a5a679 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/optional_property_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/optional_property_implicit.h @@ -1,9 +1,8 @@ class OptionalPropertyImplicit final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; OptionalPropertyImplicit() - : SchemaTransformRule{"optional_property_implicit", ""} {}; + : SchemaTransformRule{"optional_property_implicit"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class OptionalPropertyImplicit final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1, @@ -36,8 +34,8 @@ class OptionalPropertyImplicit final : public SchemaTransformRule { return false; } - auto transform(JSON &schema, const Result &) const -> void override { - std::vector keys; + auto transform(sourcemeta::core::JSON &schema) const -> void override { + std::vector keys; for (const auto &entry : schema.at("properties").as_object()) { if (entry.second.is_object() && !entry.second.empty() && !entry.second.defines("optional")) { diff --git a/vendor/blaze/src/canonicalizer/rules/orphan_definitions.h b/vendor/blaze/src/canonicalizer/rules/orphan_definitions.h new file mode 100644 index 000000000..21d830fe8 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/orphan_definitions.h @@ -0,0 +1,149 @@ +class OrphanDefinitions final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + OrphanDefinitions() : SchemaTransformRule{"orphan_definitions"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &resolver) const + -> bool override { + ONLY_CONTINUE_IF(schema.is_object()); + const bool has_modern_core{ + vocabularies.contains(Vocabularies::Known::JSON_Schema_2020_12_Core) || + vocabularies.contains(Vocabularies::Known::JSON_Schema_2019_09_Core)}; + const bool has_draft_definitions{ + vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_7) || + vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_6) || + vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_4)}; + const bool has_defs{has_modern_core && schema.defines("$defs")}; + const bool has_definitions{(has_modern_core || has_draft_definitions) && + schema.defines("definitions")}; + ONLY_CONTINUE_IF(has_defs || has_definitions); + + const auto base{frame.traverse(frame.root())}; + ONLY_CONTINUE_IF(base.has_value()); + + std::vector orphans; + collect_orphans(frame, base->get(), walker, resolver, location.pointer, + schema, "$defs", has_defs, orphans); + collect_orphans(frame, base->get(), walker, resolver, location.pointer, + schema, "definitions", has_definitions, orphans); + + ONLY_CONTINUE_IF(!orphans.empty()); + this->locations_ = std::move(orphans); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + for (const auto &pointer : this->locations_) { + assert(pointer.size() == 2); + assert(pointer.at(0).is_property()); + assert(pointer.at(1).is_property()); + const auto &container{pointer.at(0).to_property()}; + schema.at(container).erase(pointer.at(1).to_property()); + } + + const auto *defs{schema.try_at("$defs")}; + if (defs && defs->empty()) { + schema.erase("$defs"); + } + + const auto *definitions{schema.try_at("definitions")}; + if (definitions && definitions->empty()) { + schema.erase("definitions"); + } + } + +private: + static auto + subtree_has_dynamic_anchor(const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::core::WeakPointer &entry_pointer) + -> bool { + for (const auto &[key, location] : frame.locations()) { + if (key.first != sourcemeta::blaze::SchemaReferenceType::Dynamic) { + continue; + } + if (location.type != + sourcemeta::blaze::SchemaFrame::LocationType::Anchor) { + continue; + } + if (location.pointer.starts_with(entry_pointer)) { + return true; + } + } + return false; + } + + static auto has_reachable_reference_through( + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &base, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &resolver, + const sourcemeta::core::WeakPointer &pointer) -> bool { + for (const auto &reference : frame.references()) { + const auto destination{frame.traverse(reference.second.destination)}; + if (!destination.has_value()) { + continue; + } + + if (!destination->get().pointer.starts_with(pointer)) { + continue; + } + + const auto &source_pointer{reference.first.second}; + if (source_pointer.empty()) { + return true; + } + + const auto source_location{frame.traverse( + source_pointer.initial(), + sourcemeta::blaze::SchemaFrame::LocationType::Subschema)}; + if (source_location.has_value() && + frame.is_reachable(base, source_location->get(), walker, resolver)) { + return true; + } + } + + return false; + } + + static auto + collect_orphans(const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &base, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &resolver, + const sourcemeta::core::WeakPointer &prefix, + const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON::String &container, + const bool has_container, + std::vector &orphans) -> void { + if (!has_container || !schema.at(container).is_object()) { + return; + } + + for (const auto &entry : schema.at(container).as_object()) { + const sourcemeta::core::WeakPointer entry_pointer{std::cref(container), + std::cref(entry.first)}; + const auto absolute_entry_pointer{prefix.concat(entry_pointer)}; + const auto entry_location{frame.traverse( + absolute_entry_pointer, + sourcemeta::blaze::SchemaFrame::LocationType::Subschema)}; + if (entry_location.has_value() && + !frame.is_reachable(base, entry_location->get(), walker, resolver) && + !has_reachable_reference_through(frame, base, walker, resolver, + absolute_entry_pointer) && + !(!frame.standalone() && + subtree_has_dynamic_anchor(frame, absolute_entry_pointer))) { + orphans.push_back(sourcemeta::core::Pointer{container, entry.first}); + } + } + } + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/recursive_anchor_false_drop.h b/vendor/blaze/src/canonicalizer/rules/recursive_anchor_false_drop.h similarity index 75% rename from vendor/blaze/src/alterschema/canonicalizer/recursive_anchor_false_drop.h rename to vendor/blaze/src/canonicalizer/rules/recursive_anchor_false_drop.h index d9b9310ba..faa505dab 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/recursive_anchor_false_drop.h +++ b/vendor/blaze/src/canonicalizer/rules/recursive_anchor_false_drop.h @@ -1,9 +1,8 @@ class RecursiveAnchorFalseDrop final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; RecursiveAnchorFalseDrop() - : SchemaTransformRule{"recursive_anchor_false_drop", ""} {}; + : SchemaTransformRule{"recursive_anchor_false_drop"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class RecursiveAnchorFalseDrop final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains(Vocabularies::Known::JSON_Schema_2019_09_Core) && schema.is_object()); @@ -24,7 +22,7 @@ class RecursiveAnchorFalseDrop final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.erase("$recursiveAnchor"); } }; diff --git a/vendor/blaze/src/canonicalizer/rules/required_properties_in_properties.h b/vendor/blaze/src/canonicalizer/rules/required_properties_in_properties.h new file mode 100644 index 000000000..ea3e240c2 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/required_properties_in_properties.h @@ -0,0 +1,85 @@ +class RequiredPropertiesInProperties final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + RequiredPropertiesInProperties() + : SchemaTransformRule{"required_properties_in_properties"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &root, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &resolver) const + -> bool override { + ONLY_CONTINUE_IF( + ((vocabularies.contains( + Vocabularies::Known::JSON_Schema_2020_12_Validation) && + vocabularies.contains( + Vocabularies::Known::JSON_Schema_2020_12_Applicator)) || + (vocabularies.contains( + Vocabularies::Known::JSON_Schema_2019_09_Validation) && + vocabularies.contains( + Vocabularies::Known::JSON_Schema_2019_09_Applicator)) || + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4})) && + schema.is_object()); + + const auto *required{schema.try_at("required")}; + ONLY_CONTINUE_IF(required && required->is_array() && !required->empty()); + + const auto *additional_properties{schema.try_at("additionalProperties")}; + ONLY_CONTINUE_IF(!additional_properties || + (additional_properties->is_boolean() && + additional_properties->to_boolean())); + + std::vector locations; + std::size_t index{0}; + for (const auto &property : required->as_array()) { + if (property.is_string() && + !this->defined_in_properties_sibling(schema, property.to_string()) && + !WALK_UP_IN_PLACE_APPLICATORS( + root, frame, location, walker, resolver, + [&](const sourcemeta::core::JSON &ancestor, + const Vocabularies &) -> bool { + return this->defined_in_properties_sibling( + ancestor, property.to_string()); + }) + .has_value()) { + locations.push_back(sourcemeta::core::Pointer{"required", index}); + } + + index += 1; + } + + ONLY_CONTINUE_IF(!locations.empty()); + this->locations_ = std::move(locations); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.assign_if_missing("properties", + sourcemeta::core::JSON::make_object()); + for (const auto &location : this->locations_) { + const auto &property{ + schema.at("required").at(location.at(1).to_index()).to_string()}; + schema.at("properties").assign(property, sourcemeta::core::JSON{true}); + } + } + +private: + [[nodiscard]] auto defined_in_properties_sibling( + const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON::String &property) const -> bool { + assert(schema.is_object()); + const auto *properties{schema.try_at("properties")}; + return properties && properties->is_object() && + properties->defines(property); + }; + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/required_property_implicit.h b/vendor/blaze/src/canonicalizer/rules/required_property_implicit.h similarity index 82% rename from vendor/blaze/src/alterschema/canonicalizer/required_property_implicit.h rename to vendor/blaze/src/canonicalizer/rules/required_property_implicit.h index ffc862da1..a823588c8 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/required_property_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/required_property_implicit.h @@ -1,9 +1,8 @@ class RequiredPropertyImplicit final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; RequiredPropertyImplicit() - : SchemaTransformRule{"required_property_implicit", ""} {}; + : SchemaTransformRule{"required_property_implicit"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class RequiredPropertyImplicit final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_3) && schema.is_object()); @@ -34,8 +32,8 @@ class RequiredPropertyImplicit final : public SchemaTransformRule { return false; } - auto transform(JSON &schema, const Result &) const -> void override { - std::vector keys; + auto transform(sourcemeta::core::JSON &schema) const -> void override { + std::vector keys; for (const auto &entry : schema.at("properties").as_object()) { if (entry.second.is_object() && !entry.second.empty() && !entry.second.defines("$ref") && !entry.second.defines("required")) { diff --git a/vendor/blaze/src/alterschema/canonicalizer/required_to_extends.h b/vendor/blaze/src/canonicalizer/rules/required_to_extends.h similarity index 76% rename from vendor/blaze/src/alterschema/canonicalizer/required_to_extends.h rename to vendor/blaze/src/canonicalizer/rules/required_to_extends.h index 86c7f109c..d33b0bd73 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/required_to_extends.h +++ b/vendor/blaze/src/canonicalizer/rules/required_to_extends.h @@ -1,13 +1,7 @@ class RequiredToExtends final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - RequiredToExtends() - : SchemaTransformRule{ - "required_to_extends", - "In Draft 3 canonical form, `required` is only ever a sibling of " - "`extends`; its other siblings are wrapped into an `extends` " - "branch"} {}; + RequiredToExtends() : SchemaTransformRule{"required_to_extends"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -16,8 +10,7 @@ class RequiredToExtends final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && @@ -35,7 +28,7 @@ class RequiredToExtends final : public SchemaTransformRule { return false; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { this->wrapped_keywords_.clear(); for (const auto &entry : schema.as_object()) { if (!stays_at_top(entry.first)) { @@ -43,7 +36,7 @@ class RequiredToExtends final : public SchemaTransformRule { } } - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; for (const auto &keyword : this->wrapped_keywords_) { branch.assign(keyword, schema.at(keyword)); } @@ -58,23 +51,24 @@ class RequiredToExtends final : public SchemaTransformRule { } else if (schema.defines("extends")) { // Draft 3 allows `extends` to be a single schema; preserve it as the // first branch of the new array - auto extends{JSON::make_array()}; + auto extends{sourcemeta::core::JSON::make_array()}; extends.push_back(schema.at("extends")); this->branch_index_ = extends.size(); extends.push_back(std::move(branch)); schema.assign("extends", std::move(extends)); } else { this->branch_index_ = 0; - auto extends{JSON::make_array()}; + auto extends{sourcemeta::core::JSON::make_array()}; extends.push_back(std::move(branch)); schema.assign("extends", std::move(extends)); } } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { for (const auto &keyword : this->wrapped_keywords_) { const auto keyword_prefix{current.concat(keyword)}; if (target.starts_with(keyword_prefix)) { diff --git a/vendor/blaze/src/alterschema/canonicalizer/single_branch_allof.h b/vendor/blaze/src/canonicalizer/rules/single_branch_allof.h similarity index 70% rename from vendor/blaze/src/alterschema/canonicalizer/single_branch_allof.h rename to vendor/blaze/src/canonicalizer/rules/single_branch_allof.h index 38b4b475d..27dfb7a70 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/single_branch_allof.h +++ b/vendor/blaze/src/canonicalizer/rules/single_branch_allof.h @@ -1,8 +1,7 @@ class SingleBranchAllOf final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - SingleBranchAllOf() : SchemaTransformRule{"single_branch_allof", ""} {}; + SingleBranchAllOf() : SchemaTransformRule{"single_branch_allof"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,9 +10,8 @@ class SingleBranchAllOf final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { - static const JSON::String KEYWORD{"allOf"}; + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"allOf"}; ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Applicator, Vocabularies::Known::JSON_Schema_2019_09_Applicator, @@ -31,7 +29,8 @@ class SingleBranchAllOf final : public SchemaTransformRule { (schema.defines("unevaluatedProperties") || schema.defines("unevaluatedItems")))); ONLY_CONTINUE_IF(!frame.has_references_through( - location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); const auto &branch{all_of->at(0)}; if (branch.is_object()) { ONLY_CONTINUE_IF(!branch.defines("$ref") && @@ -41,13 +40,13 @@ class SingleBranchAllOf final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { auto &branch{schema.at("allOf").at(0)}; if (branch.is_boolean()) { if (branch.to_boolean()) { schema.erase("allOf"); } else { - schema.into(JSON{false}); + schema.into(sourcemeta::core::JSON{false}); } return; } @@ -56,12 +55,13 @@ class SingleBranchAllOf final : public SchemaTransformRule { schema.erase("allOf"); } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { - static const JSON::String KEYWORD{"allOf"}; - const auto prefix{current.concat(Pointer{KEYWORD, 0})}; + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + static const sourcemeta::core::JSON::String KEYWORD{"allOf"}; + const auto prefix{current.concat(sourcemeta::core::Pointer{KEYWORD, 0})}; if (!target.starts_with(prefix)) { return target; } @@ -69,7 +69,8 @@ class SingleBranchAllOf final : public SchemaTransformRule { if (relative.empty()) { return target; } - const Pointer new_prefix{current.concat({relative.at(0)})}; + const sourcemeta::core::Pointer new_prefix{ + current.concat({relative.at(0)})}; return target.rebase(prefix.concat({relative.at(0)}), new_prefix); } }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/single_branch_anyof.h b/vendor/blaze/src/canonicalizer/rules/single_branch_anyof.h similarity index 69% rename from vendor/blaze/src/alterschema/canonicalizer/single_branch_anyof.h rename to vendor/blaze/src/canonicalizer/rules/single_branch_anyof.h index e43abf563..50cd0a528 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/single_branch_anyof.h +++ b/vendor/blaze/src/canonicalizer/rules/single_branch_anyof.h @@ -1,8 +1,7 @@ class SingleBranchAnyOf final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - SingleBranchAnyOf() : SchemaTransformRule{"single_branch_anyof", ""} {}; + SingleBranchAnyOf() : SchemaTransformRule{"single_branch_anyof"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,9 +10,8 @@ class SingleBranchAnyOf final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { - static const JSON::String KEYWORD{"anyOf"}; + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"anyOf"}; ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Applicator, Vocabularies::Known::JSON_Schema_2019_09_Applicator, @@ -25,7 +23,8 @@ class SingleBranchAnyOf final : public SchemaTransformRule { const auto *any_of{schema.try_at(KEYWORD)}; ONLY_CONTINUE_IF(any_of && any_of->is_array() && any_of->size() == 1); ONLY_CONTINUE_IF(!frame.has_references_through( - location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); this->has_unevaluated_ = vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Unevaluated, @@ -35,7 +34,7 @@ class SingleBranchAnyOf final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { if (this->has_unevaluated_) { schema.rename("anyOf", "allOf"); return; @@ -46,7 +45,7 @@ class SingleBranchAnyOf final : public SchemaTransformRule { if (branch.to_boolean()) { schema.erase("anyOf"); } else { - schema.into(JSON{false}); + schema.into(sourcemeta::core::JSON{false}); } return; } @@ -55,17 +54,18 @@ class SingleBranchAnyOf final : public SchemaTransformRule { schema.erase("anyOf"); } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { - static const JSON::String KEYWORD{"anyOf"}; + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + static const sourcemeta::core::JSON::String KEYWORD{"anyOf"}; if (this->has_unevaluated_) { const auto old_prefix{current.concat(KEYWORD)}; - const Pointer new_prefix{current.concat("allOf")}; + const sourcemeta::core::Pointer new_prefix{current.concat("allOf")}; return target.rebase(old_prefix, new_prefix); } - const auto prefix{current.concat(Pointer{KEYWORD, 0})}; + const auto prefix{current.concat(sourcemeta::core::Pointer{KEYWORD, 0})}; if (!target.starts_with(prefix)) { return target; } @@ -73,7 +73,8 @@ class SingleBranchAnyOf final : public SchemaTransformRule { if (relative.empty()) { return target; } - const Pointer new_prefix{current.concat({relative.at(0)})}; + const sourcemeta::core::Pointer new_prefix{ + current.concat({relative.at(0)})}; return target.rebase(prefix.concat({relative.at(0)}), new_prefix); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/single_branch_oneof.h b/vendor/blaze/src/canonicalizer/rules/single_branch_oneof.h similarity index 69% rename from vendor/blaze/src/alterschema/canonicalizer/single_branch_oneof.h rename to vendor/blaze/src/canonicalizer/rules/single_branch_oneof.h index d7b21e9cf..858bab4c5 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/single_branch_oneof.h +++ b/vendor/blaze/src/canonicalizer/rules/single_branch_oneof.h @@ -1,8 +1,7 @@ class SingleBranchOneOf final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - SingleBranchOneOf() : SchemaTransformRule{"single_branch_oneof", ""} {}; + SingleBranchOneOf() : SchemaTransformRule{"single_branch_oneof"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,9 +10,8 @@ class SingleBranchOneOf final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { - static const JSON::String KEYWORD{"oneOf"}; + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + static const sourcemeta::core::JSON::String KEYWORD{"oneOf"}; ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Applicator, Vocabularies::Known::JSON_Schema_2019_09_Applicator, @@ -25,7 +23,8 @@ class SingleBranchOneOf final : public SchemaTransformRule { const auto *one_of{schema.try_at(KEYWORD)}; ONLY_CONTINUE_IF(one_of && one_of->is_array() && one_of->size() == 1); ONLY_CONTINUE_IF(!frame.has_references_through( - location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); this->has_unevaluated_ = vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Unevaluated, @@ -35,7 +34,7 @@ class SingleBranchOneOf final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { if (this->has_unevaluated_) { schema.rename("oneOf", "allOf"); return; @@ -46,7 +45,7 @@ class SingleBranchOneOf final : public SchemaTransformRule { if (branch.to_boolean()) { schema.erase("oneOf"); } else { - schema.into(JSON{false}); + schema.into(sourcemeta::core::JSON{false}); } return; } @@ -55,17 +54,18 @@ class SingleBranchOneOf final : public SchemaTransformRule { schema.erase("oneOf"); } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { - static const JSON::String KEYWORD{"oneOf"}; + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + static const sourcemeta::core::JSON::String KEYWORD{"oneOf"}; if (this->has_unevaluated_) { const auto old_prefix{current.concat(KEYWORD)}; - const Pointer new_prefix{current.concat("allOf")}; + const sourcemeta::core::Pointer new_prefix{current.concat("allOf")}; return target.rebase(old_prefix, new_prefix); } - const auto prefix{current.concat(Pointer{KEYWORD, 0})}; + const auto prefix{current.concat(sourcemeta::core::Pointer{KEYWORD, 0})}; if (!target.starts_with(prefix)) { return target; } @@ -73,7 +73,8 @@ class SingleBranchOneOf final : public SchemaTransformRule { if (relative.empty()) { return target; } - const Pointer new_prefix{current.concat({relative.at(0)})}; + const sourcemeta::core::Pointer new_prefix{ + current.concat({relative.at(0)})}; return target.rebase(prefix.concat({relative.at(0)}), new_prefix); } diff --git a/vendor/blaze/src/canonicalizer/rules/single_type_array.h b/vendor/blaze/src/canonicalizer/rules/single_type_array.h new file mode 100644 index 000000000..5a81f03f2 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/single_type_array.h @@ -0,0 +1,37 @@ +class SingleTypeArray final : public SchemaTransformRule { +public: + using reframe_after_transform = std::false_type; + SingleTypeArray() : SchemaTransformRule{"single_type_array"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1, + Vocabularies::Known::JSON_Schema_Draft_0}) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_array() && type->size() == 1 && + type->front().is_string()); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto type{schema.at("type").front()}; + schema.at("type").into(std::move(type)); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/then_without_if.h b/vendor/blaze/src/canonicalizer/rules/then_without_if.h new file mode 100644 index 000000000..f69d95166 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/then_without_if.h @@ -0,0 +1,33 @@ +class ThenWithoutIf final : public SchemaTransformRule { +private: + // NOLINTNEXTLINE(bugprone-throwing-static-initialization) + static inline const std::string KEYWORD{"then"}; + +public: + using reframe_after_transform = std::true_type; + ThenWithoutIf() : SchemaTransformRule{"then_without_if"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7}) && + schema.is_object() && schema.defines(KEYWORD) && + !schema.defines("if")); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(KEYWORD)})); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase(KEYWORD); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h b/vendor/blaze/src/canonicalizer/rules/type_array_to_any_of.h similarity index 84% rename from vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h rename to vendor/blaze/src/canonicalizer/rules/type_array_to_any_of.h index de4bca749..95edeeb62 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h +++ b/vendor/blaze/src/canonicalizer/rules/type_array_to_any_of.h @@ -1,12 +1,7 @@ class TypeArrayToAnyOf final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - TypeArrayToAnyOf() - : SchemaTransformRule{ - "type_array_to_any_of", - "Setting `type` to more than one choice is syntax sugar to " - "`anyOf` over the corresponding types"} {}; + TypeArrayToAnyOf() : SchemaTransformRule{"type_array_to_any_of"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -15,8 +10,7 @@ class TypeArrayToAnyOf final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( ((vocabularies.contains( @@ -54,10 +48,10 @@ class TypeArrayToAnyOf final : public SchemaTransformRule { } } - return APPLIES_TO_KEYWORDS("type"); + return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { this->keyword_branch_index_.clear(); auto disjunctors{sourcemeta::core::JSON::make_array()}; std::size_t branch_index{0}; @@ -113,9 +107,10 @@ class TypeArrayToAnyOf final : public SchemaTransformRule { } [[nodiscard]] auto rereference(const std::string_view reference, - const Pointer &origin, const Pointer &target, - const Pointer ¤t) const - -> Pointer override { + const sourcemeta::core::Pointer &origin, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { const auto relative{target.resolve_from(current)}; assert(!relative.empty() && relative.at(0).is_property()); const auto &keyword{relative.at(0).to_property()}; @@ -125,9 +120,10 @@ class TypeArrayToAnyOf final : public SchemaTransformRule { current); } - const Pointer old_prefix{current.concat(keyword)}; - const Pointer new_prefix{current.concat(this->disjunctors_prefix_) - .concat(Pointer{match->second, keyword})}; + const sourcemeta::core::Pointer old_prefix{current.concat(keyword)}; + const sourcemeta::core::Pointer new_prefix{ + current.concat(this->disjunctors_prefix_) + .concat(sourcemeta::core::Pointer{match->second, keyword})}; return target.rebase(old_prefix, new_prefix); } @@ -135,5 +131,5 @@ class TypeArrayToAnyOf final : public SchemaTransformRule { mutable std::unordered_map keyword_instances_; mutable std::unordered_map keyword_branch_index_; - mutable Pointer disjunctors_prefix_; + mutable sourcemeta::core::Pointer disjunctors_prefix_; }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_boolean_as_enum.h b/vendor/blaze/src/canonicalizer/rules/type_boolean_as_enum.h similarity index 77% rename from vendor/blaze/src/alterschema/canonicalizer/type_boolean_as_enum.h rename to vendor/blaze/src/canonicalizer/rules/type_boolean_as_enum.h index 8dea52de6..b78b4ad33 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_boolean_as_enum.h +++ b/vendor/blaze/src/canonicalizer/rules/type_boolean_as_enum.h @@ -1,12 +1,7 @@ class TypeBooleanAsEnum final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - TypeBooleanAsEnum() - : SchemaTransformRule{ - "type_boolean_as_enum", - "Setting `type` to `boolean` is syntax sugar for an enumeration " - "of two values: `false` and `true`"} {}; + TypeBooleanAsEnum() : SchemaTransformRule{"type_boolean_as_enum"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -15,8 +10,7 @@ class TypeBooleanAsEnum final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation, @@ -33,10 +27,10 @@ class TypeBooleanAsEnum final : public SchemaTransformRule { const auto *type{schema.try_at("type")}; ONLY_CONTINUE_IF(type && type->is_string() && type->to_string() == "boolean"); - return APPLIES_TO_KEYWORDS("type"); + return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { auto choices = sourcemeta::core::JSON::make_array(); choices.push_back(sourcemeta::core::JSON{false}); choices.push_back(sourcemeta::core::JSON{true}); diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_inherit_in_place.h b/vendor/blaze/src/canonicalizer/rules/type_inherit_in_place.h similarity index 88% rename from vendor/blaze/src/alterschema/canonicalizer/type_inherit_in_place.h rename to vendor/blaze/src/canonicalizer/rules/type_inherit_in_place.h index 033425e78..aa060824a 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_inherit_in_place.h +++ b/vendor/blaze/src/canonicalizer/rules/type_inherit_in_place.h @@ -1,12 +1,7 @@ class TypeInheritInPlace final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - TypeInheritInPlace() - : SchemaTransformRule{ - "type_inherit_in_place", - "An untyped schema inside an in-place applicator inherits " - "the type from its nearest typed ancestor"} {}; + TypeInheritInPlace() : SchemaTransformRule{"type_inherit_in_place"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -15,9 +10,8 @@ class TypeInheritInPlace final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &resolver, const bool) const - -> SchemaTransformRule::Result override { - using namespace sourcemeta::core; + const sourcemeta::blaze::SchemaResolver &resolver) const + -> bool override { ONLY_CONTINUE_IF(schema.is_object()); ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, @@ -55,12 +49,12 @@ class TypeInheritInPlace final : public SchemaTransformRule { return IS_IN_PLACE_APPLICATOR(keyword_type) && keyword_type != SchemaKeywordType::ApplicatorElementsInPlace; }, - [](const JSON &ancestor_schema, const Vocabularies &) -> bool { - return ancestor_schema.defines("type"); - })}; + [](const sourcemeta::core::JSON &ancestor_schema, const Vocabularies &) + -> bool { return ancestor_schema.defines("type"); })}; if (ancestor.has_value()) { - const auto &ancestor_type{get(root, ancestor.value().get()).at("type")}; + const auto &ancestor_type{ + sourcemeta::core::get(root, ancestor.value().get()).at("type")}; if (ancestor_type.is_array()) { for (const auto &element : ancestor_type.as_array()) { if (!element.is_string()) { @@ -96,7 +90,7 @@ class TypeInheritInPlace final : public SchemaTransformRule { if (walk_keyword_type == SchemaKeywordType::ApplicatorElementsInPlace && walk_relative.size() >= 2 && walk_relative.at(1).is_index()) { const auto branch_index{walk_relative.at(1).to_index()}; - const auto &allof_parent{get(root, wp)}; + const auto &allof_parent{sourcemeta::core::get(root, wp)}; const auto &keyword_name{walk_relative.at(0).to_property()}; const auto *branches{allof_parent.is_object() ? allof_parent.try_at(keyword_name) @@ -120,7 +114,7 @@ class TypeInheritInPlace final : public SchemaTransformRule { !sibling_enum->empty()) { const auto inferred{infer_type_from_enum(*sibling_enum)}; if (!inferred.empty()) { - this->inherited_type_ = JSON{inferred}; + this->inherited_type_ = sourcemeta::core::JSON{inferred}; return true; } } @@ -128,8 +122,8 @@ class TypeInheritInPlace final : public SchemaTransformRule { if (sibling_ref && sibling_ref->is_string()) { const auto ref_target{frame.traverse(sibling_ref->to_string())}; if (ref_target.has_value()) { - const auto &ref_schema{ - get(root, ref_target.value().get().pointer)}; + const auto &ref_schema{sourcemeta::core::get( + root, ref_target.value().get().pointer)}; const auto *ref_type{ref_schema.is_object() ? ref_schema.try_at("type") : nullptr}; @@ -150,7 +144,7 @@ class TypeInheritInPlace final : public SchemaTransformRule { return false; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.assign("type", this->inherited_type_); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_null_as_enum.h b/vendor/blaze/src/canonicalizer/rules/type_null_as_enum.h similarity index 77% rename from vendor/blaze/src/alterschema/canonicalizer/type_null_as_enum.h rename to vendor/blaze/src/canonicalizer/rules/type_null_as_enum.h index e8b4e62f8..0d14bd9ef 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_null_as_enum.h +++ b/vendor/blaze/src/canonicalizer/rules/type_null_as_enum.h @@ -1,12 +1,7 @@ class TypeNullAsEnum final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - TypeNullAsEnum() - : SchemaTransformRule{ - "type_null_as_enum", - "Setting `type` to `null` is syntax sugar for an enumeration " - "of a single value: `null`"} {}; + TypeNullAsEnum() : SchemaTransformRule{"type_null_as_enum"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -15,8 +10,7 @@ class TypeNullAsEnum final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation, @@ -32,10 +26,10 @@ class TypeNullAsEnum final : public SchemaTransformRule { const auto *type{schema.try_at("type")}; ONLY_CONTINUE_IF(type && type->is_string() && type->to_string() == "null"); - return APPLIES_TO_KEYWORDS("type"); + return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { auto choices = sourcemeta::core::JSON::make_array(); choices.push_back(sourcemeta::core::JSON{nullptr}); schema.at("type").into(std::move(choices)); diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_union_distribute_keywords.h b/vendor/blaze/src/canonicalizer/rules/type_union_distribute_keywords.h similarity index 87% rename from vendor/blaze/src/alterschema/canonicalizer/type_union_distribute_keywords.h rename to vendor/blaze/src/canonicalizer/rules/type_union_distribute_keywords.h index 70076b94b..487a5c2cf 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_union_distribute_keywords.h +++ b/vendor/blaze/src/canonicalizer/rules/type_union_distribute_keywords.h @@ -1,12 +1,8 @@ class TypeUnionDistributeKeywords final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; TypeUnionDistributeKeywords() - : SchemaTransformRule{ - "type_union_distribute_keywords", - "A type-specific keyword sibling to a `type` union belongs inside " - "the branch of the type that it applies to"} {}; + : SchemaTransformRule{"type_union_distribute_keywords"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -15,8 +11,7 @@ class TypeUnionDistributeKeywords final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && @@ -31,7 +26,7 @@ class TypeUnionDistributeKeywords final : public SchemaTransformRule { this->moves_.clear(); this->wrap_keywords_.clear(); this->wrap_ = false; - std::vector movable; + std::vector movable; for (const auto &entry : schema.as_object()) { // `required` is a property-presence flag, not a value assertion, so it // is never pushed into a branch @@ -89,11 +84,11 @@ class TypeUnionDistributeKeywords final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { if (this->wrap_) { - auto union_branch{JSON::make_object()}; + auto union_branch{sourcemeta::core::JSON::make_object()}; union_branch.assign("type", schema.at("type")); - auto sibling_branch{JSON::make_object()}; + auto sibling_branch{sourcemeta::core::JSON::make_object()}; for (const auto &keyword : this->wrap_keywords_) { sibling_branch.assign(keyword, schema.at(keyword)); } @@ -109,7 +104,7 @@ class TypeUnionDistributeKeywords final : public SchemaTransformRule { this->sibling_index_ = schema.at("extends").size(); schema.at("extends").push_back(std::move(sibling_branch)); } else { - auto extends{JSON::make_array()}; + auto extends{sourcemeta::core::JSON::make_array()}; this->type_index_ = 0; extends.push_back(std::move(union_branch)); this->sibling_index_ = 1; @@ -133,10 +128,11 @@ class TypeUnionDistributeKeywords final : public SchemaTransformRule { } } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { if (this->wrap_) { const auto type_prefix{current.concat("type")}; if (target.starts_with(type_prefix)) { diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_union_implicit.h b/vendor/blaze/src/canonicalizer/rules/type_union_implicit.h similarity index 91% rename from vendor/blaze/src/alterschema/canonicalizer/type_union_implicit.h rename to vendor/blaze/src/canonicalizer/rules/type_union_implicit.h index 4d40c9b2c..54bdf98c0 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_union_implicit.h +++ b/vendor/blaze/src/canonicalizer/rules/type_union_implicit.h @@ -1,11 +1,7 @@ class TypeUnionImplicit final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - TypeUnionImplicit() - : SchemaTransformRule{ - "type_union_implicit", - "Not setting `type` is equivalent to accepting any type"} {}; + TypeUnionImplicit() : SchemaTransformRule{"type_union_implicit"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -14,9 +10,8 @@ class TypeUnionImplicit final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &resolver, const bool) const - -> SchemaTransformRule::Result override { - using namespace sourcemeta::core; + const sourcemeta::blaze::SchemaResolver &resolver) const + -> bool override { ONLY_CONTINUE_IF(schema.is_object() && !schema.empty()); ONLY_CONTINUE_IF(!vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_0, @@ -60,7 +55,7 @@ class TypeUnionImplicit final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { auto types{sourcemeta::core::JSON::make_array()}; types.push_back(sourcemeta::core::JSON{"null"}); @@ -82,7 +77,6 @@ class TypeUnionImplicit final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &walker, const sourcemeta::blaze::SchemaResolver &resolver) -> bool { - using namespace sourcemeta::core; auto walk_pointer{location.pointer}; auto walk_parent{location.parent}; while (walk_parent.has_value()) { @@ -107,7 +101,7 @@ class TypeUnionImplicit final : public SchemaTransformRule { if (walk_keyword_type == SchemaKeywordType::ApplicatorElementsInPlace && walk_relative.size() >= 2 && walk_relative.at(1).is_index()) { const auto branch_index{walk_relative.at(1).to_index()}; - const auto &allof_parent{get(root, wp)}; + const auto &allof_parent{sourcemeta::core::get(root, wp)}; const auto &keyword_name{walk_relative.at(0).to_property()}; const auto *branches{allof_parent.is_object() ? allof_parent.try_at(keyword_name) diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_union_to_schemas.h b/vendor/blaze/src/canonicalizer/rules/type_union_to_schemas.h similarity index 68% rename from vendor/blaze/src/alterschema/canonicalizer/type_union_to_schemas.h rename to vendor/blaze/src/canonicalizer/rules/type_union_to_schemas.h index dd914cbc9..c0e836445 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_union_to_schemas.h +++ b/vendor/blaze/src/canonicalizer/rules/type_union_to_schemas.h @@ -1,8 +1,7 @@ class TypeUnionToSchemas final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; - TypeUnionToSchemas() : SchemaTransformRule{"type_union_to_schemas", ""} {}; + TypeUnionToSchemas() : SchemaTransformRule{"type_union_to_schemas"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -11,8 +10,7 @@ class TypeUnionToSchemas final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1, @@ -32,8 +30,8 @@ class TypeUnionToSchemas final : public SchemaTransformRule { return false; } - auto transform(JSON &schema, const Result &) const -> void override { - auto new_array{JSON::make_array()}; + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto new_array{sourcemeta::core::JSON::make_array()}; for (const auto &element : schema.at("type").as_array()) { if (element.is_string()) { new_array.push_back(type_string_to_schema(element.to_string())); @@ -45,30 +43,31 @@ class TypeUnionToSchemas final : public SchemaTransformRule { } private: - static auto type_string_to_schema(const std::string &type_name) -> JSON { + static auto type_string_to_schema(const std::string &type_name) + -> sourcemeta::core::JSON { if (type_name == "null") { - auto result{JSON::make_object()}; - auto values{JSON::make_array()}; - values.push_back(JSON{nullptr}); + auto result{sourcemeta::core::JSON::make_object()}; + auto values{sourcemeta::core::JSON::make_array()}; + values.push_back(sourcemeta::core::JSON{nullptr}); result.assign("enum", std::move(values)); return result; } if (type_name == "boolean") { - auto result{JSON::make_object()}; - auto values{JSON::make_array()}; - values.push_back(JSON{false}); - values.push_back(JSON{true}); + auto result{sourcemeta::core::JSON::make_object()}; + auto values{sourcemeta::core::JSON::make_array()}; + values.push_back(sourcemeta::core::JSON{false}); + values.push_back(sourcemeta::core::JSON{true}); result.assign("enum", std::move(values)); return result; } if (type_name == "any") { - return JSON::make_object(); + return sourcemeta::core::JSON::make_object(); } - auto result{JSON::make_object()}; - result.assign("type", JSON{type_name}); + auto result{sourcemeta::core::JSON::make_object()}; + result.assign("type", sourcemeta::core::JSON{type_name}); return result; } }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_with_applicator_to_allof.h b/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_allof.h similarity index 91% rename from vendor/blaze/src/alterschema/canonicalizer/type_with_applicator_to_allof.h rename to vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_allof.h index 006e23cb1..276ae2b7d 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_with_applicator_to_allof.h +++ b/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_allof.h @@ -1,9 +1,8 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; TypeWithApplicatorToAllOf() - : SchemaTransformRule{"type_with_applicator_to_allof", ""} {}; + : SchemaTransformRule{"type_with_applicator_to_allof"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &frame, const sourcemeta::blaze::SchemaFrame::Location &location, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_4, @@ -192,7 +190,7 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { this->typed_keywords_.clear(); if (this->strategy_ == Strategy::MergeIntoAllOf) { @@ -200,7 +198,7 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { if (!schema.defines(applicator)) { continue; } - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; branch.assign(applicator, schema.at(applicator)); if (std::string_view{applicator} == "if" && this->has_if_then_else_) { if (schema.defines("then")) { @@ -224,7 +222,7 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { return; } - auto typed_branch{JSON::make_object()}; + auto typed_branch{sourcemeta::core::JSON::make_object()}; for (const auto &entry : schema.as_object()) { if (entry.first == "not" || entry.first == "anyOf" || entry.first == "allOf" || entry.first == "oneOf" || @@ -258,25 +256,25 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { schema.at("allOf").push_back(std::move(typed_branch)); } else { this->typed_branch_index_ = 0; - auto new_allof{JSON::make_array()}; + auto new_allof{sourcemeta::core::JSON::make_array()}; new_allof.push_back(std::move(typed_branch)); schema.assign("allOf", std::move(new_allof)); } if (this->has_modern_ref_ && schema.defines("$ref")) { - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; branch.assign("$ref", schema.at("$ref")); schema.at("allOf").push_back(std::move(branch)); schema.erase("$ref"); } if (this->has_dynamic_ref_ && schema.defines("$dynamicRef")) { - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; branch.assign("$dynamicRef", schema.at("$dynamicRef")); schema.at("allOf").push_back(std::move(branch)); schema.erase("$dynamicRef"); } if (this->has_recursive_ref_ && schema.defines("$recursiveRef")) { - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; branch.assign("$recursiveRef", schema.at("$recursiveRef")); schema.at("allOf").push_back(std::move(branch)); schema.erase("$recursiveRef"); @@ -289,7 +287,7 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { if (this->applicators_with_refs_ & applicator_bit(applicator)) { continue; } - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; branch.assign(applicator, schema.at(applicator)); if (std::string_view{applicator} == "if" && this->has_if_then_else_) { if (schema.defines("then")) { @@ -314,11 +312,11 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { return; } - auto new_allof{JSON::make_array()}; + auto new_allof{sourcemeta::core::JSON::make_array()}; this->applicator_indices_ = 0; if (this->has_modern_ref_ && schema.defines("$ref")) { - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; branch.assign("$ref", schema.at("$ref")); if (this->ref_annotations_only_ && !this->typed_keywords_.empty()) { for (const auto &entry : typed_branch.as_object()) { @@ -329,7 +327,7 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { new_allof.push_back(std::move(branch)); } if (this->has_dynamic_ref_ && schema.defines("$dynamicRef")) { - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; branch.assign("$dynamicRef", schema.at("$dynamicRef")); if (this->ref_annotations_only_ && !this->typed_keywords_.empty()) { for (const auto &entry : typed_branch.as_object()) { @@ -340,7 +338,7 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { new_allof.push_back(std::move(branch)); } if (this->has_recursive_ref_ && schema.defines("$recursiveRef")) { - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; branch.assign("$recursiveRef", schema.at("$recursiveRef")); if (this->ref_annotations_only_ && !this->typed_keywords_.empty()) { for (const auto &entry : typed_branch.as_object()) { @@ -355,7 +353,7 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { if (!schema.defines(applicator)) { continue; } - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; branch.assign(applicator, schema.at(applicator)); if (std::string_view{applicator} == "if" && this->has_if_then_else_) { if (schema.defines("then")) { @@ -373,7 +371,7 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { new_allof.push_back(std::move(typed_branch)); } - auto new_schema{JSON::make_object()}; + auto new_schema{sourcemeta::core::JSON::make_object()}; if (schema.defines("$schema")) { new_schema.assign("$schema", schema.at("$schema")); } @@ -420,23 +418,24 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { schema.into(std::move(new_schema)); } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { const auto relative{target.resolve_from(current)}; if (relative.empty() || !relative.at(0).is_property()) { return target; } const auto &keyword{relative.at(0).to_property()}; - static const JSON::String allof_keyword{"allOf"}; + static const sourcemeta::core::JSON::String allof_keyword{"allOf"}; for (const auto &typed_kw : this->typed_keywords_) { if (typed_kw == keyword) { - const Pointer old_prefix{current.concat(keyword)}; + const sourcemeta::core::Pointer old_prefix{current.concat(keyword)}; if (this->strategy_ == Strategy::SafeExtract) { - const Pointer new_prefix{current.concat( + const sourcemeta::core::Pointer new_prefix{current.concat( {allof_keyword, this->typed_branch_index_, keyword})}; return target.rebase(old_prefix, new_prefix); } else { @@ -445,7 +444,7 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { (this->has_recursive_ref_ ? 1U : 0U) + static_cast(std::popcount( this->applicator_indices_))}; - const Pointer new_prefix{ + const sourcemeta::core::Pointer new_prefix{ current.concat({allof_keyword, typed_index, keyword})}; return target.rebase(old_prefix, new_prefix); } @@ -468,8 +467,8 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { if (keyword == applicator || (this->has_if_then_else_ && std::string_view{applicator} == "if" && (keyword == "then" || keyword == "else"))) { - const Pointer old_prefix{current.concat(keyword)}; - const Pointer new_prefix{ + const sourcemeta::core::Pointer old_prefix{current.concat(keyword)}; + const sourcemeta::core::Pointer new_prefix{ current.concat({allof_keyword, index, keyword})}; return target.rebase(old_prefix, new_prefix); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_with_applicator_to_extends.h b/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_extends.h similarity index 79% rename from vendor/blaze/src/alterschema/canonicalizer/type_with_applicator_to_extends.h rename to vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_extends.h index 65e59b447..e48100fe7 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_with_applicator_to_extends.h +++ b/vendor/blaze/src/canonicalizer/rules/type_with_applicator_to_extends.h @@ -1,9 +1,8 @@ class TypeWithApplicatorToExtends final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; TypeWithApplicatorToExtends() - : SchemaTransformRule{"type_with_applicator_to_extends", ""} {}; + : SchemaTransformRule{"type_with_applicator_to_extends"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class TypeWithApplicatorToExtends final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1, @@ -39,10 +37,10 @@ class TypeWithApplicatorToExtends final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { this->typed_keywords_.clear(); - auto typed_branch{JSON::make_object()}; + auto typed_branch{sourcemeta::core::JSON::make_object()}; for (const auto &entry : schema.as_object()) { if (entry.first == "extends" || entry.first == "disallow" || entry.first == "$schema" || entry.first == "id" || @@ -57,7 +55,7 @@ class TypeWithApplicatorToExtends final : public SchemaTransformRule { schema.erase(key); } - auto new_extends{JSON::make_array()}; + auto new_extends{sourcemeta::core::JSON::make_array()}; this->applicator_indices_ = 0; for (const auto &applicator : APPLICATORS) { @@ -68,7 +66,7 @@ class TypeWithApplicatorToExtends final : public SchemaTransformRule { if (std::string_view{applicator} == "type" && !value.is_array()) { continue; } - auto branch{JSON::make_object()}; + auto branch{sourcemeta::core::JSON::make_object()}; branch.assign(applicator, value); new_extends.push_back(std::move(branch)); this->applicator_indices_ |= applicator_bit(applicator); @@ -78,7 +76,7 @@ class TypeWithApplicatorToExtends final : public SchemaTransformRule { new_extends.push_back(std::move(typed_branch)); } - auto new_schema{JSON::make_object()}; + auto new_schema{sourcemeta::core::JSON::make_object()}; if (schema.defines("$schema")) { new_schema.assign("$schema", schema.at("$schema")); } @@ -89,24 +87,25 @@ class TypeWithApplicatorToExtends final : public SchemaTransformRule { schema.into(std::move(new_schema)); } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { const auto relative{target.resolve_from(current)}; if (relative.empty() || !relative.at(0).is_property()) { return target; } const auto &keyword{relative.at(0).to_property()}; - static const JSON::String extends_keyword{"extends"}; + static const sourcemeta::core::JSON::String extends_keyword{"extends"}; for (const auto &typed_keyword : this->typed_keywords_) { if (typed_keyword == keyword) { - const Pointer old_prefix{current.concat(keyword)}; + const sourcemeta::core::Pointer old_prefix{current.concat(keyword)}; const std::size_t typed_index{ static_cast(std::popcount(this->applicator_indices_))}; - const Pointer new_prefix{ + const sourcemeta::core::Pointer new_prefix{ current.concat({extends_keyword, typed_index, keyword})}; return target.rebase(old_prefix, new_prefix); } @@ -115,8 +114,8 @@ class TypeWithApplicatorToExtends final : public SchemaTransformRule { std::size_t index{0}; for (const auto &applicator : APPLICATORS) { if (keyword == applicator) { - const Pointer old_prefix{current.concat(keyword)}; - const Pointer new_prefix{ + const sourcemeta::core::Pointer old_prefix{current.concat(keyword)}; + const sourcemeta::core::Pointer new_prefix{ current.concat({extends_keyword, index, keyword})}; return target.rebase(old_prefix, new_prefix); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/unevaluated_items_to_items.h b/vendor/blaze/src/canonicalizer/rules/unevaluated_items_to_items.h similarity index 75% rename from vendor/blaze/src/alterschema/canonicalizer/unevaluated_items_to_items.h rename to vendor/blaze/src/canonicalizer/rules/unevaluated_items_to_items.h index 35d7cb2f7..ae8f3abcf 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/unevaluated_items_to_items.h +++ b/vendor/blaze/src/canonicalizer/rules/unevaluated_items_to_items.h @@ -1,9 +1,8 @@ class UnevaluatedItemsToItems final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; UnevaluatedItemsToItems() - : SchemaTransformRule{"unevaluated_items_to_items", ""} {}; + : SchemaTransformRule{"unevaluated_items_to_items"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class UnevaluatedItemsToItems final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Unevaluated, @@ -40,14 +38,15 @@ class UnevaluatedItemsToItems final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.rename("unevaluatedItems", "items"); } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { return target.rebase(current.concat("unevaluatedItems"), current.concat("items")); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/unevaluated_properties_to_additional_properties.h b/vendor/blaze/src/canonicalizer/rules/unevaluated_properties_to_additional_properties.h similarity index 78% rename from vendor/blaze/src/alterschema/canonicalizer/unevaluated_properties_to_additional_properties.h rename to vendor/blaze/src/canonicalizer/rules/unevaluated_properties_to_additional_properties.h index 628c14c7a..3564b701c 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/unevaluated_properties_to_additional_properties.h +++ b/vendor/blaze/src/canonicalizer/rules/unevaluated_properties_to_additional_properties.h @@ -1,11 +1,10 @@ class UnevaluatedPropertiesToAdditionalProperties final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::true_type; UnevaluatedPropertiesToAdditionalProperties() - : SchemaTransformRule{"unevaluated_properties_to_additional_properties", - ""} {}; + : SchemaTransformRule{"unevaluated_properties_to_additional_properties"} { + }; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -14,8 +13,7 @@ class UnevaluatedPropertiesToAdditionalProperties final const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Unevaluated, @@ -42,14 +40,15 @@ class UnevaluatedPropertiesToAdditionalProperties final return true; } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(sourcemeta::core::JSON &schema) const -> void override { schema.rename("unevaluatedProperties", "additionalProperties"); } - [[nodiscard]] auto rereference(const std::string_view, const Pointer &, - const Pointer &target, - const Pointer ¤t) const - -> Pointer override { + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { return target.rebase(current.concat("unevaluatedProperties"), current.concat("additionalProperties")); } diff --git a/vendor/blaze/src/canonicalizer/rules/unknown_keywords_prefix.h b/vendor/blaze/src/canonicalizer/rules/unknown_keywords_prefix.h new file mode 100644 index 000000000..ceabef270 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/unknown_keywords_prefix.h @@ -0,0 +1,71 @@ +class UnknownKeywordsPrefix final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + UnknownKeywordsPrefix() : SchemaTransformRule{"unknown_keywords_prefix"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, const Vocabularies &vocabularies, + const SchemaFrame &, const SchemaFrame::Location &, + const SchemaWalker &walker, const SchemaResolver &) const + -> bool override { + ONLY_CONTINUE_IF(schema.is_object()); + std::vector locations; + for (const auto &entry : schema.as_object()) { + if (entry.first.starts_with("x-")) { + continue; + } + + const auto &metadata = walker(entry.first, vocabularies); + if (metadata.type == SchemaKeywordType::Unknown && + // 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 + !vocabularies.has_unknown()) { + locations.push_back(sourcemeta::core::Pointer{entry.first}); + } + } + + ONLY_CONTINUE_IF(!locations.empty()); + this->locations_ = std::move(locations); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + this->renames_.clear(); + for (const auto &location : this->locations_) { + const auto &keyword{location.at(0).to_property()}; + assert(schema.defines(keyword)); + std::string prefixed_name = "x-" + keyword; + while (schema.defines(prefixed_name)) { + prefixed_name.insert(0, "x-"); + } + + this->renames_.emplace(keyword, prefixed_name); + schema.rename(keyword, std::move(prefixed_name)); + } + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> std::optional override { + for (const auto &[old_name, new_name] : this->renames_) { + auto result{ + target.rebase(current.concat(sourcemeta::core::Pointer{old_name}), + current.concat(sourcemeta::core::Pointer{new_name}))}; + if (result != target) { + return result; + } + } + + return target; + } + +private: + mutable std::unordered_map renames_; + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/unknown_local_ref.h b/vendor/blaze/src/canonicalizer/rules/unknown_local_ref.h new file mode 100644 index 000000000..f7bda5d4e --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/unknown_local_ref.h @@ -0,0 +1,60 @@ +class UnknownLocalRef final : public SchemaTransformRule { +private: + // NOLINTNEXTLINE(bugprone-throwing-static-initialization) + static inline const std::string KEYWORD{"$ref"}; + +public: + using reframe_after_transform = std::true_type; + UnknownLocalRef() : SchemaTransformRule{"unknown_local_ref"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, const Vocabularies &vocabularies, + const SchemaFrame &frame, const SchemaFrame::Location &location, + const SchemaWalker &, const SchemaResolver &) const + -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Core, + Vocabularies::Known::JSON_Schema_2019_09_Core, + // In sourcemeta::core::JSON Schema Draft 7 and older, `$ref` overrides + // siblings. However, we do not need to worry about this case here, as + // if the + // `$ref` points to an unknown local location, the entire schema is + // invalid anyway. We just help at least making the schema valid + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})); + ONLY_CONTINUE_IF(schema.is_object() && schema.defines(KEYWORD) && + schema.at(KEYWORD).is_string()); + + // Find the keyword location entry + auto keyword_pointer{location.pointer}; + keyword_pointer.push_back(std::cref(KEYWORD)); + const auto reference_entry{ + frame.reference(SchemaReferenceType::Static, keyword_pointer)}; + ONLY_CONTINUE_IF(reference_entry.has_value()); + + // If the keyword has no fragment, continue + const auto &reference_fragment{reference_entry->get().fragment}; + ONLY_CONTINUE_IF(reference_fragment.has_value()); + + // Only continue if the reference target does not exist + ONLY_CONTINUE_IF( + !frame.traverse(reference_entry->get().destination).has_value()); + + // If there is a base beyond the fragment, the base must exist. + // Otherwise it is likely an external reference? + const auto &reference_base{reference_entry->get().base}; + if (!reference_base.empty()) { + ONLY_CONTINUE_IF(frame.traverse(reference_base).has_value()); + } + + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.erase(KEYWORD); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/unnecessary_allof_ref_wrapper_draft.h b/vendor/blaze/src/canonicalizer/rules/unnecessary_allof_ref_wrapper_draft.h new file mode 100644 index 000000000..ff3b61270 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/unnecessary_allof_ref_wrapper_draft.h @@ -0,0 +1,39 @@ +class UnnecessaryAllOfRefWrapperDraft final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + UnnecessaryAllOfRefWrapperDraft() + : SchemaTransformRule{"unnecessary_allof_ref_wrapper_draft"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF( + vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4})); + 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()); + + // In Draft 7 and older, `$ref` overrides sibling keywords, so we can only + // elevate it if it is the only keyword of the only branch, and the outer + // subschema only declares `allOf` + ONLY_CONTINUE_IF(all_of->size() == 1); + const auto &entry{all_of->at(0)}; + ONLY_CONTINUE_IF(entry.is_object()); + ONLY_CONTINUE_IF(entry.size() == 1 && entry.defines("$ref")); + + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + auto value{schema.at("allOf").at(0).at("$ref")}; + schema.at("allOf").into(std::move(value)); + schema.rename("allOf", "$ref"); + } +}; diff --git a/vendor/blaze/src/canonicalizer/rules/unnecessary_extends_ref_wrapper.h b/vendor/blaze/src/canonicalizer/rules/unnecessary_extends_ref_wrapper.h new file mode 100644 index 000000000..64f1a5fe1 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/unnecessary_extends_ref_wrapper.h @@ -0,0 +1,57 @@ +class UnnecessaryExtendsRefWrapper final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + UnnecessaryExtendsRefWrapper() + : SchemaTransformRule{"unnecessary_extends_ref_wrapper"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})); + ONLY_CONTINUE_IF(schema.is_object() && schema.size() == 1); + const auto *extends{schema.try_at("extends")}; + ONLY_CONTINUE_IF(extends); + + // In Draft 3, `$ref` overrides sibling keywords, so we can only elevate + // it if it is the only keyword of the only branch, and the outer + // subschema only declares `extends` + if (extends->is_object()) { + ONLY_CONTINUE_IF(extends->size() == 1 && extends->defines("$ref")); + this->locations_ = {{"extends", "$ref"}}; + return true; + } + + if (extends->is_array()) { + ONLY_CONTINUE_IF(extends->size() == 1); + const auto &branch{extends->at(0)}; + ONLY_CONTINUE_IF(branch.is_object()); + ONLY_CONTINUE_IF(branch.size() == 1 && branch.defines("$ref")); + this->locations_ = {{"extends", 0, "$ref"}}; + return true; + } + + return false; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + const auto &location{this->locations_.at(0)}; + if (location.size() == 3) { + auto value{schema.at("extends").at(0).at("$ref")}; + schema.at("extends").into(std::move(value)); + } else { + auto value{schema.at("extends").at("$ref")}; + schema.at("extends").into(std::move(value)); + } + schema.rename("extends", "$ref"); + } + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/canonicalizer/rules/unsatisfiable_drop_validation.h b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_drop_validation.h new file mode 100644 index 000000000..3f946a883 --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_drop_validation.h @@ -0,0 +1,125 @@ +class UnsatisfiableDropValidation final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + UnsatisfiableDropValidation() + : SchemaTransformRule{"unsatisfiable_drop_validation"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const bool is_draft_3{vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})}; + + std::string_view trigger_keyword; + if (is_draft_3) { + const auto *disallow_value{schema.try_at("disallow")}; + if (disallow_value && is_disallow_tautology(*disallow_value)) { + trigger_keyword = "disallow"; + } + } else { + const auto *not_value{schema.try_at("not")}; + if (not_value && sourcemeta::blaze::is_empty_schema(*not_value)) { + trigger_keyword = "not"; + } + } + + ONLY_CONTINUE_IF(!trigger_keyword.empty()); + + std::vector positions; + for (const auto &entry : schema.as_object()) { + if (entry.first == trigger_keyword) { + continue; + } + + const auto &metadata{walker(entry.first, vocabularies)}; + if (!is_removable_keyword_type(metadata.type)) { + continue; + } + + if (frame.has_references_through( + location.pointer, + sourcemeta::core::WeakPointer::Token{std::cref(entry.first)})) { + continue; + } + + positions.push_back(sourcemeta::core::Pointer{entry.first}); + } + + ONLY_CONTINUE_IF(!positions.empty()); + this->locations_ = std::move(positions); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + for (const auto &location : this->locations_) { + schema.erase(location.at(0).to_property()); + } + } + +private: + static auto is_disallow_tautology(const sourcemeta::core::JSON &value) + -> bool { + if (value.is_string()) { + return value.to_string() == "any"; + } + if (sourcemeta::blaze::is_empty_schema(value)) { + return true; + } + if (value.is_array()) { + return std::ranges::any_of( + value.as_array(), [](const auto &entry) -> auto { + if (entry.is_string()) { + return entry.to_string() == "any"; + } + return sourcemeta::blaze::is_empty_schema(entry); + }); + } + return false; + } + + static auto is_removable_keyword_type(const SchemaKeywordType type) -> bool { + switch (type) { + case SchemaKeywordType::Assertion: + case SchemaKeywordType::Reference: + case SchemaKeywordType::LocationMembers: + case SchemaKeywordType::ApplicatorMembersTraversePropertyStatic: + case SchemaKeywordType::ApplicatorMembersTraversePropertyRegex: + case SchemaKeywordType::ApplicatorValueTraverseSomeProperty: + case SchemaKeywordType::ApplicatorValueTraverseAnyPropertyKey: + case SchemaKeywordType::ApplicatorValueTraverseAnyItem: + case SchemaKeywordType::ApplicatorValueTraverseSomeItem: + case SchemaKeywordType::ApplicatorValueTraverseParent: + case SchemaKeywordType::ApplicatorElementsTraverseItem: + case SchemaKeywordType::ApplicatorValueOrElementsTraverseAnyItemOrItem: + case SchemaKeywordType::ApplicatorValueOrElementsInPlace: + case SchemaKeywordType::ApplicatorMembersInPlaceSome: + case SchemaKeywordType::ApplicatorElementsInPlace: + case SchemaKeywordType::ApplicatorElementsInPlaceSome: + case SchemaKeywordType::ApplicatorElementsInPlaceSomeNegate: + case SchemaKeywordType::ApplicatorValueInPlaceMaybe: + case SchemaKeywordType::ApplicatorValueInPlaceOther: + case SchemaKeywordType::ApplicatorValueInPlaceNegate: + return true; + default: + return false; + } + } + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_exclusive_equal_bounds.h b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_exclusive_equal_bounds.h similarity index 86% rename from vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_exclusive_equal_bounds.h rename to vendor/blaze/src/canonicalizer/rules/unsatisfiable_exclusive_equal_bounds.h index 5f694ac29..dc70d1e8e 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_exclusive_equal_bounds.h +++ b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_exclusive_equal_bounds.h @@ -1,9 +1,8 @@ class UnsatisfiableExclusiveEqualBounds final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::false_type; UnsatisfiableExclusiveEqualBounds() - : SchemaTransformRule{"unsatisfiable_exclusive_equal_bounds", ""} {}; + : SchemaTransformRule{"unsatisfiable_exclusive_equal_bounds"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class UnsatisfiableExclusiveEqualBounds final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_4}) && @@ -40,7 +38,7 @@ class UnsatisfiableExclusiveEqualBounds final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { - schema.into(JSON{false}); + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.into(sourcemeta::core::JSON{false}); } }; diff --git a/vendor/blaze/src/canonicalizer/rules/unsatisfiable_in_place_applicator_type.h b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_in_place_applicator_type.h new file mode 100644 index 000000000..9494ca73e --- /dev/null +++ b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_in_place_applicator_type.h @@ -0,0 +1,98 @@ +class UnsatisfiableInPlaceApplicatorType final : public SchemaTransformRule { +public: + using reframe_after_transform = std::true_type; + UnsatisfiableInPlaceApplicatorType() + : SchemaTransformRule{"unsatisfiable_in_place_applicator_type"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &) const -> bool override { + ONLY_CONTINUE_IF(schema.is_object() && schema.defines("type")); + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1, + Vocabularies::Known::JSON_Schema_Draft_0})); + const auto parent_types{parse_schema_type(schema.at("type"))}; + ONLY_CONTINUE_IF(parent_types.any()); + + std::vector locations; + + for (const auto &entry : schema.as_object()) { + const auto &keyword{entry.first}; + const auto &keyword_type{walker(keyword, vocabularies).type}; + + if (keyword_type == SchemaKeywordType::ApplicatorElementsInPlace || + keyword_type == SchemaKeywordType::ApplicatorElementsInPlaceSome) { + if (!entry.second.is_array()) { + continue; + } + + const auto &branches{entry.second}; + for (std::size_t index = 0; index < branches.size(); ++index) { + const auto &branch{branches.at(index)}; + if (!branch.is_object()) { + continue; + } + const auto *branch_type{branch.try_at("type")}; + if (!branch_type) { + continue; + } + + const auto branch_types{parse_schema_type(*branch_type)}; + if (branch_types.any() && (parent_types & branch_types).none()) { + locations.push_back(sourcemeta::core::Pointer{keyword, index}); + } + } + } else if (keyword_type == + SchemaKeywordType::ApplicatorValueInPlaceMaybe || + keyword_type == + SchemaKeywordType::ApplicatorValueInPlaceNegate) { + if (!entry.second.is_object()) { + continue; + } + const auto *branch_type{entry.second.try_at("type")}; + if (!branch_type) { + continue; + } + + const auto branch_types{parse_schema_type(*branch_type)}; + if (branch_types.any() && (parent_types & branch_types).none()) { + locations.push_back(sourcemeta::core::Pointer{keyword}); + } + } + } + + ONLY_CONTINUE_IF(!locations.empty()); + this->locations_ = std::move(locations); + return true; + } + + auto transform(sourcemeta::core::JSON &schema) const -> void override { + for (const auto &location : this->locations_) { + if (location.size() == 2) { + const auto &keyword{location.at(0).to_property()}; + const auto index{location.at(1).to_index()}; + schema.at(keyword).at(index).into(sourcemeta::core::JSON{false}); + } else { + assert(location.size() == 1); + const auto &keyword{location.at(0).to_property()}; + schema.at(keyword).into(sourcemeta::core::JSON{false}); + } + } + } + +private: + mutable std::vector locations_; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_type_and_enum.h b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_type_and_enum.h similarity index 84% rename from vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_type_and_enum.h rename to vendor/blaze/src/canonicalizer/rules/unsatisfiable_type_and_enum.h index ba0a4e242..1537316e3 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_type_and_enum.h +++ b/vendor/blaze/src/canonicalizer/rules/unsatisfiable_type_and_enum.h @@ -1,9 +1,8 @@ class UnsatisfiableTypeAndEnum final : public SchemaTransformRule { public: - using mutates = std::true_type; using reframe_after_transform = std::false_type; UnsatisfiableTypeAndEnum() - : SchemaTransformRule{"unsatisfiable_type_and_enum", ""} {}; + : SchemaTransformRule{"unsatisfiable_type_and_enum"} {}; [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, @@ -12,8 +11,7 @@ class UnsatisfiableTypeAndEnum final : public SchemaTransformRule { const sourcemeta::blaze::SchemaFrame &, const sourcemeta::blaze::SchemaFrame::Location &, const sourcemeta::blaze::SchemaWalker &, - const sourcemeta::blaze::SchemaResolver &, const bool) const - -> SchemaTransformRule::Result override { + const sourcemeta::blaze::SchemaResolver &) const -> bool override { ONLY_CONTINUE_IF( vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_0, @@ -41,7 +39,8 @@ class UnsatisfiableTypeAndEnum final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_2020_12_Validation}) && - declared_types.test(std::to_underlying(JSON::Type::Integer))}; + 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 { @@ -51,7 +50,7 @@ class UnsatisfiableTypeAndEnum final : public SchemaTransformRule { return true; } - auto transform(JSON &schema, const Result &) const -> void override { - schema.into(JSON{false}); + auto transform(sourcemeta::core::JSON &schema) const -> void override { + schema.into(sourcemeta::core::JSON{false}); } }; diff --git a/vendor/blaze/src/codegen/CMakeLists.txt b/vendor/blaze/src/codegen/CMakeLists.txt index cb512d2c5..9437d8645 100644 --- a/vendor/blaze/src/codegen/CMakeLists.txt +++ b/vendor/blaze/src/codegen/CMakeLists.txt @@ -21,4 +21,4 @@ target_link_libraries(sourcemeta_blaze_codegen PUBLIC target_link_libraries(sourcemeta_blaze_codegen PRIVATE sourcemeta::blaze::bundle) target_link_libraries(sourcemeta_blaze_codegen PRIVATE - sourcemeta::blaze::alterschema) + sourcemeta::blaze::canonicalizer) diff --git a/vendor/blaze/src/codegen/codegen.cc b/vendor/blaze/src/codegen/codegen.cc index debc631e8..f7752ec87 100644 --- a/vendor/blaze/src/codegen/codegen.cc +++ b/vendor/blaze/src/codegen/codegen.cc @@ -1,5 +1,5 @@ -#include #include +#include #include #include @@ -70,15 +70,8 @@ auto compile(const sourcemeta::core::JSON &input, // (2) Canonicalize the schema for easier analysis // -------------------------------------------------------------------------- - sourcemeta::blaze::SchemaTransformer canonicalizer; - sourcemeta::blaze::add(canonicalizer, - sourcemeta::blaze::AlterSchemaMode::Canonicalizer); - [[maybe_unused]] const auto canonicalized{canonicalizer.apply( - schema, walker, resolver, - [](const auto &, const auto, const auto, const auto &, - [[maybe_unused]] const auto applied) -> auto { assert(applied); }, - default_dialect, default_id)}; - assert(canonicalized.first); + sourcemeta::blaze::canonicalize(schema, walker, resolver, default_dialect, + default_id); // -------------------------------------------------------------------------- // (3) Frame the resulting schema with instance location information diff --git a/vendor/blaze/src/compiler/default_compiler_draft3.h b/vendor/blaze/src/compiler/default_compiler_draft3.h index ffc9f8080..ae717352e 100644 --- a/vendor/blaze/src/compiler/default_compiler_draft3.h +++ b/vendor/blaze/src/compiler/default_compiler_draft3.h @@ -921,7 +921,8 @@ auto compiler_draft3_applicator_properties_with_options( } if (context.mode == Mode::FastValidation) { - if (fusion_possible && !fusion_entries.empty()) { + if (fusion_possible && !fusion_entries.empty() && + !annotations_collected(context)) { for (const auto &req : required) { const auto &req_name{req.first}; bool already_tracked{false}; @@ -2379,12 +2380,11 @@ auto compiler_draft3_applicator_extends(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - assert(!context.uses_dynamic_scopes); - const auto &value{schema_context.schema.at(dynamic_context.keyword)}; if (value.is_object()) { - if (context.mode == Mode::FastValidation) { + // TODO: Make this work with `$dynamicRef` + if (context.mode == Mode::FastValidation && !context.uses_dynamic_scopes) { return compile(context, schema_context, dynamic_context, sourcemeta::core::empty_weak_pointer, sourcemeta::core::empty_weak_pointer); @@ -2412,7 +2412,8 @@ auto compiler_draft3_applicator_extends(const Context &context, Instructions children; - if (context.mode == Mode::FastValidation) { + // TODO: Make this work with `$dynamicRef` + if (context.mode == Mode::FastValidation && !context.uses_dynamic_scopes) { for (std::uint64_t index = 0; index < value.size(); index++) { for (auto &&step : compile( context, schema_context, dynamic_context, diff --git a/vendor/blaze/src/compiler/default_compiler_draft7.h b/vendor/blaze/src/compiler/default_compiler_draft7.h index 716af57cf..715eb66f8 100644 --- a/vendor/blaze/src/compiler/default_compiler_draft7.h +++ b/vendor/blaze/src/compiler/default_compiler_draft7.h @@ -29,10 +29,47 @@ auto compiler_draft7_applicator_if(const Context &context, .recompose()}; assert(context.frame.locations().contains( {sourcemeta::blaze::SchemaReferenceType::Static, destination})); + + // If `if` compiled to nothing, the `then` children will be hoisted to the + // current level without the `LogicalCondition` wrapper. When the schema + // uses dynamic scoping, we still need a wrapper instruction that keeps the + // current schema resource on the evaluation path for dynamic anchor + // resolution, as the omitted `LogicalCondition` instruction would have + // otherwise done it + if (then_cursor == 0 && context.uses_dynamic_scopes) { + Instructions substeps{ + compile(context, schema_context, relative_dynamic_context(), + sourcemeta::core::empty_weak_pointer, + sourcemeta::core::empty_weak_pointer, destination)}; + if (substeps.empty()) { + return children; + } + + const auto then_pointer{schema_context.relative_pointer.initial().concat( + make_weak_pointer(KEYWORD_THEN))}; + const SchemaContext then_schema_context{ + .relative_pointer = then_pointer, + .schema = schema_context.schema, + .vocabularies = schema_context.vocabularies, + .base = schema_context.base, + .is_property_name = schema_context.is_property_name}; + return {make( + sourcemeta::blaze::InstructionIndex::LogicalAnd, context, + then_schema_context, + {.keyword = KEYWORD_THEN, + .base_schema_location = dynamic_context.base_schema_location, + .base_instance_location = dynamic_context.base_instance_location}, + ValueNone{}, std::move(substeps))}; + } + + // When hoisting, the `then` children must carry the instance navigation + // that the omitted `LogicalCondition` wrapper would have performed DynamicContext new_dynamic_context{ .keyword = KEYWORD_THEN, .base_schema_location = dynamic_context.base_schema_location, - .base_instance_location = sourcemeta::core::empty_weak_pointer}; + .base_instance_location = then_cursor == 0 + ? dynamic_context.base_instance_location + : sourcemeta::core::empty_weak_pointer}; for (auto &&step : compile(context, schema_context, new_dynamic_context, sourcemeta::core::empty_weak_pointer, @@ -46,6 +83,14 @@ auto compiler_draft7_applicator_if(const Context &context, } } + // If `if` compiled to nothing, it always succeeds, so in the absence of + // `then`, the `else` subschema can never apply and the entire conditional + // is a no-op + if (then_cursor == 0) { + assert(children.empty()); + return {}; + } + // `else` std::size_t else_cursor{0}; if (schema_context.schema.defines("else")) { diff --git a/vendor/blaze/src/documentation/CMakeLists.txt b/vendor/blaze/src/documentation/CMakeLists.txt deleted file mode 100644 index 189ac91c2..000000000 --- a/vendor/blaze/src/documentation/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ -sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME documentation - FOLDER "Blaze/Documentation" - SOURCES documentation.cc documentation_html.cc) - -if(BLAZE_INSTALL) - sourcemeta_library_install(NAMESPACE sourcemeta PROJECT blaze NAME documentation) -endif() - -target_link_libraries(sourcemeta_blaze_documentation PUBLIC - sourcemeta::core::json) -target_link_libraries(sourcemeta_blaze_documentation PUBLIC - sourcemeta::blaze::foundation) -target_link_libraries(sourcemeta_blaze_documentation PRIVATE - sourcemeta::blaze::frame) -target_link_libraries(sourcemeta_blaze_documentation PRIVATE - sourcemeta::blaze::alterschema) -target_link_libraries(sourcemeta_blaze_documentation PRIVATE - sourcemeta::core::html) diff --git a/vendor/blaze/src/documentation/documentation.cc b/vendor/blaze/src/documentation/documentation.cc deleted file mode 100644 index 19cf2f3c2..000000000 --- a/vendor/blaze/src/documentation/documentation.cc +++ /dev/null @@ -1,1561 +0,0 @@ -#include - -#include - -#include -#include -#include - -#include // assert -#include // std::int64_t -#include // std::map -#include // std::set -#include // std::ostringstream -#include // std::to_string -#include // std::move - -namespace sourcemeta::blaze { - -namespace { - -auto resolve_destination(const sourcemeta::core::JSON::String &raw_ref, - const sourcemeta::blaze::SchemaFrame &frame) - -> std::optional> { - auto result{frame.traverse(raw_ref)}; - if (result.has_value()) { - return result; - } - for (const auto &[key, entry] : frame.references()) { - if (key.first == sourcemeta::blaze::SchemaReferenceType::Static && - entry.original == raw_ref) { - return frame.traverse(entry.destination); - } - } - return std::nullopt; -} - -struct VisitedEntry { - std::size_t identifier; - sourcemeta::core::JSON path; -}; -using VisitedSchemas = std::map; -using RefChain = std::set; - -auto type_expression_of(const sourcemeta::core::JSON &schema, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, - const VisitedSchemas &visited, RefChain &ref_chain) - -> sourcemeta::core::JSON { - auto result{sourcemeta::core::JSON::make_object()}; - - if (schema.is_boolean()) { - if (schema.to_boolean()) { - result.assign("kind", sourcemeta::core::JSON{"any"}); - } else { - result.assign("kind", sourcemeta::core::JSON{"never"}); - } - return result; - } - - if (!schema.is_object()) { - return result; - } - - if (schema.defines("$ref") && schema.at("$ref").is_string()) { - const auto &destination{schema.at("$ref").to_string()}; - const auto target{resolve_destination(destination, frame)}; - if (!target.has_value()) { - result.assign("kind", sourcemeta::core::JSON{"externalRef"}); - result.assign("url", sourcemeta::core::JSON{destination}); - return result; - } - const auto &target_schema{ - sourcemeta::core::get(root, target->get().pointer)}; - const auto visited_entry{visited.find(&target_schema)}; - if (visited_entry != visited.end()) { - result.assign("kind", sourcemeta::core::JSON{"recursiveRef"}); - result.assign("identifier", - sourcemeta::core::JSON{static_cast( - visited_entry->second.identifier)}); - result.assign("path", visited_entry->second.path); - return result; - } - - if (ref_chain.contains(&target_schema)) { - result.assign("kind", sourcemeta::core::JSON{"any"}); - return result; - } - - ref_chain.insert(&target_schema); - auto ref_result{ - type_expression_of(target_schema, frame, root, visited, ref_chain)}; - ref_chain.erase(&target_schema); - return ref_result; - } - - if (schema.defines("$dynamicRef") && schema.at("$dynamicRef").is_string()) { - const auto &value{schema.at("$dynamicRef").to_string()}; - result.assign("kind", sourcemeta::core::JSON{"dynamicRef"}); - const auto fragment_start{value.find('#')}; - if (fragment_start != sourcemeta::core::JSON::String::npos) { - result.assign("anchor", - sourcemeta::core::JSON{value.substr(fragment_start + 1)}); - } else { - result.assign("anchor", sourcemeta::core::JSON{value}); - } - return result; - } - - if (schema.defines("enum") && schema.at("enum").is_array()) { - result.assign("kind", sourcemeta::core::JSON{"enum"}); - auto values{sourcemeta::core::JSON::make_array()}; - auto overflow{sourcemeta::core::JSON::make_array()}; - std::size_t index{0}; - for (const auto &value : schema.at("enum").as_array()) { - if (index < 10) { - values.push_back(value); - } else { - overflow.push_back(value); - } - ++index; - } - result.assign("values", std::move(values)); - if (!overflow.empty()) { - result.assign("overflow", std::move(overflow)); - } - return result; - } - - if (!schema.defines("type") || !schema.at("type").is_string()) { - result.assign("kind", sourcemeta::core::JSON{"any"}); - return result; - } - - { - const auto &type{schema.at("type").to_string()}; - if (type == "object") { - result.assign("kind", sourcemeta::core::JSON{"object"}); - } else if (type == "array") { - if (schema.defines("prefixItems") && - schema.at("prefixItems").is_array()) { - result.assign("kind", sourcemeta::core::JSON{"tuple"}); - auto items{sourcemeta::core::JSON::make_array()}; - for (const auto &item : schema.at("prefixItems").as_array()) { - items.push_back( - type_expression_of(item, frame, root, visited, ref_chain)); - } - result.assign("items", std::move(items)); - if (schema.defines("items") && schema.at("items").is_object()) { - result.assign("additional", - type_expression_of(schema.at("items"), frame, root, - visited, ref_chain)); - } else if (schema.defines("unevaluatedItems") && - schema.at("unevaluatedItems").is_object()) { - result.assign("additional", - type_expression_of(schema.at("unevaluatedItems"), frame, - root, visited, ref_chain)); - } - } else if (schema.defines("items") && schema.at("items").is_array()) { - result.assign("kind", sourcemeta::core::JSON{"tuple"}); - auto items{sourcemeta::core::JSON::make_array()}; - for (const auto &item : schema.at("items").as_array()) { - items.push_back( - type_expression_of(item, frame, root, visited, ref_chain)); - } - result.assign("items", std::move(items)); - if (schema.defines("additionalItems") && - schema.at("additionalItems").is_object()) { - result.assign("additional", - type_expression_of(schema.at("additionalItems"), frame, - root, visited, ref_chain)); - } - } else { - result.assign("kind", sourcemeta::core::JSON{"array"}); - if (schema.defines("items") && schema.at("items").is_object()) { - result.assign("items", type_expression_of(schema.at("items"), frame, - root, visited, ref_chain)); - } - } - } else if (type == "string") { - result.assign("kind", sourcemeta::core::JSON{"primitive"}); - result.assign("name", sourcemeta::core::JSON{"string"}); - } else if (type == "integer") { - result.assign("kind", sourcemeta::core::JSON{"primitive"}); - result.assign("name", sourcemeta::core::JSON{"integer"}); - } else if (type == "number") { - result.assign("kind", sourcemeta::core::JSON{"primitive"}); - result.assign("name", sourcemeta::core::JSON{"number"}); - } - } - - return result; -} - -auto type_expression_of(const sourcemeta::core::JSON &schema, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, - const VisitedSchemas &visited) - -> sourcemeta::core::JSON { - RefChain ref_chain; - return type_expression_of(schema, frame, root, visited, ref_chain); -} - -auto badges_of(const sourcemeta::core::JSON &schema) -> sourcemeta::core::JSON { - auto badges{sourcemeta::core::JSON::make_array()}; - if (!schema.is_object()) { - return badges; - } - if (schema.defines("format") && schema.at("format").is_string()) { - auto badge{sourcemeta::core::JSON::make_object()}; - badge.assign("kind", sourcemeta::core::JSON{"format"}); - badge.assign("value", - sourcemeta::core::JSON{schema.at("format").to_string()}); - badges.push_back(std::move(badge)); - } - if (schema.defines("contentEncoding") && - schema.at("contentEncoding").is_string()) { - auto badge{sourcemeta::core::JSON::make_object()}; - badge.assign("kind", sourcemeta::core::JSON{"encoding"}); - badge.assign("value", sourcemeta::core::JSON{ - schema.at("contentEncoding").to_string()}); - badges.push_back(std::move(badge)); - } - if (schema.defines("contentMediaType") && - schema.at("contentMediaType").is_string()) { - auto badge{sourcemeta::core::JSON::make_object()}; - badge.assign("kind", sourcemeta::core::JSON{"mime"}); - badge.assign("value", sourcemeta::core::JSON{ - schema.at("contentMediaType").to_string()}); - badges.push_back(std::move(badge)); - } - return badges; -} - -auto modifiers_of(const sourcemeta::core::JSON &schema) - -> sourcemeta::core::JSON { - auto modifiers{sourcemeta::core::JSON::make_array()}; - if (!schema.is_object()) { - return modifiers; - } - if (schema.defines("readOnly") && schema.at("readOnly").is_boolean() && - schema.at("readOnly").to_boolean()) { - modifiers.push_back(sourcemeta::core::JSON{"readOnly"}); - } - if (schema.defines("writeOnly") && schema.at("writeOnly").is_boolean() && - schema.at("writeOnly").to_boolean()) { - modifiers.push_back(sourcemeta::core::JSON{"writeOnly"}); - } - if (schema.defines("deprecated") && schema.at("deprecated").is_boolean() && - schema.at("deprecated").to_boolean()) { - modifiers.push_back(sourcemeta::core::JSON{"deprecated"}); - } - return modifiers; -} - -auto format_json_number(const sourcemeta::core::JSON &value) - -> sourcemeta::core::JSON::String { - std::ostringstream result; - sourcemeta::core::stringify(value, result); - return result.str(); -} - -auto constraints_of(const sourcemeta::core::JSON &schema) - -> sourcemeta::core::JSON { - auto constraints{sourcemeta::core::JSON::make_array()}; - if (!schema.is_object()) { - return constraints; - } - - const auto has_min_length{schema.defines("minLength") && - schema.at("minLength").is_integer()}; - const auto has_max_length{schema.defines("maxLength") && - schema.at("maxLength").is_integer()}; - if (has_min_length && has_max_length && - schema.at("minLength") == schema.at("maxLength")) { - const auto value{schema.at("minLength").to_integer()}; - if (value != 0) { - constraints.push_back(sourcemeta::core::JSON{ - "exactly " + std::to_string(value) + " chars"}); - } - } else { - if (has_min_length) { - const auto value{schema.at("minLength").to_integer()}; - if (value > 0) { - constraints.push_back( - sourcemeta::core::JSON{">= " + std::to_string(value) + " chars"}); - } - } - if (has_max_length) { - constraints.push_back(sourcemeta::core::JSON{ - "<= " + std::to_string(schema.at("maxLength").to_integer()) + - " chars"}); - } - } - - if (schema.defines("minimum") && schema.at("minimum").is_number()) { - const auto exclusive{schema.defines("exclusiveMinimum") && - schema.at("exclusiveMinimum").is_boolean() && - schema.at("exclusiveMinimum").to_boolean()}; - constraints.push_back(sourcemeta::core::JSON{ - (exclusive ? "> " : ">= ") + format_json_number(schema.at("minimum"))}); - } - if (schema.defines("maximum") && schema.at("maximum").is_number()) { - const auto exclusive{schema.defines("exclusiveMaximum") && - schema.at("exclusiveMaximum").is_boolean() && - schema.at("exclusiveMaximum").to_boolean()}; - constraints.push_back(sourcemeta::core::JSON{ - (exclusive ? "< " : "<= ") + format_json_number(schema.at("maximum"))}); - } - if (schema.defines("exclusiveMinimum") && - schema.at("exclusiveMinimum").is_number()) { - constraints.push_back(sourcemeta::core::JSON{ - "> " + format_json_number(schema.at("exclusiveMinimum"))}); - } - if (schema.defines("exclusiveMaximum") && - schema.at("exclusiveMaximum").is_number()) { - constraints.push_back(sourcemeta::core::JSON{ - "< " + format_json_number(schema.at("exclusiveMaximum"))}); - } - - if (schema.defines("multipleOf") && schema.at("multipleOf").is_number()) { - const auto &value{schema.at("multipleOf")}; - if (!value.is_integer() || value.to_integer() != 1) { - constraints.push_back( - sourcemeta::core::JSON{"multiple of " + format_json_number(value)}); - } - } - - if (schema.defines("minItems") && schema.at("minItems").is_integer()) { - const auto value{schema.at("minItems").to_integer()}; - if (value > 0) { - constraints.push_back( - sourcemeta::core::JSON{">= " + std::to_string(value) + " items"}); - } - } - if (schema.defines("maxItems") && schema.at("maxItems").is_integer()) { - constraints.push_back(sourcemeta::core::JSON{ - "<= " + std::to_string(schema.at("maxItems").to_integer()) + " items"}); - } - - if (schema.defines("uniqueItems") && schema.at("uniqueItems").is_boolean() && - schema.at("uniqueItems").to_boolean()) { - constraints.push_back(sourcemeta::core::JSON{"unique"}); - } - - if (schema.defines("minProperties") && - schema.at("minProperties").is_integer()) { - const auto value{schema.at("minProperties").to_integer()}; - if (value > 0) { - bool covered_by_required{false}; - if (schema.defines("required") && schema.at("required").is_array() && - schema.defines("properties") && schema.at("properties").is_object() && - std::cmp_equal(schema.at("required").size(), value)) { - covered_by_required = true; - for (const auto &req : schema.at("required").as_array()) { - if (!req.is_string() || - !schema.at("properties").defines(req.to_string())) { - covered_by_required = false; - break; - } - } - } - if (!covered_by_required) { - constraints.push_back(sourcemeta::core::JSON{ - ">= " + std::to_string(value) + " properties"}); - } - } - } - if (schema.defines("maxProperties") && - schema.at("maxProperties").is_integer()) { - constraints.push_back(sourcemeta::core::JSON{ - "<= " + std::to_string(schema.at("maxProperties").to_integer()) + - " properties"}); - } - - if (schema.defines("pattern") && schema.at("pattern").is_string()) { - constraints.push_back( - sourcemeta::core::JSON{"pattern: " + schema.at("pattern").to_string()}); - } - - const auto has_trivial_contains{schema.defines("contains") && - schema.at("contains").is_boolean() && - schema.at("contains").to_boolean()}; - - if (schema.defines("contains") && schema.at("contains").is_object()) { - const auto &contains_schema{schema.at("contains")}; - const auto is_flat{!contains_schema.defines("anyOf") && - !contains_schema.defines("oneOf") && - !contains_schema.defines("allOf") && - !contains_schema.defines("not") && - !contains_schema.defines("enum")}; - if (is_flat) { - if (contains_schema.defines("type") && - contains_schema.at("type").is_string()) { - constraints.push_back(sourcemeta::core::JSON{ - "contains: " + contains_schema.at("type").to_string()}); - } - - const auto inner{constraints_of(contains_schema)}; - for (const auto &constraint : inner.as_array()) { - constraints.push_back( - sourcemeta::core::JSON{"contains " + constraint.to_string()}); - } - } - } - - const auto has_min_contains{!has_trivial_contains && - schema.defines("minContains") && - schema.at("minContains").is_integer()}; - const auto has_max_contains{!has_trivial_contains && - schema.defines("maxContains") && - schema.at("maxContains").is_integer()}; - if (has_min_contains && has_max_contains && - schema.at("minContains") == schema.at("maxContains")) { - constraints.push_back(sourcemeta::core::JSON{ - "exactly " + std::to_string(schema.at("minContains").to_integer()) + - " matching items"}); - } else { - if (has_min_contains) { - const auto value{schema.at("minContains").to_integer()}; - if (value == 0) { - constraints.push_back( - sourcemeta::core::JSON{"0 or more matching items"}); - } else { - constraints.push_back(sourcemeta::core::JSON{ - ">= " + std::to_string(value) + " matching items"}); - } - } - if (has_max_contains) { - constraints.push_back(sourcemeta::core::JSON{ - "<= " + std::to_string(schema.at("maxContains").to_integer()) + - " matching items"}); - } - } - - if (schema.defines("propertyNames") && - schema.at("propertyNames").is_object()) { - const auto &names_schema{schema.at("propertyNames")}; - const auto is_branching{ - names_schema.defines("anyOf") || names_schema.defines("oneOf") || - names_schema.defines("allOf") || names_schema.defines("not")}; - if (!is_branching) { - const auto inner{constraints_of(names_schema)}; - if (inner.empty() && names_schema.defines("type") && - names_schema.at("type").is_string()) { - constraints.push_back(sourcemeta::core::JSON{ - "keys: " + names_schema.at("type").to_string()}); - } - - for (const auto &constraint : inner.as_array()) { - constraints.push_back( - sourcemeta::core::JSON{"keys " + constraint.to_string()}); - } - } - } - - if (schema.defines("contentSchema") && - schema.at("contentSchema").is_object()) { - const auto &content_schema{schema.at("contentSchema")}; - const auto is_branching{ - content_schema.defines("anyOf") || content_schema.defines("oneOf") || - content_schema.defines("allOf") || content_schema.defines("not")}; - if (!is_branching) { - const auto inner{constraints_of(content_schema)}; - if (inner.empty() && content_schema.defines("type") && - content_schema.at("type").is_string()) { - constraints.push_back(sourcemeta::core::JSON{ - "decoded: " + content_schema.at("type").to_string()}); - } - - for (const auto &constraint : inner.as_array()) { - constraints.push_back( - sourcemeta::core::JSON{"decoded " + constraint.to_string()}); - } - } - } - - if (schema.defines("not") && schema.at("not").is_object()) { - const auto ¬_schema{schema.at("not")}; - const auto is_branching{ - not_schema.defines("anyOf") || not_schema.defines("oneOf") || - not_schema.defines("allOf") || not_schema.defines("not")}; - if (!is_branching) { - const auto inner{constraints_of(not_schema)}; - for (const auto &constraint : inner.as_array()) { - constraints.push_back( - sourcemeta::core::JSON{"must NOT match " + constraint.to_string()}); - } - } - } - - return constraints; -} - -auto is_required_property(const sourcemeta::core::JSON &schema, - const sourcemeta::core::JSON::String &property) - -> bool { - if (!schema.is_object() || !schema.defines("required") || - !schema.at("required").is_array()) { - return false; - } - for (const auto &item : schema.at("required").as_array()) { - if (item.is_string() && item.to_string() == property) { - return true; - } - } - return false; -} - -auto make_path_segment(const sourcemeta::core::JSON::String &type, - const sourcemeta::core::JSON::String &value) - -> sourcemeta::core::JSON { - auto segment{sourcemeta::core::JSON::make_object()}; - segment.assign("type", sourcemeta::core::JSON{type}); - segment.assign("value", sourcemeta::core::JSON{value}); - return segment; -} - -auto make_section(const std::string &label, sourcemeta::core::JSON tables) - -> sourcemeta::core::JSON { - auto section{sourcemeta::core::JSON::make_object()}; - section.assign("label", sourcemeta::core::JSON{label}); - section.assign("children", std::move(tables)); - return section; -} - -auto walk_schema(const sourcemeta::core::JSON &schema, bool include_root, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, VisitedSchemas &visited, - std::size_t &next_identifier) -> sourcemeta::core::JSON; - -auto is_complex_schema(const sourcemeta::core::JSON &schema) -> bool; - -auto walk_branching_subschema(const std::string &label, - const std::string &synthetic_name, - const sourcemeta::core::JSON &inner_schema, - sourcemeta::core::JSON &doc_children, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, - VisitedSchemas &visited, - std::size_t &next_identifier, - bool include_properties) -> void; - -auto walk_branches(const std::string &keyword, const std::string &label, - const sourcemeta::core::JSON &schema, - sourcemeta::core::JSON &children, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, VisitedSchemas &visited, - std::size_t &next_identifier) -> void; - -auto walk_all_of(const sourcemeta::core::JSON &schema, - sourcemeta::core::JSON &rows, sourcemeta::core::JSON &children, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, VisitedSchemas &visited, - std::size_t &next_identifier) -> void; - -auto walk_if_then_else(const sourcemeta::core::JSON &schema, - sourcemeta::core::JSON &children, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, - VisitedSchemas &visited, std::size_t &next_identifier) - -> void; - -auto walk_wildcard_keyword(const sourcemeta::core::JSON &schema, - const std::string &keyword, - const sourcemeta::core::JSON &base_path, - sourcemeta::core::JSON &rows, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, - VisitedSchemas &visited, - std::size_t &next_identifier) -> void; - -auto walk_pattern_properties(const sourcemeta::core::JSON &schema, - const sourcemeta::core::JSON &base_path, - sourcemeta::core::JSON &rows, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, - VisitedSchemas &visited, - std::size_t &next_identifier) -> void; - -auto resolve_ref(const sourcemeta::core::JSON &schema, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, - const VisitedSchemas &visited) - -> const sourcemeta::core::JSON & { - if (schema.is_object() && schema.defines("$ref") && - schema.at("$ref").is_string()) { - const auto target{ - resolve_destination(schema.at("$ref").to_string(), frame)}; - if (target.has_value()) { - const auto &target_schema{ - sourcemeta::core::get(root, target->get().pointer)}; - if (visited.find(&target_schema) != visited.end()) { - return schema; // NOLINT(bugprone-return-const-ref-from-parameter) - } - return target_schema; - } - } - return schema; // NOLINT(bugprone-return-const-ref-from-parameter) -} - -auto emit_row(const sourcemeta::core::JSON &schema, sourcemeta::core::JSON path, - sourcemeta::core::JSON &rows, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, VisitedSchemas &visited, - std::size_t &next_identifier, - const bool expand_applicators = true) -> void { - auto row{sourcemeta::core::JSON::make_object()}; - row.assign("identifier", sourcemeta::core::JSON{ - static_cast(next_identifier++)}); - row.assign("path", std::move(path)); - - auto modifiers{modifiers_of(schema)}; - if (!modifiers.empty()) { - row.assign("modifiers", std::move(modifiers)); - } - - row.assign("type", type_expression_of(schema, frame, root, visited)); - - auto badges{badges_of(schema)}; - if (!badges.empty()) { - row.assign("badges", std::move(badges)); - } - - auto constraints{constraints_of(schema)}; - if (!constraints.empty()) { - row.assign("constraints", std::move(constraints)); - } - - if (schema.is_object()) { - if (schema.defines("title") && schema.at("title").is_string()) { - row.assign("title", - sourcemeta::core::JSON{schema.at("title").to_string()}); - } - if (schema.defines("description") && schema.at("description").is_string()) { - row.assign("description", - sourcemeta::core::JSON{schema.at("description").to_string()}); - } - if (schema.defines("default")) { - row.assign("default", schema.at("default")); - } - if (schema.defines("examples") && schema.at("examples").is_array()) { - auto examples{sourcemeta::core::JSON::make_array()}; - for (const auto &example : schema.at("examples").as_array()) { - examples.push_back(example); - } - row.assign("examples", std::move(examples)); - } - } - - if (expand_applicators && is_complex_schema(schema)) { - auto row_children{sourcemeta::core::JSON::make_array()}; - walk_branches("anyOf", "Any of", schema, row_children, frame, root, visited, - next_identifier); - walk_branches("oneOf", "One of", schema, row_children, frame, root, visited, - next_identifier); - walk_all_of(schema, rows, row_children, frame, root, visited, - next_identifier); - walk_if_then_else(schema, row_children, frame, root, visited, - next_identifier); - if (schema.is_object() && schema.defines("not")) { - const auto ¬_schema{schema.at("not")}; - const auto has_inline{ - not_schema.is_object() && - !(not_schema.defines("anyOf") || not_schema.defines("oneOf") || - not_schema.defines("allOf") || not_schema.defines("not")) && - !constraints_of(not_schema).empty()}; - if (!has_inline) { - walk_branching_subschema("Must NOT match", "value", not_schema, - row_children, frame, root, visited, - next_identifier, false); - } - } - if (!row_children.empty()) { - row.assign("children", std::move(row_children)); - } - } - - rows.push_back(std::move(row)); -} - -auto walk_properties(const sourcemeta::core::JSON &schema, - const sourcemeta::core::JSON &base_path, - sourcemeta::core::JSON &rows, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, - VisitedSchemas &visited, std::size_t &next_identifier) - -> void { - if (!schema.is_object() || !schema.defines("properties") || - !schema.at("properties").is_object()) { - return; - } - - for (const auto &entry : schema.at("properties").as_object()) { - const auto &resolved{resolve_ref(entry.second, frame, root, visited)}; - auto path{base_path}; - path.push_back(make_path_segment("literal", entry.first)); - - auto row{sourcemeta::core::JSON::make_object()}; - row.assign("identifier", sourcemeta::core::JSON{ - static_cast(next_identifier++)}); - row.assign("path", path); - - auto modifiers{modifiers_of(resolved)}; - if (!modifiers.empty()) { - row.assign("modifiers", std::move(modifiers)); - } - - row.assign("type", type_expression_of(resolved, frame, root, visited)); - - auto badges{badges_of(resolved)}; - if (!badges.empty()) { - row.assign("badges", std::move(badges)); - } - - row.assign("required", sourcemeta::core::JSON{ - is_required_property(schema, entry.first)}); - - auto constraints{constraints_of(resolved)}; - if (!constraints.empty()) { - row.assign("constraints", std::move(constraints)); - } - - if (resolved.is_object()) { - if (resolved.defines("title") && resolved.at("title").is_string()) { - row.assign("title", - sourcemeta::core::JSON{resolved.at("title").to_string()}); - } - if (resolved.defines("description") && - resolved.at("description").is_string()) { - row.assign("description", sourcemeta::core::JSON{ - resolved.at("description").to_string()}); - } - if (resolved.defines("default")) { - row.assign("default", resolved.at("default")); - } - if (resolved.defines("examples") && resolved.at("examples").is_array()) { - auto examples{sourcemeta::core::JSON::make_array()}; - for (const auto &example : resolved.at("examples").as_array()) { - examples.push_back(example); - } - row.assign("examples", std::move(examples)); - } - } - - const auto row_identifier{ - static_cast(row.at("identifier").to_integer())}; - - if (is_complex_schema(resolved)) { - auto prop_children{sourcemeta::core::JSON::make_array()}; - walk_branches("anyOf", "Any of", resolved, prop_children, frame, root, - visited, next_identifier); - walk_branches("oneOf", "One of", resolved, prop_children, frame, root, - visited, next_identifier); - walk_all_of(resolved, rows, prop_children, frame, root, visited, - next_identifier); - walk_if_then_else(resolved, prop_children, frame, root, visited, - next_identifier); - if (resolved.defines("not")) { - const auto ¬_schema{resolved.at("not")}; - const auto has_inline{ - not_schema.is_object() && - !(not_schema.defines("anyOf") || not_schema.defines("oneOf") || - not_schema.defines("allOf") || not_schema.defines("not")) && - !constraints_of(not_schema).empty()}; - if (!has_inline) { - walk_branching_subschema("Must NOT match", "value", not_schema, - prop_children, frame, root, visited, - next_identifier, false); - } - } - if (!prop_children.empty()) { - row.assign("children", std::move(prop_children)); - } - } - - rows.push_back(std::move(row)); - - if (resolved.is_object() && resolved.defines("type") && - resolved.at("type").is_string()) { - const auto &resolved_type{resolved.at("type").to_string()}; - if (resolved_type == "object") { - visited.emplace(&resolved, VisitedEntry{.identifier = row_identifier, - .path = path}); - walk_properties(resolved, path, rows, frame, root, visited, - next_identifier); - walk_pattern_properties(resolved, path, rows, frame, root, visited, - next_identifier); - walk_wildcard_keyword(resolved, "additionalProperties", path, rows, - frame, root, visited, next_identifier); - walk_wildcard_keyword(resolved, "unevaluatedProperties", path, rows, - frame, root, visited, next_identifier); - if (!resolved.defines("additionalProperties") && - !resolved.defines("unevaluatedProperties")) { - auto open_path{path}; - open_path.push_back(make_path_segment("wildcard", "*")); - emit_row(sourcemeta::core::JSON{true}, std::move(open_path), rows, - frame, root, visited, next_identifier); - } - visited.erase(&resolved); - } else if (resolved_type == "array" && resolved.defines("items") && - resolved.at("items").is_object() && - !resolved.defines("prefixItems")) { - const auto &items_schema{ - resolve_ref(resolved.at("items"), frame, root, visited)}; - if (items_schema.is_object()) { - auto wildcard_path{path}; - wildcard_path.push_back(make_path_segment("wildcard", "*")); - const auto items_row_id{next_identifier}; - emit_row(items_schema, wildcard_path, rows, frame, root, visited, - next_identifier); - if (items_schema.defines("type") && - items_schema.at("type").is_string() && - items_schema.at("type").to_string() == "object") { - visited.emplace(&items_schema, - VisitedEntry{.identifier = items_row_id, - .path = wildcard_path}); - walk_properties(items_schema, wildcard_path, rows, frame, root, - visited, next_identifier); - walk_pattern_properties(items_schema, wildcard_path, rows, frame, - root, visited, next_identifier); - walk_wildcard_keyword(items_schema, "additionalProperties", - wildcard_path, rows, frame, root, visited, - next_identifier); - walk_wildcard_keyword(items_schema, "unevaluatedProperties", - wildcard_path, rows, frame, root, visited, - next_identifier); - if (!items_schema.defines("additionalProperties") && - !items_schema.defines("unevaluatedProperties")) { - auto open_path{wildcard_path}; - open_path.push_back(make_path_segment("wildcard", "*")); - emit_row(sourcemeta::core::JSON{true}, std::move(open_path), rows, - frame, root, visited, next_identifier); - } - visited.erase(&items_schema); - } - } - } - } - } -} - -auto walk_wildcard_keyword(const sourcemeta::core::JSON &schema, - const std::string &keyword, - const sourcemeta::core::JSON &base_path, - sourcemeta::core::JSON &rows, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, - VisitedSchemas &visited, - std::size_t &next_identifier) -> void { - if (!schema.is_object() || !schema.defines(keyword)) { - return; - } - - const auto &value{schema.at(keyword)}; - - if (keyword == "unevaluatedItems" && schema.defines("prefixItems")) { - return; - } - - if (value.is_boolean() && value.to_boolean()) { - auto path{base_path}; - path.push_back(make_path_segment("wildcard", "*")); - emit_row(value, std::move(path), rows, frame, root, visited, - next_identifier); - return; - } - - if (!value.is_object()) { - return; - } - - auto path{base_path}; - path.push_back(make_path_segment("wildcard", "*")); - const auto wildcard_row_id{next_identifier}; - emit_row(value, path, rows, frame, root, visited, next_identifier); - - if (value.defines("type") && value.at("type").is_string() && - value.at("type").to_string() == "object") { - visited.emplace(&value, - VisitedEntry{.identifier = wildcard_row_id, .path = path}); - walk_properties(value, path, rows, frame, root, visited, next_identifier); - walk_pattern_properties(value, path, rows, frame, root, visited, - next_identifier); - walk_wildcard_keyword(value, "additionalProperties", path, rows, frame, - root, visited, next_identifier); - walk_wildcard_keyword(value, "unevaluatedProperties", path, rows, frame, - root, visited, next_identifier); - if (!value.defines("additionalProperties") && - !value.defines("unevaluatedProperties")) { - auto open_path{path}; - open_path.push_back(make_path_segment("wildcard", "*")); - emit_row(sourcemeta::core::JSON{true}, std::move(open_path), rows, frame, - root, visited, next_identifier); - } - visited.erase(&value); - } else if (value.defines("type") && value.at("type").is_string() && - value.at("type").to_string() == "array" && - value.defines("items") && value.at("items").is_object() && - !value.defines("prefixItems")) { - const auto &items_schema{ - resolve_ref(value.at("items"), frame, root, visited)}; - if (items_schema.is_object()) { - auto items_path{path}; - items_path.push_back(make_path_segment("wildcard", "*")); - const auto items_row_id{next_identifier}; - emit_row(items_schema, items_path, rows, frame, root, visited, - next_identifier); - if (items_schema.defines("type") && items_schema.at("type").is_string() && - items_schema.at("type").to_string() == "object") { - visited.emplace(&items_schema, VisitedEntry{.identifier = items_row_id, - .path = items_path}); - walk_properties(items_schema, items_path, rows, frame, root, visited, - next_identifier); - walk_pattern_properties(items_schema, items_path, rows, frame, root, - visited, next_identifier); - walk_wildcard_keyword(items_schema, "additionalProperties", items_path, - rows, frame, root, visited, next_identifier); - walk_wildcard_keyword(items_schema, "unevaluatedProperties", items_path, - rows, frame, root, visited, next_identifier); - if (!items_schema.defines("additionalProperties") && - !items_schema.defines("unevaluatedProperties")) { - auto open_path{items_path}; - open_path.push_back(make_path_segment("wildcard", "*")); - emit_row(sourcemeta::core::JSON{true}, std::move(open_path), rows, - frame, root, visited, next_identifier); - } - visited.erase(&items_schema); - } - } - } -} - -auto walk_pattern_properties(const sourcemeta::core::JSON &schema, - const sourcemeta::core::JSON &base_path, - sourcemeta::core::JSON &rows, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, - VisitedSchemas &visited, - std::size_t &next_identifier) -> void { - if (!schema.is_object() || !schema.defines("patternProperties") || - !schema.at("patternProperties").is_object()) { - return; - } - - for (const auto &entry : schema.at("patternProperties").as_object()) { - const auto &resolved{resolve_ref(entry.second, frame, root, visited)}; - auto path{base_path}; - path.push_back(make_path_segment("pattern", entry.first)); - - const auto row_id{next_identifier}; - emit_row(resolved, path, rows, frame, root, visited, next_identifier); - - if (resolved.is_object() && resolved.defines("type") && - resolved.at("type").is_string() && - resolved.at("type").to_string() == "object") { - visited.emplace(&resolved, - VisitedEntry{.identifier = row_id, .path = path}); - walk_properties(resolved, path, rows, frame, root, visited, - next_identifier); - walk_pattern_properties(resolved, path, rows, frame, root, visited, - next_identifier); - walk_wildcard_keyword(resolved, "additionalProperties", path, rows, frame, - root, visited, next_identifier); - walk_wildcard_keyword(resolved, "unevaluatedProperties", path, rows, - frame, root, visited, next_identifier); - if (!resolved.defines("additionalProperties") && - !resolved.defines("unevaluatedProperties")) { - auto open_path{path}; - open_path.push_back(make_path_segment("wildcard", "*")); - emit_row(sourcemeta::core::JSON{true}, std::move(open_path), rows, - frame, root, visited, next_identifier); - } - visited.erase(&resolved); - } - } -} - -auto is_complex_schema(const sourcemeta::core::JSON &schema) -> bool { - if (!schema.is_object()) { - return false; - } - return schema.defines("properties") || schema.defines("anyOf") || - schema.defines("oneOf") || schema.defines("allOf") || - schema.defines("not") || schema.defines("if") || - schema.defines("prefixItems") || schema.defines("contains") || - schema.defines("patternProperties") || - schema.defines("additionalProperties") || - schema.defines("propertyNames") || schema.defines("contentSchema"); -} - -auto walk_prefix_items(const sourcemeta::core::JSON &schema, - const sourcemeta::core::JSON &base_path, - sourcemeta::core::JSON &rows, - sourcemeta::core::JSON &children, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, - VisitedSchemas &visited, std::size_t &next_identifier) - -> void { - const auto has_prefix_items{schema.is_object() && - schema.defines("prefixItems") && - schema.at("prefixItems").is_array()}; - const auto has_draft4_tuple{!has_prefix_items && schema.is_object() && - schema.defines("items") && - schema.at("items").is_array()}; - if (!has_prefix_items && !has_draft4_tuple) { - return; - } - - const auto &tuple_items{has_prefix_items ? schema.at("prefixItems") - : schema.at("items")}; - - std::size_t min_items{0}; - if (schema.defines("minItems") && schema.at("minItems").is_integer() && - schema.at("minItems").to_integer() > 0) { - min_items = static_cast(schema.at("minItems").to_integer()); - } - - std::size_t index{0}; - for (const auto &item : tuple_items.as_array()) { - if (is_complex_schema(item)) { - auto section_children{sourcemeta::core::JSON::make_array()}; - section_children.push_back( - walk_schema(item, true, frame, root, visited, next_identifier)); - children.push_back(make_section("Array item " + std::to_string(index), - std::move(section_children))); - } else { - auto path{base_path}; - path.push_back(make_path_segment("literal", std::to_string(index))); - - auto row{sourcemeta::core::JSON::make_object()}; - row.assign("identifier", sourcemeta::core::JSON{static_cast( - next_identifier++)}); - row.assign("path", std::move(path)); - - auto modifiers{modifiers_of(item)}; - if (!modifiers.empty()) { - row.assign("modifiers", std::move(modifiers)); - } - - row.assign("type", type_expression_of(item, frame, root, visited)); - - auto badges{badges_of(item)}; - if (!badges.empty()) { - row.assign("badges", std::move(badges)); - } - - row.assign("required", sourcemeta::core::JSON{index < min_items}); - - auto constraints{constraints_of(item)}; - if (!constraints.empty()) { - row.assign("constraints", std::move(constraints)); - } - - if (item.is_object()) { - if (item.defines("title") && item.at("title").is_string()) { - row.assign("title", - sourcemeta::core::JSON{item.at("title").to_string()}); - } - if (item.defines("description") && item.at("description").is_string()) { - row.assign("description", sourcemeta::core::JSON{ - item.at("description").to_string()}); - } - if (item.defines("default")) { - row.assign("default", item.at("default")); - } - if (item.defines("examples") && item.at("examples").is_array()) { - auto examples{sourcemeta::core::JSON::make_array()}; - for (const auto &example : item.at("examples").as_array()) { - examples.push_back(example); - } - row.assign("examples", std::move(examples)); - } - } - - rows.push_back(std::move(row)); - } - ++index; - } - - if (has_prefix_items && schema.defines("items") && - schema.at("items").is_object()) { - auto path{base_path}; - path.push_back(make_path_segment("wildcard", "*")); - emit_row(schema.at("items"), std::move(path), rows, frame, root, visited, - next_identifier); - } else if (has_draft4_tuple && schema.defines("additionalItems") && - schema.at("additionalItems").is_object()) { - auto path{base_path}; - path.push_back(make_path_segment("wildcard", "*")); - emit_row(schema.at("additionalItems"), std::move(path), rows, frame, root, - visited, next_identifier); - } -} - -auto walk_branches(const std::string &keyword, const std::string &label, - const sourcemeta::core::JSON &schema, - sourcemeta::core::JSON &children, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, VisitedSchemas &visited, - std::size_t &next_identifier) -> void { - if (!schema.is_object() || !schema.defines(keyword) || - !schema.at(keyword).is_array()) { - return; - } - - auto section_children{sourcemeta::core::JSON::make_array()}; - for (const auto &branch : schema.at(keyword).as_array()) { - section_children.push_back( - walk_schema(branch, false, frame, root, visited, next_identifier)); - } - children.push_back(make_section(label, std::move(section_children))); -} - -auto has_recursive_ref_in_rows(const sourcemeta::core::JSON &rows) -> bool { - for (const auto &row : rows.as_array()) { - const auto &type{row.at("type")}; - if (type.defines("kind") && type.at("kind").to_string() == "recursiveRef") { - return true; - } - if (type.defines("kind") && type.at("kind").to_string() == "array" && - type.defines("items")) { - const auto &items{type.at("items")}; - if (items.is_object() && items.defines("kind") && - items.at("kind").to_string() == "recursiveRef") { - return true; - } - } - } - return false; -} - -auto walk_all_of(const sourcemeta::core::JSON &schema, - sourcemeta::core::JSON &rows, sourcemeta::core::JSON &children, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, VisitedSchemas &visited, - std::size_t &next_identifier) -> void { - if (!schema.is_object() || !schema.defines("allOf") || - !schema.at("allOf").is_array()) { - return; - } - - const auto &all_of{schema.at("allOf").as_array()}; - - if (all_of.size() == 1) { - auto branch{walk_schema(schema.at("allOf").at(0), false, frame, root, - visited, next_identifier)}; - const auto &branch_rows{branch.at("rows")}; - const auto branch_has_recursive_ref{has_recursive_ref_in_rows(branch_rows)}; - - if (!branch_has_recursive_ref) { - // Merge the branch's root row info into the parent's root row - if (!rows.empty() && !branch_rows.empty()) { - const auto &parent_last_type{rows.at(rows.size() - 1).at("type")}; - const auto &branch_first{branch_rows.at(0)}; - const auto parent_is_any{parent_last_type.defines("kind") && - parent_last_type.at("kind").to_string() == - "any"}; - const auto &branch_first_path{branch_first.at("path")}; - const auto branch_first_is_synthetic_root{ - !branch_first_path.empty() && - branch_first_path.at(0).at("type").to_string() == "synthetic" && - branch_first_path.at(0).at("value").to_string() == "root"}; - - if (parent_is_any && branch_first_is_synthetic_root) { - // Copy fields from branch root into parent root - auto &parent_root{rows.at(rows.size() - 1)}; - parent_root.assign("type", branch_first.at("type")); - - if (branch_first.defines("constraints")) { - parent_root.assign("constraints", branch_first.at("constraints")); - } else if (parent_root.defines("constraints")) { - parent_root.erase("constraints"); - } - - if (branch_first.defines("title") && !parent_root.defines("title")) { - parent_root.assign("title", branch_first.at("title")); - } - if (branch_first.defines("description") && - !parent_root.defines("description")) { - parent_root.assign("description", branch_first.at("description")); - } - if (branch_first.defines("default") && - !parent_root.defines("default")) { - parent_root.assign("default", branch_first.at("default")); - } - - if (branch_first.defines("modifiers")) { - parent_root.assign("modifiers", branch_first.at("modifiers")); - } else if (parent_root.defines("modifiers")) { - parent_root.erase("modifiers"); - } - - if (branch_first.defines("badges")) { - parent_root.assign("badges", branch_first.at("badges")); - } else if (parent_root.defines("badges")) { - parent_root.erase("badges"); - } - - for (std::size_t index = 1; index < branch_rows.size(); ++index) { - rows.push_back(branch_rows.at(index)); - } - } else { - for (const auto &row : branch_rows.as_array()) { - rows.push_back(row); - } - } - } else { - for (const auto &row : branch_rows.as_array()) { - rows.push_back(row); - } - } - - if (branch.defines("children")) { - for (const auto &child : branch.at("children").as_array()) { - children.push_back(child); - } - } - return; - } - - auto section_children{sourcemeta::core::JSON::make_array()}; - section_children.push_back(std::move(branch)); - children.push_back(make_section("All of", std::move(section_children))); - return; - } - - auto section_children{sourcemeta::core::JSON::make_array()}; - for (const auto &branch : all_of) { - section_children.push_back( - walk_schema(branch, false, frame, root, visited, next_identifier)); - } - children.push_back(make_section("All of", std::move(section_children))); -} - -auto walk_if_then_else(const sourcemeta::core::JSON &schema, - sourcemeta::core::JSON &children, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, - VisitedSchemas &visited, std::size_t &next_identifier) - -> void { - if (!schema.is_object() || !schema.defines("if") || !schema.defines("then") || - !schema.defines("else")) { - return; - } - - { - auto section_children{sourcemeta::core::JSON::make_array()}; - section_children.push_back(walk_schema(schema.at("if"), false, frame, root, - visited, next_identifier)); - children.push_back(make_section("If", std::move(section_children))); - } - - { - auto section_children{sourcemeta::core::JSON::make_array()}; - section_children.push_back(walk_schema(schema.at("then"), false, frame, - root, visited, next_identifier)); - children.push_back(make_section("Then", std::move(section_children))); - } - - { - auto section_children{sourcemeta::core::JSON::make_array()}; - section_children.push_back(walk_schema(schema.at("else"), false, frame, - root, visited, next_identifier)); - children.push_back(make_section("Else", std::move(section_children))); - } -} - -auto walk_branching_subschema(const std::string &label, - const std::string &synthetic_name, - const sourcemeta::core::JSON &inner_schema, - sourcemeta::core::JSON &doc_children, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, - VisitedSchemas &visited, - std::size_t &next_identifier, - const bool include_properties) -> void { - auto table{sourcemeta::core::JSON::make_object()}; - table.assign("identifier", sourcemeta::core::JSON{ - static_cast(next_identifier++)}); - auto table_rows{sourcemeta::core::JSON::make_array()}; - auto table_children{sourcemeta::core::JSON::make_array()}; - auto synthetic_path{sourcemeta::core::JSON::make_array()}; - synthetic_path.push_back(make_path_segment("synthetic", synthetic_name)); - if (include_properties) { - walk_properties(inner_schema, synthetic_path, table_rows, frame, root, - visited, next_identifier); - } - emit_row(inner_schema, std::move(synthetic_path), table_rows, frame, root, - visited, next_identifier, false); - walk_branches("anyOf", "Any of", inner_schema, table_children, frame, root, - visited, next_identifier); - walk_branches("oneOf", "One of", inner_schema, table_children, frame, root, - visited, next_identifier); - walk_all_of(inner_schema, table_rows, table_children, frame, root, visited, - next_identifier); - table.assign("rows", std::move(table_rows)); - if (!table_children.empty()) { - table.assign("children", std::move(table_children)); - } - auto section_children{sourcemeta::core::JSON::make_array()}; - section_children.push_back(std::move(table)); - doc_children.push_back(make_section(label, std::move(section_children))); -} - -auto walk_schema(const sourcemeta::core::JSON &schema, const bool include_root, - const sourcemeta::blaze::SchemaFrame &frame, - const sourcemeta::core::JSON &root, VisitedSchemas &visited, - std::size_t &next_identifier) -> sourcemeta::core::JSON { - if (schema.is_object() && schema.defines("$ref") && - schema.at("$ref").is_string()) { - const auto target{ - resolve_destination(schema.at("$ref").to_string(), frame)}; - if (target.has_value()) { - const auto &target_schema{ - sourcemeta::core::get(root, target->get().pointer)}; - const auto visited_entry{visited.find(&target_schema)}; - if (visited_entry != visited.end()) { - auto documentation{sourcemeta::core::JSON::make_object()}; - documentation.assign("identifier", - sourcemeta::core::JSON{ - static_cast(next_identifier++)}); - auto rows{sourcemeta::core::JSON::make_array()}; - auto row{sourcemeta::core::JSON::make_object()}; - row.assign("identifier", - sourcemeta::core::JSON{ - static_cast(next_identifier++)}); - auto path{sourcemeta::core::JSON::make_array()}; - path.push_back(make_path_segment("synthetic", "root")); - row.assign("path", std::move(path)); - auto type_expr{sourcemeta::core::JSON::make_object()}; - type_expr.assign("kind", sourcemeta::core::JSON{"recursiveRef"}); - type_expr.assign("identifier", - sourcemeta::core::JSON{static_cast( - visited_entry->second.identifier)}); - type_expr.assign("path", visited_entry->second.path); - row.assign("type", std::move(type_expr)); - rows.push_back(std::move(row)); - documentation.assign("rows", std::move(rows)); - return documentation; - } - auto ref_path{sourcemeta::core::JSON::make_array()}; - ref_path.push_back(make_path_segment("synthetic", "root")); - visited.emplace(&target_schema, - VisitedEntry{.identifier = next_identifier, - .path = std::move(ref_path)}); - auto result{walk_schema(target_schema, include_root, frame, root, visited, - next_identifier)}; - visited.erase(&target_schema); - return result; - } - } - - auto documentation{sourcemeta::core::JSON::make_object()}; - const auto doc_identifier{next_identifier++}; - documentation.assign( - "identifier", - sourcemeta::core::JSON{static_cast(doc_identifier)}); - - if (schema.is_object() && schema.defines("$dynamicAnchor") && - schema.at("$dynamicAnchor").is_string()) { - documentation.assign( - "dynamicAnchor", - sourcemeta::core::JSON{schema.at("$dynamicAnchor").to_string()}); - } - - auto rows{sourcemeta::core::JSON::make_array()}; - auto doc_children{sourcemeta::core::JSON::make_array()}; - - if (include_root) { - auto root_path{sourcemeta::core::JSON::make_array()}; - root_path.push_back(make_path_segment("synthetic", "root")); - emit_row(schema, std::move(root_path), rows, frame, root, visited, - next_identifier, false); - const auto root_row_identifier{static_cast( - rows.at(rows.size() - 1).at("identifier").to_integer())}; - auto visited_root_path{sourcemeta::core::JSON::make_array()}; - visited_root_path.push_back(make_path_segment("synthetic", "root")); - visited.emplace(&schema, - VisitedEntry{.identifier = root_row_identifier, - .path = std::move(visited_root_path)}); - } - - if (!schema.is_object()) { - if (!include_root) { - auto root_path{sourcemeta::core::JSON::make_array()}; - root_path.push_back(make_path_segment("synthetic", "root")); - emit_row(schema, std::move(root_path), rows, frame, root, visited, - next_identifier, false); - } - documentation.assign("rows", std::move(rows)); - return documentation; - } - - if (!include_root) { - auto root_path{sourcemeta::core::JSON::make_array()}; - root_path.push_back(make_path_segment("synthetic", "root")); - emit_row(schema, std::move(root_path), rows, frame, root, visited, - next_identifier, false); - } - - const auto empty_path{sourcemeta::core::JSON::make_array()}; - walk_properties(schema, empty_path, rows, frame, root, visited, - next_identifier); - walk_pattern_properties(schema, empty_path, rows, frame, root, visited, - next_identifier); - walk_wildcard_keyword(schema, "additionalProperties", empty_path, rows, frame, - root, visited, next_identifier); - if (schema.defines("type") && schema.at("type").is_string() && - schema.at("type").to_string() == "object" && - !schema.defines("additionalProperties") && - !schema.defines("unevaluatedProperties")) { - auto open_path{empty_path}; - open_path.push_back(make_path_segment("wildcard", "*")); - emit_row(sourcemeta::core::JSON{true}, std::move(open_path), rows, frame, - root, visited, next_identifier); - } - walk_prefix_items(schema, empty_path, rows, doc_children, frame, root, - visited, next_identifier); - - if (schema.is_object() && schema.defines("items") && - schema.at("items").is_object() && !schema.defines("prefixItems")) { - const auto &items_schema{ - resolve_ref(schema.at("items"), frame, root, visited)}; - if (items_schema.is_object()) { - auto wildcard_path{sourcemeta::core::JSON::make_array()}; - wildcard_path.push_back(make_path_segment("wildcard", "*")); - const auto items_row_id{next_identifier}; - emit_row(items_schema, wildcard_path, rows, frame, root, visited, - next_identifier); - if (items_schema.defines("type") && items_schema.at("type").is_string() && - items_schema.at("type").to_string() == "object") { - visited.emplace(&items_schema, VisitedEntry{.identifier = items_row_id, - .path = wildcard_path}); - walk_properties(items_schema, wildcard_path, rows, frame, root, visited, - next_identifier); - walk_pattern_properties(items_schema, wildcard_path, rows, frame, root, - visited, next_identifier); - walk_wildcard_keyword(items_schema, "additionalProperties", - wildcard_path, rows, frame, root, visited, - next_identifier); - walk_wildcard_keyword(items_schema, "unevaluatedProperties", - wildcard_path, rows, frame, root, visited, - next_identifier); - if (!items_schema.defines("additionalProperties") && - !items_schema.defines("unevaluatedProperties")) { - auto open_path{wildcard_path}; - open_path.push_back(make_path_segment("wildcard", "*")); - emit_row(sourcemeta::core::JSON{true}, std::move(open_path), rows, - frame, root, visited, next_identifier); - } - visited.erase(&items_schema); - } - } - } - - walk_branches("anyOf", "Any of", schema, doc_children, frame, root, visited, - next_identifier); - walk_branches("oneOf", "One of", schema, doc_children, frame, root, visited, - next_identifier); - walk_all_of(schema, rows, doc_children, frame, root, visited, - next_identifier); - walk_if_then_else(schema, doc_children, frame, root, visited, - next_identifier); - walk_wildcard_keyword(schema, "unevaluatedProperties", empty_path, rows, - frame, root, visited, next_identifier); - walk_wildcard_keyword(schema, "unevaluatedItems", empty_path, rows, frame, - root, visited, next_identifier); - - if (schema.is_object() && schema.defines("contains") && - schema.at("contains").is_object()) { - const auto &contains_schema{schema.at("contains")}; - const auto is_branching{ - contains_schema.defines("anyOf") || contains_schema.defines("oneOf") || - contains_schema.defines("allOf") || contains_schema.defines("not") || - contains_schema.defines("enum")}; - if (is_branching) { - walk_branching_subschema("Contains", "matching item", contains_schema, - doc_children, frame, root, visited, - next_identifier, false); - } - } - - if (schema.is_object() && schema.defines("contentSchema") && - schema.at("contentSchema").is_object()) { - const auto &content_schema{schema.at("contentSchema")}; - const auto is_branching{ - content_schema.defines("anyOf") || content_schema.defines("oneOf") || - content_schema.defines("allOf") || content_schema.defines("not")}; - if (is_branching) { - walk_branching_subschema("Decoded content", "decoded", content_schema, - doc_children, frame, root, visited, - next_identifier, true); - } - } - - if (schema.is_object() && schema.defines("propertyNames") && - schema.at("propertyNames").is_object()) { - const auto &names_schema{schema.at("propertyNames")}; - const auto is_branching{ - names_schema.defines("anyOf") || names_schema.defines("oneOf") || - names_schema.defines("allOf") || names_schema.defines("not")}; - if (is_branching) { - walk_branching_subschema("Property names", "key", names_schema, - doc_children, frame, root, visited, - next_identifier, false); - } - } - - if (schema.is_object() && schema.defines("not")) { - const auto ¬_schema{schema.at("not")}; - const auto is_branching{ - not_schema.is_object() && - (not_schema.defines("anyOf") || not_schema.defines("oneOf") || - not_schema.defines("allOf") || not_schema.defines("not"))}; - const auto has_inline_constraints{!is_branching && not_schema.is_object() && - !constraints_of(not_schema).empty()}; - if (!has_inline_constraints) { - walk_branching_subschema("Must NOT match", "value", not_schema, - doc_children, frame, root, visited, - next_identifier, false); - } - } - - assert(!rows.empty() || !doc_children.empty()); - - documentation.assign("rows", std::move(rows)); - if (!doc_children.empty()) { - documentation.assign("children", std::move(doc_children)); - } - - return documentation; -} - -} // namespace - -auto to_documentation(const sourcemeta::core::JSON &schema, - const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &resolver) - -> sourcemeta::core::JSON { - sourcemeta::blaze::SchemaTransformer canonicalizer; - sourcemeta::blaze::add(canonicalizer, - sourcemeta::blaze::AlterSchemaMode::Canonicalizer); - sourcemeta::core::JSON canonical{schema}; - [[maybe_unused]] const auto canonicalized{canonicalizer.apply( - canonical, walker, resolver, - [](const auto &, const auto, const auto, const auto &, - [[maybe_unused]] const auto applied) -> auto { assert(applied); })}; - assert(canonicalized.first); - - sourcemeta::blaze::SchemaFrame frame{ - sourcemeta::blaze::SchemaFrame::Mode::References}; - frame.analyse(canonical, walker, resolver); - - VisitedSchemas visited; - std::size_t next_identifier{0}; - return walk_schema(canonical, true, frame, canonical, visited, - next_identifier); -} - -} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/documentation/documentation_html.cc b/vendor/blaze/src/documentation/documentation_html.cc deleted file mode 100644 index 91a3bbb77..000000000 --- a/vendor/blaze/src/documentation/documentation_html.cc +++ /dev/null @@ -1,412 +0,0 @@ -#include - -#include -#include - -#include // assert -#include // std::set -#include // std::ostringstream -#include // std::string, std::to_string - -namespace sourcemeta::blaze { - -namespace { - -auto json_to_string(const sourcemeta::core::JSON &value) -> std::string { - std::ostringstream stream; - sourcemeta::core::stringify(value, stream); - return stream.str(); -} - -auto is_empty_row(const sourcemeta::core::JSON &row) -> bool { - assert(row.is_object()); - assert(row.defines("type")); - return row.at("type").defines("kind") && - row.at("type").at("kind").to_string() == "any" && - !row.defines("constraints") && !row.defines("badges") && - !row.defines("modifiers") && !row.defines("title") && - !row.defines("description") && !row.defines("default") && - !row.defines("examples"); -} - -auto collect_ref_targets(const sourcemeta::core::JSON &table, - std::set &targets) -> void { - for (const auto &row : table.at("rows").as_array()) { - if (row.defines("type") && row.at("type").defines("kind") && - row.at("type").at("kind").to_string() == "recursiveRef" && - row.at("type").defines("identifier")) { - targets.insert(row.at("type").at("identifier").to_integer()); - } - if (row.defines("children")) { - for (const auto §ion : row.at("children").as_array()) { - for (const auto &child : section.at("children").as_array()) { - collect_ref_targets(child, targets); - } - } - } - } - if (table.defines("children")) { - for (const auto §ion : table.at("children").as_array()) { - for (const auto &child : section.at("children").as_array()) { - collect_ref_targets(child, targets); - } - } - } -} - -auto render_path(sourcemeta::core::HTMLWriter &writer, - const sourcemeta::core::JSON &path) -> void { - assert(path.is_array()); - writer.code(); - bool first{true}; - for (const auto &segment : path.as_array()) { - assert(segment.defines("type")); - assert(segment.defines("value")); - const auto &type{segment.at("type").to_string()}; - const auto &value{segment.at("value").to_string()}; - - if (!first) { - writer.text("/"); - } - - if (type == "literal" || type == "pattern") { - writer.text(first ? "/" + value : value); - } else if (type == "wildcard") { - writer.text(first ? "/*" : "*"); - } else if (type == "synthetic") { - writer.em("(" + value + ")"); - } - - first = false; - } - - writer.close(); -} - -auto render_modifiers(sourcemeta::core::HTMLWriter &writer, - const sourcemeta::core::JSON &row) -> void { - if (!row.defines("modifiers")) { - return; - } - - for (const auto &modifier : row.at("modifiers").as_array()) { - writer.span(modifier.to_string()); - } -} - -auto render_enum_values(sourcemeta::core::HTMLWriter &writer, - const sourcemeta::core::JSON &values, - const bool leading_separator) -> void { - assert(values.is_array()); - bool first{true}; - for (const auto &value : values.as_array()) { - if (!first || leading_separator) { - writer.text(" | "); - } - - writer.code(json_to_string(value)); - first = false; - } -} - -auto render_type_expression(sourcemeta::core::HTMLWriter &writer, - const sourcemeta::core::JSON &type) -> void { - assert(type.is_object()); - assert(type.defines("kind")); - const auto &kind{type.at("kind").to_string()}; - - if (kind == "object") { - writer.text("Object"); - } else if (kind == "primitive") { - assert(type.defines("name")); - const auto &name{type.at("name").to_string()}; - if (name == "string") { - writer.text("String"); - } else if (name == "integer") { - writer.text("Integer"); - } else if (name == "number") { - writer.text("Number"); - } - } else if (kind == "array" || kind == "tuple") { - writer.text("Array"); - } else if (kind == "enum") { - assert(type.defines("values")); - render_enum_values(writer, type.at("values"), false); - if (type.defines("overflow")) { - writer.details(); - writer.summary("+ " + std::to_string(type.at("overflow").array_size()) + - " more"); - render_enum_values(writer, type.at("overflow"), true); - writer.close(); - } - } else if (kind == "externalRef") { - assert(type.defines("url")); - const auto &url{type.at("url").to_string()}; - writer.a().attribute("href", url); - writer.text(url); - writer.close(); - } else if (kind == "recursiveRef") { - assert(type.defines("identifier")); - const auto identifier{std::to_string(type.at("identifier").to_integer())}; - writer.a().attribute("data-index", identifier); - if (type.defines("path")) { - bool first_seg{true}; - for (const auto &segment : type.at("path").as_array()) { - const auto &seg_type{segment.at("type").to_string()}; - const auto &seg_value{segment.at("value").to_string()}; - if (!first_seg) { - writer.text("/"); - } - if (seg_type == "synthetic") { - writer.text("(" + seg_value + ")"); - } else if (seg_type == "literal" || seg_type == "pattern") { - writer.text(first_seg ? "/" + seg_value : seg_value); - } else if (seg_type == "wildcard") { - writer.text(first_seg ? "/*" : "*"); - } - first_seg = false; - } - writer.text(" #" + identifier); - } else { - writer.text(identifier); - } - writer.close(); - } else if (kind == "dynamicRef") { - assert(type.defines("anchor")); - writer.text("dynamic: " + type.at("anchor").to_string()); - } else if (kind == "any") { - writer.text("Any"); - } else if (kind == "never") { - writer.text("Never"); - } -} - -auto render_badges(sourcemeta::core::HTMLWriter &writer, - const sourcemeta::core::JSON &row) -> void { - if (!row.defines("badges")) { - return; - } - - for (const auto &badge : row.at("badges").as_array()) { - assert(badge.defines("kind")); - assert(badge.defines("value")); - const auto &kind{badge.at("kind").to_string()}; - const auto &value{badge.at("value").to_string()}; - if (kind == "format") { - writer.span(value); - } else if (kind == "encoding") { - writer.span("encoding: " + value); - } else if (kind == "mime") { - writer.span("mime: " + value); - } - } -} - -auto render_notes(sourcemeta::core::HTMLWriter &writer, - const sourcemeta::core::JSON &row) -> void { - if (row.defines("title")) { - writer.strong(row.at("title").to_string()); - } - - if (row.defines("description")) { - writer.p(row.at("description").to_string()); - } - - if (row.defines("default")) { - writer.span(); - writer.text("default: "); - writer.code(json_to_string(row.at("default"))); - writer.close(); - } -} - -auto render_row(sourcemeta::core::HTMLWriter &writer, - const sourcemeta::core::JSON &row, - const std::set &ref_targets) -> void; -auto render_section(sourcemeta::core::HTMLWriter &writer, - const sourcemeta::core::JSON §ion, - const std::set &ref_targets) -> void; -auto render_table(sourcemeta::core::HTMLWriter &writer, - const sourcemeta::core::JSON &table, - const std::set &ref_targets) -> void; - -auto emit_header(sourcemeta::core::HTMLWriter &writer) -> void { - writer.thead(); - writer.tr(); - writer.th("Path"); - writer.th("Type"); - writer.th("Required"); - writer.th("Constraints"); - writer.th("Notes"); - writer.close(); - writer.close(); -} - -auto render_row(sourcemeta::core::HTMLWriter &writer, - const sourcemeta::core::JSON &row, - const std::set &ref_targets) -> void { - assert(row.defines("identifier")); - assert(row.defines("path")); - assert(row.defines("type")); - - const auto identifier{row.at("identifier").to_integer()}; - writer.tr().attribute("data-index", std::to_string(identifier)); - - // Path - writer.td(); - render_path(writer, row.at("path")); - if (ref_targets.contains(identifier)) { - writer.text(" "); - writer.strong("#" + std::to_string(identifier)); - } - render_modifiers(writer, row); - writer.close(); - - // Type - writer.td(); - render_type_expression(writer, row.at("type")); - render_badges(writer, row); - writer.close(); - - // Required - writer.td(); - if (row.defines("required")) { - writer.text(row.at("required").to_boolean() ? "Yes" : "No"); - } - writer.close(); - - // Constraints - writer.td(); - if (row.defines("constraints")) { - for (const auto &constraint : row.at("constraints").as_array()) { - writer.span(constraint.to_string()); - } - } - writer.close(); - - // Notes - writer.td(); - render_notes(writer, row); - writer.close(); - - writer.close(); - - if (row.defines("children")) { - for (const auto §ion : row.at("children").as_array()) { - render_section(writer, section, ref_targets); - } - } -} - -auto render_section(sourcemeta::core::HTMLWriter &writer, - const sourcemeta::core::JSON §ion, - const std::set &ref_targets) -> void { - assert(section.defines("label")); - assert(section.defines("children")); - - writer.tr(); - writer.td().attribute("colspan", "5"); - writer.div(); - - writer.div(); - writer.text(section.at("label").to_string()); - if (section.defines("position")) { - writer.text(" "); - writer.code(section.at("position").to_string()); - } - writer.close(); - - for (const auto &child : section.at("children").as_array()) { - writer.div(); - if (child.defines("title")) { - writer.div(child.at("title").to_string()); - } - - render_table(writer, child, ref_targets); - writer.close(); - } - - writer.close(); - writer.close(); - writer.close(); -} - -auto render_table(sourcemeta::core::HTMLWriter &writer, - const sourcemeta::core::JSON &table, - const std::set &ref_targets) -> void { - assert(table.defines("identifier")); - assert(table.defines("rows")); - - writer.table().attribute("data-index", - std::to_string(table.at("identifier").to_integer())); - - const auto &rows{table.at("rows")}; - const auto has_children{table.defines("children")}; - const auto root_is_ref_target{ - !rows.empty() && rows.at(0).defines("identifier") && - ref_targets.contains(rows.at(0).at("identifier").to_integer())}; - const auto skip_root{has_children && !rows.empty() && - is_empty_row(rows.at(0)) && !root_is_ref_target}; - - if (!skip_root || rows.array_size() > 1) { - emit_header(writer); - } - - writer.tbody(); - for (std::size_t index = skip_root ? 1 : 0; index < rows.array_size(); - ++index) { - render_row(writer, rows.at(index), ref_targets); - } - - if (has_children) { - for (const auto §ion : table.at("children").as_array()) { - render_section(writer, section, ref_targets); - } - } - - writer.close(); - writer.close(); -} - -} // namespace - -auto to_html(const sourcemeta::core::JSON &documentation) -> std::string { - assert(documentation.is_object()); - assert(documentation.defines("rows")); - - std::set ref_targets; - collect_ref_targets(documentation, ref_targets); - - sourcemeta::core::HTMLWriter writer; - writer.table().attribute("class", "sourcemeta-blaze-documentation"); - - const auto &rows{documentation.at("rows")}; - const auto has_children{documentation.defines("children")}; - const auto root_is_ref_target{ - !rows.empty() && rows.at(0).defines("identifier") && - ref_targets.contains(rows.at(0).at("identifier").to_integer())}; - const auto skip_root{has_children && !rows.empty() && - is_empty_row(rows.at(0)) && !root_is_ref_target}; - - if (!skip_root || rows.array_size() > 1) { - emit_header(writer); - } - - writer.tbody(); - for (std::size_t index = skip_root ? 1 : 0; index < rows.array_size(); - ++index) { - render_row(writer, rows.at(index), ref_targets); - } - - if (has_children) { - for (const auto §ion : documentation.at("children").as_array()) { - render_section(writer, section, ref_targets); - } - } - - writer.close(); - writer.close(); - return writer.str(); -} - -} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/documentation/include/sourcemeta/blaze/documentation.h b/vendor/blaze/src/documentation/include/sourcemeta/blaze/documentation.h deleted file mode 100644 index 90d60f16a..000000000 --- a/vendor/blaze/src/documentation/include/sourcemeta/blaze/documentation.h +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef SOURCEMETA_BLAZE_DOCUMENTATION_H_ -#define SOURCEMETA_BLAZE_DOCUMENTATION_H_ - -/// @defgroup documentation Documentation -/// @brief Generate human-readable documentation from a JSON Schema -/// -/// This functionality is included as follows: -/// -/// ```cpp -/// #include -/// ``` - -#ifndef SOURCEMETA_BLAZE_DOCUMENTATION_EXPORT -#include -#endif - -#include -#include - -#include // std::string - -namespace sourcemeta::blaze { - -/// @ingroup documentation -/// -/// Generate documentation JSON from an input JSON Schema. For example: -/// -/// ```cpp -/// #include -/// -/// #include -/// #include -/// -/// const sourcemeta::core::JSON schema = -/// sourcemeta::core::parse_json(R"JSON({ -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "type": "string" -/// })JSON"); -/// -/// const auto documentation{sourcemeta::blaze::to_documentation( -/// schema, sourcemeta::blaze::schema_walker, -/// sourcemeta::blaze::schema_resolver)}; -/// ``` -[[nodiscard]] SOURCEMETA_BLAZE_DOCUMENTATION_EXPORT auto -to_documentation(const sourcemeta::core::JSON &schema, - const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &resolver) - -> sourcemeta::core::JSON; - -/// @ingroup documentation -/// Render a documentation JSON object as an HTML string -[[nodiscard]] SOURCEMETA_BLAZE_DOCUMENTATION_EXPORT auto -to_html(const sourcemeta::core::JSON &documentation) -> std::string; - -} // namespace sourcemeta::blaze - -#endif diff --git a/vendor/blaze/src/evaluator/evaluator_describe.cc b/vendor/blaze/src/evaluator/evaluator_describe.cc index cd108e419..615462918 100644 --- a/vendor/blaze/src/evaluator/evaluator_describe.cc +++ b/vendor/blaze/src/evaluator/evaluator_describe.cc @@ -385,6 +385,16 @@ auto describe(const bool valid, const Instruction &step, return describe_reference(target); } + // For the wrapper instruction we emit when hoisting `then` subschemas + // whose `if` condition compiles to nothing + if (keyword == "then") { + std::ostringstream message; + message << "The " << type_name(target.type()) + << " value was expected to validate against the given " + "conditional"; + return message.str(); + } + return unknown(); } diff --git a/vendor/blaze/src/foundation/foundation.cc b/vendor/blaze/src/foundation/foundation.cc index 589c875f1..4371c415a 100644 --- a/vendor/blaze/src/foundation/foundation.cc +++ b/vendor/blaze/src/foundation/foundation.cc @@ -2,14 +2,9 @@ #include "helpers.h" -#include // std::max, std::ranges::fold_left #include // assert -#include // std::uint64_t -#include // std::numeric_limits #include // std::ostringstream #include // std::string_view -#include // std::remove_reference_t -#include // std::unordered_map #include // std::unordered_set #include // std::move, std::to_underlying @@ -802,30 +797,6 @@ auto sourcemeta::blaze::vocabularies(const SchemaResolver &resolver, return result; } -auto sourcemeta::blaze::schema_keyword_priority( - std::string_view keyword, - const sourcemeta::blaze::Vocabularies &vocabularies, - const sourcemeta::blaze::SchemaWalker &walker) -> std::uint64_t { - const auto &result{walker(keyword, vocabularies)}; - const auto priority_from_dependencies{std::ranges::fold_left( - result.dependencies, static_cast(0), - [&vocabularies, &walker](const auto accumulator, - const auto &dependency) -> std::uint64_t { - return std::max( - accumulator, - schema_keyword_priority(dependency, vocabularies, walker) + 1); - })}; - const auto priority_from_order_dependencies{std::ranges::fold_left( - result.order_dependencies, static_cast(0), - [&vocabularies, &walker](const auto accumulator, - const auto &dependency) -> std::uint64_t { - return std::max( - accumulator, - schema_keyword_priority(dependency, vocabularies, walker) + 1); - })}; - return std::max(priority_from_dependencies, priority_from_order_dependencies); -} - static auto parse_schema_type_string(const sourcemeta::core::JSON::String &type, sourcemeta::core::JSON::TypeSet &result) -> void { diff --git a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h index ee3945167..49ef8d62b 100644 --- a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h +++ b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h @@ -14,11 +14,7 @@ #include // NOLINTEND(misc-include-cleaner) -#include // std::uint8_t -#include // std::function #include // std::optional, std::nullopt -#include // std::set -#include // std::string #include // std::string_view /// @defgroup foundation Foundation @@ -67,46 +63,6 @@ SOURCEMETA_BLAZE_FOUNDATION_EXPORT auto to_base_dialect(const std::string_view base_dialect) -> std::optional; -/// @ingroup foundation -/// -/// Calculate the priority of a keyword that determines the ordering in which a -/// JSON Schema implementation should evaluate keyword on a subschema. It does -/// so based on the keyword dependencies expressed in the schema walker. The -/// higher the priority, the more the evaluation of such keyword must be -/// delayed. -/// -/// For example: -/// -/// ```cpp -/// #include -/// #include -/// #include -/// -/// const sourcemeta::core::JSON document = -/// sourcemeta::core::parse_json(R"JSON({ -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "prefixItems": [ true, true ], -/// "items": false -/// })JSON"); -/// -/// const auto vocabularies{ -/// sourcemeta::blaze::vocabularies( -/// document, sourcemeta::blaze::schema_resolver)}; -/// -/// assert(sourcemeta::blaze::schema_keyword_priority( -/// "prefixItems", vocabularies, -/// sourcemeta::blaze::schema_walker) == 0); -/// -/// // The "items" keyword must be evaluated after the "prefixItems" keyword -/// assert(sourcemeta::blaze::schema_keyword_priority( -/// "items", vocabularies, -/// sourcemeta::blaze::schema_walker) == 1); -/// ``` -SOURCEMETA_BLAZE_FOUNDATION_EXPORT -auto schema_keyword_priority(const std::string_view keyword, - const Vocabularies &vocabularies, - const SchemaWalker &walker) -> std::uint64_t; - /// @ingroup foundation /// /// This function returns true if the given JSON instance is of a diff --git a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_error.h b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_error.h index d9a20ad31..882989425 100644 --- a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_error.h +++ b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_error.h @@ -171,26 +171,6 @@ class SOURCEMETA_BLAZE_FOUNDATION_EXPORT SchemaReferenceObjectResourceError std::string identifier_; }; -/// @ingroup foundation -/// An error that represents an unrecognized base dialect -class SOURCEMETA_BLAZE_FOUNDATION_EXPORT SchemaBaseDialectError - : public std::exception { -public: - SchemaBaseDialectError(const std::string_view base_dialect) - : base_dialect_{base_dialect} {} - - [[nodiscard]] auto what() const noexcept -> const char * override { - return "Unrecognized base dialect"; - } - - [[nodiscard]] auto base_dialect() const noexcept -> std::string_view { - return this->base_dialect_; - } - -private: - std::string base_dialect_; -}; - /// @ingroup foundation /// An error that represents a schema keyword error class SOURCEMETA_BLAZE_FOUNDATION_EXPORT SchemaKeywordError diff --git a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_walker.h b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_walker.h index 9c6daa996..c786af65d 100644 --- a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_walker.h +++ b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_walker.h @@ -9,8 +9,6 @@ #include -#include // std::uint64_t -#include // std::optional #include // std::string_view #include // std::vector @@ -81,72 +79,6 @@ class SOURCEMETA_BLAZE_FOUNDATION_EXPORT SchemaIterator { #endif }; -/// @ingroup foundation -/// -/// Return an iterator over the subschemas of a given JSON Schema definition -/// according to the applicators understood by the provided walker function. -/// This walker traverse over the first-level of subschemas of the JSON Schema -/// definition, ignoring the top-level schema and reporting back each subschema. -/// -/// Note that we don't promise any specific walking ordering. -/// -/// For example: -/// -/// ```cpp -/// #include -/// #include -/// #include -/// -/// const sourcemeta::core::JSON document = -/// sourcemeta::core::parse_json(R"JSON({ -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "type": "object", -/// "properties": { -/// "foo": { -/// "type": "array", -/// "items": { -/// "type": "string" -/// } -/// } -/// } -/// })JSON"); -/// -/// for (const auto &entry : -/// sourcemeta::blaze::SchemaIteratorFlat{ -/// document, sourcemeta::blaze::schema_walker, -/// sourcemeta::blaze::schema_resolver}) { -/// sourcemeta::core::prettify( -/// sourcemeta::core::get(document, entry.pointer), std::cout); -/// std::cout << "\n"; -/// } -/// ``` -class SOURCEMETA_BLAZE_FOUNDATION_EXPORT SchemaIteratorFlat { -private: - using internal = typename std::vector; - -public: - using const_iterator = typename internal::const_iterator; - SchemaIteratorFlat(const sourcemeta::core::JSON &input, - const SchemaWalker &walker, const SchemaResolver &resolver, - std::string_view default_dialect = ""); - [[nodiscard]] auto begin() const -> const_iterator; - [[nodiscard]] auto end() const -> const_iterator; - [[nodiscard]] auto cbegin() const -> const_iterator; - [[nodiscard]] auto cend() const -> const_iterator; - -private: -// Exporting symbols that depends on the standard C++ library is considered -// safe. -// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN -#if defined(_MSC_VER) -#pragma warning(disable : 4251) -#endif - internal subschemas{}; -#if defined(_MSC_VER) -#pragma warning(default : 4251) -#endif -}; - /// @ingroup foundation /// /// Return an iterator over the top-level keywords of a given JSON Schema diff --git a/vendor/blaze/src/foundation/walker.cc b/vendor/blaze/src/foundation/walker.cc index 869435db0..103306f1a 100644 --- a/vendor/blaze/src/foundation/walker.cc +++ b/vendor/blaze/src/foundation/walker.cc @@ -2,11 +2,34 @@ #include "helpers.h" -#include // std::max, std::sort +#include // std::max, std::ranges::fold_left, std::ranges::sort #include // assert +#include // std::uint64_t namespace { -enum class SchemaWalkerType_t : std::uint8_t { Deep, Flat }; +auto schema_keyword_priority( + const std::string_view keyword, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaWalker &walker) -> std::uint64_t { + const auto &result{walker(keyword, vocabularies)}; + const auto priority_from_dependencies{std::ranges::fold_left( + result.dependencies, static_cast(0), + [&vocabularies, &walker](const auto accumulator, + const auto &dependency) -> std::uint64_t { + return std::max( + accumulator, + schema_keyword_priority(dependency, vocabularies, walker) + 1); + })}; + const auto priority_from_order_dependencies{std::ranges::fold_left( + result.order_dependencies, static_cast(0), + [&vocabularies, &walker](const auto accumulator, + const auto &dependency) -> std::uint64_t { + return std::max( + accumulator, + schema_keyword_priority(dependency, vocabularies, walker) + 1); + })}; + return std::max(priority_from_dependencies, priority_from_order_dependencies); +} struct DialectInfo { std::string_view dialect; @@ -56,8 +79,8 @@ auto walk(const std::optional &parent, const sourcemeta::blaze::SchemaResolver &resolver, const std::string_view dialect, const sourcemeta::blaze::SchemaBaseDialect base_dialect, - const SchemaWalkerType_t type, const std::size_t level, - const bool orphan, const bool property_name) -> void { + const std::size_t level, const bool orphan, const bool property_name) + -> void { if (!sourcemeta::blaze::is_schema(subschema)) { return; } @@ -103,22 +126,19 @@ auto walk(const std::optional &parent, }, current_base_dialect, current_dialect)}; - if (type == SchemaWalkerType_t::Deep || level > 0) { - sourcemeta::blaze::SchemaIteratorEntry iterator_entry{ - .parent = parent, - .pointer = pointer, - .dialect = current_dialect, - .vocabularies = vocabularies, - .base_dialect = current_base_dialect, - .subschema = subschema, - .orphan = orphan, - .property_name = property_name}; - subschemas.push_back(std::move(iterator_entry)); - } + sourcemeta::blaze::SchemaIteratorEntry iterator_entry{ + .parent = parent, + .pointer = pointer, + .dialect = current_dialect, + .vocabularies = vocabularies, + .base_dialect = current_base_dialect, + .subschema = subschema, + .orphan = orphan, + .property_name = property_name}; + subschemas.push_back(std::move(iterator_entry)); // We can't recurse any further - if (!subschema.is_object() || - (type == SchemaWalkerType_t::Flat && level > 0)) { + if (!subschema.is_object()) { return; } @@ -150,7 +170,7 @@ auto walk(const std::optional &parent, sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - child_dialect, child_base_dialect, type, level + 1, orphan, false); + child_dialect, child_base_dialect, level + 1, orphan, false); } break; case sourcemeta::blaze::SchemaKeywordType:: @@ -158,7 +178,7 @@ auto walk(const std::optional &parent, sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - child_dialect, child_base_dialect, type, level + 1, orphan, true); + child_dialect, child_base_dialect, level + 1, orphan, true); } break; case sourcemeta::blaze::SchemaKeywordType:: @@ -166,7 +186,7 @@ auto walk(const std::optional &parent, sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - child_dialect, child_base_dialect, type, level + 1, orphan, false); + child_dialect, child_base_dialect, level + 1, orphan, false); } break; case sourcemeta::blaze::SchemaKeywordType:: @@ -174,7 +194,7 @@ auto walk(const std::optional &parent, sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - child_dialect, child_base_dialect, type, level + 1, orphan, false); + child_dialect, child_base_dialect, level + 1, orphan, false); } break; case sourcemeta::blaze::SchemaKeywordType:: @@ -182,14 +202,14 @@ auto walk(const std::optional &parent, sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - child_dialect, child_base_dialect, type, level + 1, orphan, false); + child_dialect, child_base_dialect, level + 1, orphan, false); } break; case sourcemeta::blaze::SchemaKeywordType::ApplicatorValueInPlaceOther: { sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - child_dialect, child_base_dialect, type, level + 1, orphan, + child_dialect, child_base_dialect, level + 1, orphan, property_name); } break; @@ -197,7 +217,7 @@ auto walk(const std::optional &parent, sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - child_dialect, child_base_dialect, type, level + 1, orphan, + child_dialect, child_base_dialect, level + 1, orphan, property_name); } break; @@ -205,7 +225,7 @@ auto walk(const std::optional &parent, sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - child_dialect, child_base_dialect, type, level + 1, orphan, + child_dialect, child_base_dialect, level + 1, orphan, property_name); } break; @@ -216,8 +236,8 @@ auto walk(const std::optional &parent, new_pointer.push_back(std::cref(pair.first)); new_pointer.emplace_back(index); walk(pointer, new_pointer, subschemas, pair.second.at(index), - walker, resolver, child_dialect, child_base_dialect, type, - level + 1, orphan, false); + walker, resolver, child_dialect, child_base_dialect, level + 1, + orphan, false); } } @@ -230,8 +250,8 @@ auto walk(const std::optional &parent, new_pointer.push_back(std::cref(pair.first)); new_pointer.emplace_back(index); walk(pointer, new_pointer, subschemas, pair.second.at(index), - walker, resolver, child_dialect, child_base_dialect, type, - level + 1, orphan, property_name); + walker, resolver, child_dialect, child_base_dialect, level + 1, + orphan, property_name); } } @@ -244,8 +264,8 @@ auto walk(const std::optional &parent, new_pointer.push_back(std::cref(pair.first)); new_pointer.emplace_back(index); walk(pointer, new_pointer, subschemas, pair.second.at(index), - walker, resolver, child_dialect, child_base_dialect, type, - level + 1, orphan, property_name); + walker, resolver, child_dialect, child_base_dialect, level + 1, + orphan, property_name); } } @@ -259,8 +279,8 @@ auto walk(const std::optional &parent, new_pointer.push_back(std::cref(pair.first)); new_pointer.emplace_back(index); walk(pointer, new_pointer, subschemas, pair.second.at(index), - walker, resolver, child_dialect, child_base_dialect, type, - level + 1, orphan, property_name); + walker, resolver, child_dialect, child_base_dialect, level + 1, + orphan, property_name); } } @@ -274,8 +294,8 @@ auto walk(const std::optional &parent, new_pointer.push_back(std::cref(pair.first)); new_pointer.push_back(std::cref(subpair.first)); walk(pointer, new_pointer, subschemas, subpair.second, walker, - resolver, child_dialect, child_base_dialect, type, level + 1, - orphan, false); + resolver, child_dialect, child_base_dialect, level + 1, orphan, + false); } } @@ -289,8 +309,8 @@ auto walk(const std::optional &parent, new_pointer.push_back(std::cref(pair.first)); new_pointer.push_back(std::cref(subpair.first)); walk(pointer, new_pointer, subschemas, subpair.second, walker, - resolver, child_dialect, child_base_dialect, type, level + 1, - orphan, false); + resolver, child_dialect, child_base_dialect, level + 1, orphan, + false); } } @@ -303,8 +323,8 @@ auto walk(const std::optional &parent, new_pointer.push_back(std::cref(pair.first)); new_pointer.push_back(std::cref(subpair.first)); walk(pointer, new_pointer, subschemas, subpair.second, walker, - resolver, child_dialect, child_base_dialect, type, level + 1, - orphan, property_name); + resolver, child_dialect, child_base_dialect, level + 1, orphan, + property_name); } } @@ -317,8 +337,8 @@ auto walk(const std::optional &parent, new_pointer.push_back(std::cref(pair.first)); new_pointer.push_back(std::cref(subpair.first)); walk(pointer, new_pointer, subschemas, subpair.second, walker, - resolver, child_dialect, child_base_dialect, type, level + 1, - true, false); + resolver, child_dialect, child_base_dialect, level + 1, true, + false); } } @@ -332,15 +352,14 @@ auto walk(const std::optional &parent, new_pointer.push_back(std::cref(pair.first)); new_pointer.emplace_back(index); walk(pointer, new_pointer, subschemas, pair.second.at(index), - walker, resolver, child_dialect, child_base_dialect, type, - level + 1, orphan, false); + walker, resolver, child_dialect, child_base_dialect, level + 1, + orphan, false); } } else { sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - child_dialect, child_base_dialect, type, level + 1, orphan, - false); + child_dialect, child_base_dialect, level + 1, orphan, false); } break; @@ -353,14 +372,14 @@ auto walk(const std::optional &parent, new_pointer.push_back(std::cref(pair.first)); new_pointer.emplace_back(index); walk(pointer, new_pointer, subschemas, pair.second.at(index), - walker, resolver, child_dialect, child_base_dialect, type, - level + 1, orphan, property_name); + walker, resolver, child_dialect, child_base_dialect, level + 1, + orphan, property_name); } } else { sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - child_dialect, child_base_dialect, type, level + 1, orphan, + child_dialect, child_base_dialect, level + 1, orphan, property_name); } @@ -407,26 +426,7 @@ sourcemeta::blaze::SchemaIterator::SchemaIterator( sourcemeta::blaze::base_dialect(schema, resolver, resolved_dialect)}; assert(resolved_base_dialect.has_value()); walk(std::nullopt, pointer, this->subschemas, schema, walker, resolver, - resolved_dialect, resolved_base_dialect.value(), - SchemaWalkerType_t::Deep, 0, false, false); - } -} - -sourcemeta::blaze::SchemaIteratorFlat::SchemaIteratorFlat( - const sourcemeta::core::JSON &schema, - const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &resolver, - const std::string_view default_dialect) { - const std::string_view resolved_dialect{ - sourcemeta::blaze::dialect(schema, default_dialect)}; - if (!resolved_dialect.empty()) { - sourcemeta::core::WeakPointer pointer; - const auto resolved_base_dialect{ - sourcemeta::blaze::base_dialect(schema, resolver, resolved_dialect)}; - assert(resolved_base_dialect.has_value()); - walk(std::nullopt, pointer, this->subschemas, schema, walker, resolver, - resolved_dialect, resolved_base_dialect.value(), - SchemaWalkerType_t::Flat, 0, false, false); + resolved_dialect, resolved_base_dialect.value(), 0, false, false); } } @@ -495,19 +495,6 @@ auto sourcemeta::blaze::SchemaIterator::cend() const -> const_iterator { return this->subschemas.cend(); } -auto sourcemeta::blaze::SchemaIteratorFlat::begin() const -> const_iterator { - return this->subschemas.begin(); -} -auto sourcemeta::blaze::SchemaIteratorFlat::end() const -> const_iterator { - return this->subschemas.end(); -} -auto sourcemeta::blaze::SchemaIteratorFlat::cbegin() const -> const_iterator { - return this->subschemas.cbegin(); -} -auto sourcemeta::blaze::SchemaIteratorFlat::cend() const -> const_iterator { - return this->subschemas.cend(); -} - auto sourcemeta::blaze::SchemaKeywordIterator::begin() const -> const_iterator { return this->entries.begin(); } diff --git a/vendor/blaze/src/output/CMakeLists.txt b/vendor/blaze/src/output/CMakeLists.txt index 486d47cf9..94513d8f6 100644 --- a/vendor/blaze/src/output/CMakeLists.txt +++ b/vendor/blaze/src/output/CMakeLists.txt @@ -1,10 +1,11 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME output FOLDER "Blaze/Output" - PRIVATE_HEADERS simple.h trace.h standard.h + PRIVATE_HEADERS simple.h trace.h standard.h jsonld.h SOURCES output_simple.cc output_trace.cc - output_standard.cc) + output_standard.cc + output_jsonld.cc) if(BLAZE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT blaze NAME output) @@ -14,6 +15,12 @@ target_link_libraries(sourcemeta_blaze_output PUBLIC sourcemeta::core::json) target_link_libraries(sourcemeta_blaze_output PUBLIC sourcemeta::core::jsonpointer) +target_link_libraries(sourcemeta_blaze_output PRIVATE + sourcemeta::core::jsonld) +target_link_libraries(sourcemeta_blaze_output PRIVATE + sourcemeta::core::langtag) +target_link_libraries(sourcemeta_blaze_output PRIVATE + sourcemeta::core::text) target_link_libraries(sourcemeta_blaze_output PUBLIC sourcemeta::blaze::foundation) target_link_libraries(sourcemeta_blaze_output PUBLIC diff --git a/vendor/blaze/src/output/include/sourcemeta/blaze/output.h b/vendor/blaze/src/output/include/sourcemeta/blaze/output.h index c0af589a8..fa8e7ace6 100644 --- a/vendor/blaze/src/output/include/sourcemeta/blaze/output.h +++ b/vendor/blaze/src/output/include/sourcemeta/blaze/output.h @@ -1,6 +1,7 @@ #ifndef SOURCEMETA_BLAZE_OUTPUT_H_ #define SOURCEMETA_BLAZE_OUTPUT_H_ +#include #include #include #include diff --git a/vendor/blaze/src/output/include/sourcemeta/blaze/output_jsonld.h b/vendor/blaze/src/output/include/sourcemeta/blaze/output_jsonld.h new file mode 100644 index 000000000..0340abc27 --- /dev/null +++ b/vendor/blaze/src/output/include/sourcemeta/blaze/output_jsonld.h @@ -0,0 +1,118 @@ +#ifndef SOURCEMETA_BLAZE_OUTPUT_JSONLD_H_ +#define SOURCEMETA_BLAZE_OUTPUT_JSONLD_H_ + +#ifndef SOURCEMETA_BLAZE_OUTPUT_EXPORT +#include +#endif + +#include +#include + +#include +#include + +#include // std::array +#include // std::uint8_t +#include // std::string +#include // std::variant +#include // std::vector + +namespace sourcemeta::blaze { + +/// @ingroup output +/// The x-jsonld-* keywords that jsonld() resolves. Use these as the annotation +/// whitelist when compiling a schema so that its annotations are collected. +inline constexpr std::array + JSONLD_KEYWORDS{{"x-jsonld-id", "x-jsonld-type", "x-jsonld-reverse", + "x-jsonld-datatype", "x-jsonld-language", + "x-jsonld-direction", "x-jsonld-json", "x-jsonld-graph", + "x-jsonld-container", "x-jsonld-self"}}; + +/// @ingroup output +/// The descriptor facet that a JSON-LD resolution error is about +enum class JSONLDFacet : std::uint8_t { + Type, + Predicate, + Datatype, + Language, + Direction, + Graph, + JSON, + Container, + Self +}; + +/// @ingroup output +/// The instance conforms but one of its JSON-LD annotations cannot be resolved +/// into a consistent mapping. Reported as a single failure at the instance +/// location where resolution stopped, distinct from a schema validation error. +struct JSONLDResolutionError { + sourcemeta::core::Pointer instance_location; + JSONLDFacet facet; + std::string message; +}; + +/// @ingroup output +/// The instance does not conform to the schema, so no JSON-LD is produced. +/// These are the schema validation errors. +using JSONLDInvalid = std::vector; + +/// @ingroup output +/// The tri-state outcome of a JSON-LD evaluation: the expanded document (as +/// expanded JSON-LD), schema validation errors, or a JSON-LD resolution error. +using JSONLDOutcome = + std::variant; + +/// @ingroup output +/// +/// Evaluate an instance against a JSON Schema whose atoms carry JSON-LD +/// annotations and, on success, return the instance promoted to expanded +/// JSON-LD. The schema must have been compiled with annotation collection +/// enabled for its JSON-LD keywords, as shown below. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// #include +/// #include +/// +/// #include +/// #include +/// +/// const sourcemeta::core::JSON schema = +/// sourcemeta::core::parse_json(R"JSON({ +/// "$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" } +/// } +/// })JSON"); +/// +/// sourcemeta::blaze::Tweaks tweaks; +/// tweaks.annotations = std::unordered_set( +/// sourcemeta::blaze::JSONLD_KEYWORDS.begin(), +/// sourcemeta::blaze::JSONLD_KEYWORDS.end()); +/// +/// const auto schema_template{sourcemeta::blaze::compile( +/// schema, sourcemeta::blaze::schema_walker, +/// sourcemeta::blaze::schema_resolver, +/// sourcemeta::blaze::default_schema_compiler, +/// sourcemeta::blaze::Mode::FastValidation, "", "", "", tweaks)}; +/// +/// const sourcemeta::core::JSON instance{ +/// sourcemeta::core::parse_json(R"JSON({ "name": "Ada" })JSON")}; +/// +/// sourcemeta::blaze::Evaluator evaluator; +/// const auto outcome{ +/// sourcemeta::blaze::jsonld(evaluator, schema_template, instance)}; +/// ``` +auto SOURCEMETA_BLAZE_OUTPUT_EXPORT +jsonld(Evaluator &evaluator, const Template &schema, + const sourcemeta::core::JSON &instance) -> JSONLDOutcome; + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/output/include/sourcemeta/blaze/output_simple.h b/vendor/blaze/src/output/include/sourcemeta/blaze/output_simple.h index 51d288142..f50e3c535 100644 --- a/vendor/blaze/src/output/include/sourcemeta/blaze/output_simple.h +++ b/vendor/blaze/src/output/include/sourcemeta/blaze/output_simple.h @@ -103,6 +103,17 @@ class SOURCEMETA_BLAZE_OUTPUT_EXPORT SimpleOutput { return this->annotations_; } + /// Move out the collected error entries, leaving this output empty. Useful to + /// take ownership of the trace without copying when the output is no longer + /// needed + [[nodiscard]] auto release() && -> container_type { + auto result{std::move(this->output)}; + // A moved-from container is left in a valid but unspecified state, so + // clear it to honor the documented empty postcondition portably + this->output.clear(); + return result; + } + // NOLINTNEXTLINE(bugprone-exception-escape) struct Location { auto operator<(const Location &other) const noexcept -> bool { diff --git a/vendor/blaze/src/output/output_jsonld.cc b/vendor/blaze/src/output/output_jsonld.cc new file mode 100644 index 000000000..320a74b21 --- /dev/null +++ b/vendor/blaze/src/output/output_jsonld.cc @@ -0,0 +1,780 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include // std::ranges::sort, std::ranges::any_of +#include // std::deque +#include // std::ref +#include // std::optional +#include // std::ostringstream +#include // std::string +#include // std::string_view +#include // std::tie, std::make_tuple +#include // std::unordered_map +#include // std::move +#include // std::variant, std::get, std::holds_alternative +#include // std::vector + +namespace { + +// The facts gathered at one instance location before its kind is known +struct Facts { + std::vector edges; + std::vector types; + std::optional datatype; + std::optional language; + std::optional direction; + std::optional container; + std::optional self; + bool json{false}; + bool graph{false}; +}; + +using Accumulator = std::unordered_map; + +auto fill_collection(sourcemeta::core::JSONLDWeakAnnotationMap &map, + const Accumulator &accumulator, + const sourcemeta::core::WeakPointer &pointer, + const sourcemeta::core::JSON &value) -> void; + +// Give an undescribed collection member a default kind so it still +// materializes, a scalar as a literal and a nested array as an inner collection +auto fill_element(sourcemeta::core::JSONLDWeakAnnotationMap &map, + const Accumulator &accumulator, + sourcemeta::core::WeakPointer element_pointer, + const sourcemeta::core::JSON &element) -> void { + if (accumulator.contains(element_pointer)) { + return; + } + + if (element.is_array()) { + map.emplace( + element_pointer, + sourcemeta::core::JSONLDDescriptor{ + .edges = {}, .value = sourcemeta::core::JSONLDCollection{}}); + fill_collection(map, accumulator, element_pointer, element); + } else if (!element.is_object()) { + map.emplace(std::move(element_pointer), + sourcemeta::core::JSONLDDescriptor{ + .edges = {}, .value = sourcemeta::core::JSONLDLiteral{}}); + } +} + +// Default the undescribed members of a collection, whether it ranges over an +// array or over an object +auto fill_collection(sourcemeta::core::JSONLDWeakAnnotationMap &map, + const Accumulator &accumulator, + const sourcemeta::core::WeakPointer &pointer, + const sourcemeta::core::JSON &value) -> void { + if (value.is_array()) { + for (std::size_t index = 0; index < value.size(); index += 1) { + auto element_pointer{pointer}; + element_pointer.push_back(index); + fill_element(map, accumulator, std::move(element_pointer), + value.at(index)); + } + } else { + for (const auto &entry : value.as_object()) { + auto element_pointer{pointer}; + element_pointer.push_back(std::cref(entry.first)); + fill_element(map, accumulator, std::move(element_pointer), entry.second); + } + } +} + +auto add_edge(std::vector &edges, + const sourcemeta::core::JSON::String &predicate, + const bool reverse) -> void { + const auto exists{std::ranges::any_of( + edges, + [&predicate, reverse](const sourcemeta::core::JSONLDEdge &edge) -> bool { + return edge.predicate == predicate && edge.reverse == reverse; + })}; + if (!exists) { + edges.push_back({.predicate = predicate, .reverse = reverse}); + } +} + +auto add_type(std::vector &types, + const sourcemeta::core::JSON::String &type) -> void { + const auto exists{std::ranges::any_of( + types, [&type](const sourcemeta::core::JSON::String &existing) -> bool { + return existing == type; + })}; + if (!exists) { + types.push_back(type); + } +} + +// Whether an annotation value is usable as an absolute IRI +auto is_iri_value(const sourcemeta::core::JSON &value) -> bool { + return value.is_string() && sourcemeta::core::URI::is_iri(value.to_string()); +} + +auto type_iri_error(const sourcemeta::core::WeakPointer &instance_location) + -> sourcemeta::blaze::JSONLDResolutionError { + return {.instance_location = sourcemeta::core::to_pointer(instance_location), + .facet = sourcemeta::blaze::JSONLDFacet::Type, + .message = "The value of x-jsonld-type must be an absolute IRI"}; +} + +auto facet_error(const sourcemeta::core::WeakPointer &instance_location, + const sourcemeta::blaze::JSONLDFacet facet, + std::string message) + -> sourcemeta::blaze::JSONLDResolutionError { + return {.instance_location = sourcemeta::core::to_pointer(instance_location), + .facet = facet, + .message = std::move(message)}; +} + +auto parse_direction(const sourcemeta::core::JSON &value) + -> std::optional { + if (!value.is_string()) { + return std::nullopt; + } + + const auto &text{value.to_string()}; + if (text == "ltr") { + return sourcemeta::core::JSONLDDirection::LTR; + } + if (text == "rtl") { + return sourcemeta::core::JSONLDDirection::RTL; + } + + return std::nullopt; +} + +auto parse_container(const sourcemeta::core::JSON &value) + -> std::optional { + if (!value.is_string()) { + return std::nullopt; + } + + const auto &text{value.to_string()}; + if (text == "@list") { + return sourcemeta::core::JSONLDContainer::List; + } + if (text == "@set") { + return sourcemeta::core::JSONLDContainer::Set; + } + if (text == "@language") { + return sourcemeta::core::JSONLDContainer::Language; + } + if (text == "@index") { + return sourcemeta::core::JSONLDContainer::Index; + } + + return std::nullopt; +} + +// A container overrides the value shape, as a list or set ranges over an array +// and a language or index map ranges over an object. A language map only holds +// string members, which is what the materializer requires +auto container_placement_error( + const sourcemeta::core::WeakPointer &pointer, + const sourcemeta::core::JSONLDContainer container, + const sourcemeta::core::JSON &value) + -> std::optional { + if (container == sourcemeta::core::JSONLDContainer::List || + container == sourcemeta::core::JSONLDContainer::Set) { + if (!value.is_array()) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Container, + "A JSON-LD list or set container can only be assigned to an array " + "value"); + } + + return std::nullopt; + } + + if (!value.is_object()) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Container, + "A JSON-LD language or index container can only be assigned to an " + "object value"); + } + + if (container == sourcemeta::core::JSONLDContainer::Language) { + for (const auto &entry : value.as_object()) { + if (entry.first != "@none" && + !sourcemeta::core::is_langtag(entry.first)) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Container, + "A JSON-LD language container requires BCP 47 language tag keys"); + } + + // A null member, or a null item in an array member, is treated as absent + const auto &member{entry.second}; + const bool usable{member.is_null() || member.is_string() || + (member.is_array() && + std::ranges::all_of( + member.as_array(), + [](const sourcemeta::core::JSON &element) -> bool { + return element.is_string() || element.is_null(); + }))}; + if (!usable) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Container, + "A JSON-LD language container requires string or null members"); + } + } + } + + return std::nullopt; +} + +// Whether the facts suit the value shape, as a node fact needs an object and a +// literal fact needs a scalar. Returns the first mismatch, or nothing +auto placement_error(const sourcemeta::core::WeakPointer &pointer, + const Facts &facts, const sourcemeta::core::JSON &value) + -> std::optional { + if (!value.is_object()) { + // A self identity promotes a scalar to a reference, which carries its own + // types, so a type is only misplaced on a scalar that has no self identity + if (!facts.types.empty() && !facts.self.has_value()) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Type, + "A JSON-LD type can only be assigned to an object value"); + } + + if (facts.graph) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Graph, + "A JSON-LD graph flag can only be assigned to an object value"); + } + } + + if (!value.is_object() && !value.is_array()) { + return std::nullopt; + } + + if (facts.datatype.has_value()) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Datatype, + "A JSON-LD datatype can only be assigned to a scalar value"); + } + + if (facts.language.has_value()) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Language, + "A JSON-LD language can only be assigned to a scalar value"); + } + + if (facts.direction.has_value()) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Direction, + "A JSON-LD direction can only be assigned to a scalar value"); + } + + return std::nullopt; +} + +// The constraints among literal facts, as a datatype excludes a language or +// direction, and a language or direction needs a string. Returns the first +// violation, or nothing +auto literal_error(const sourcemeta::core::WeakPointer &pointer, + const Facts &facts, const sourcemeta::core::JSON &value) + -> std::optional { + if (facts.datatype.has_value() && + (facts.language.has_value() || facts.direction.has_value())) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Datatype, + "A JSON-LD datatype cannot carry a language or direction"); + } + + if (facts.language.has_value() && !value.is_string()) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Language, + "A JSON-LD language can only be assigned to a string value"); + } + + if (facts.direction.has_value() && !value.is_string()) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Direction, + "A JSON-LD direction can only be assigned to a string value"); + } + + return std::nullopt; +} + +// Expand an x-jsonld-self URI Template into a concrete identifier. An object +// binds each variable to the member of that name, and a scalar binds the +// reserved variable this to its own value. A string binds directly and any +// other scalar binds through its JSON stringification, whereas an object, +// array, absent, or null value cannot bind. A binding that is not usable, or a +// result that is not an absolute IRI, is a fail-loud resolution error +auto expand_self(const sourcemeta::core::WeakPointer &pointer, + const sourcemeta::core::JSON::String &pattern, + const sourcemeta::core::JSON &value) + -> std::variant { + std::optional failure; + std::deque stringified; + const sourcemeta::core::URITemplate uri_template{pattern}; + auto expanded{uri_template.expand( + [&value, &pointer, &failure, &stringified]( + const std::string_view name) -> sourcemeta::core::URITemplateValue { + const sourcemeta::core::JSON *bound{nullptr}; + if (value.is_object()) { + bound = value.try_at(name); + } else if (name == "this") { + bound = &value; + } + + if (bound == nullptr || bound->is_object() || bound->is_array() || + bound->is_null()) { + if (!failure.has_value()) { + failure = facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Self, + "A JSON-LD self identity template variable must bind to a " + "string, number, or boolean"); + } + + return std::nullopt; + } + + if (bound->is_string()) { + if (bound->to_string().empty()) { + if (!failure.has_value()) { + failure = facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Self, + "A JSON-LD self identity template variable cannot bind to an " + "empty string"); + } + + return std::nullopt; + } + + return std::make_tuple(std::string_view{bound->to_string()}, + std::nullopt, false); + } + + // A number or boolean has no direct string, so it binds through its + // compact JSON form, kept alive for the duration of the expansion + std::ostringstream stream; + sourcemeta::core::stringify(*bound, stream); + stringified.push_back(stream.str()); + return std::make_tuple(std::string_view{stringified.back()}, + std::nullopt, false); + })}; + + if (failure.has_value()) { + return failure.value(); + } + + if (!sourcemeta::core::URI::is_iri(expanded)) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Self, + "A JSON-LD self identity must expand to an absolute IRI"); + } + + return sourcemeta::core::JSON::String{std::move(expanded)}; +} + +// Whether every element of an array materializes as a node, so the array can be +// the subject of a reverse predicate. An element is a node when it is a raw +// object or a scalar promoted to a reference by its own self identity +auto array_of_nodes(const Accumulator &accumulator, + const sourcemeta::core::WeakPointer &pointer, + const sourcemeta::core::JSON &value) -> bool { + for (std::size_t index = 0; index < value.size(); index += 1) { + if (value.at(index).is_object()) { + continue; + } + + auto element_pointer{pointer}; + element_pointer.push_back(index); + const auto element_facts{accumulator.find(element_pointer)}; + if (element_facts == accumulator.cend() || + !element_facts->second.self.has_value()) { + return false; + } + } + + return true; +} + +// Turn the JSON-LD annotations into a resolved map, or the first error found. +// The first pass groups the facts by location, the second derives each kind +// from the value shape and validates the facts against it +auto resolve(const sourcemeta::core::JSON &instance, + const sourcemeta::blaze::SimpleOutput &output) + -> std::variant { + Accumulator accumulator; + + for (const auto &[location, values] : output.annotations()) { + if (location.evaluate_path.empty()) { + continue; + } + + const auto &keyword{location.evaluate_path.back().to_property()}; + const auto &instance_location{location.instance_location}; + + if (keyword == "x-jsonld-id" || keyword == "x-jsonld-reverse") { + const bool reverse{keyword == "x-jsonld-reverse"}; + for (const auto &value : values) { + if (!is_iri_value(value)) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Predicate, + reverse ? "The value of x-jsonld-reverse must be an absolute IRI" + : "The value of x-jsonld-id must be an absolute IRI"); + } + + add_edge(accumulator[instance_location].edges, value.to_string(), + reverse); + } + } else if (keyword == "x-jsonld-type") { + auto &types{accumulator[instance_location].types}; + for (const auto &value : values) { + if (value.is_array()) { + for (const auto &element : value.as_array()) { + if (!is_iri_value(element)) { + return type_iri_error(instance_location); + } + + add_type(types, element.to_string()); + } + } else { + if (!is_iri_value(value)) { + return type_iri_error(instance_location); + } + + add_type(types, value.to_string()); + } + } + } else if (keyword == "x-jsonld-datatype") { + for (const auto &value : values) { + if (!is_iri_value(value)) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Datatype, + "The value of x-jsonld-datatype must be an absolute IRI"); + } + + auto &datatype{accumulator[instance_location].datatype}; + if (datatype.has_value() && datatype.value() != value.to_string()) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Datatype, + "A JSON-LD datatype cannot be assigned more than one value"); + } + + datatype = value.to_string(); + } + } else if (keyword == "x-jsonld-language") { + for (const auto &value : values) { + if (!value.is_string() || + !sourcemeta::core::is_langtag(value.to_string())) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Language, + "The value of x-jsonld-language must be a BCP 47 language tag"); + } + + auto &language{accumulator[instance_location].language}; + if (language.has_value()) { + // Language tags compare case-insensitively, so the first spelling is + // kept and only a genuinely different tag is a conflict + if (!sourcemeta::core::equals_ignore_case(language.value(), + value.to_string())) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Language, + "A JSON-LD language cannot be assigned more than one value"); + } + } else { + language = value.to_string(); + } + } + } else if (keyword == "x-jsonld-direction") { + for (const auto &value : values) { + const auto parsed{parse_direction(value)}; + if (!parsed.has_value()) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Direction, + R"(The value of x-jsonld-direction must be "ltr" or "rtl")"); + } + + auto &direction{accumulator[instance_location].direction}; + if (direction.has_value() && direction.value() != parsed.value()) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Direction, + "A JSON-LD direction cannot be assigned more than one value"); + } + + direction = parsed; + } + } else if (keyword == "x-jsonld-json") { + for (const auto &value : values) { + if (!value.is_boolean()) { + return facet_error(instance_location, + sourcemeta::blaze::JSONLDFacet::JSON, + "The value of x-jsonld-json must be a boolean"); + } + + // A false value leaves the fact unset, so it must stay a no-op and not + // create an entry + if (value.to_boolean()) { + accumulator[instance_location].json = true; + } + } + } else if (keyword == "x-jsonld-graph") { + for (const auto &value : values) { + if (!value.is_boolean()) { + return facet_error(instance_location, + sourcemeta::blaze::JSONLDFacet::Graph, + "The value of x-jsonld-graph must be a boolean"); + } + + if (value.to_boolean()) { + accumulator[instance_location].graph = true; + } + } + } else if (keyword == "x-jsonld-container") { + for (const auto &value : values) { + const auto parsed{parse_container(value)}; + if (!parsed.has_value()) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Container, + R"(The value of x-jsonld-container must be "@list", "@set", "@language", or "@index")"); + } + + auto &container{accumulator[instance_location].container}; + if (container.has_value() && container.value() != parsed.value()) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Container, + "A JSON-LD container cannot be assigned more than one value"); + } + + container = parsed; + } + } else if (keyword == "x-jsonld-self") { + for (const auto &value : values) { + if (!value.is_string() || + !sourcemeta::core::URITemplate::is_uritemplate(value.to_string())) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Self, + "The value of x-jsonld-self must be a URI Template"); + } + + auto &self{accumulator[instance_location].self}; + if (self.has_value() && self.value() != value.to_string()) { + return facet_error( + instance_location, sourcemeta::blaze::JSONLDFacet::Self, + "A JSON-LD self identity cannot be assigned more than one value"); + } + + self = value.to_string(); + } + } + } + + sourcemeta::core::JSONLDWeakAnnotationMap map; + map.reserve(accumulator.size()); + for (auto &[pointer, facts] : accumulator) { + std::ranges::sort(facts.edges, + [](const sourcemeta::core::JSONLDEdge &left, + const sourcemeta::core::JSONLDEdge &right) -> bool { + return std::tie(left.predicate, left.reverse) < + std::tie(right.predicate, right.reverse); + }); + std::ranges::sort(facts.types); + + const auto &value{sourcemeta::core::get(instance, pointer)}; + + // A language container materializes its members directly as language-tagged + // strings, never consulting their descriptors, so a member cannot carry a + // JSON-LD annotation of its own + if (!pointer.empty()) { + auto parent{pointer}; + parent.pop_back(); + const auto parent_facts{accumulator.find(parent)}; + if (parent_facts != accumulator.cend() && + parent_facts->second.container == + sourcemeta::core::JSONLDContainer::Language) { + return facet_error(pointer, sourcemeta::blaze::JSONLDFacet::Container, + "A JSON-LD language container member cannot carry a " + "JSON-LD annotation"); + } + } + + // A container sets the collection shape and stands alone, excluding every + // other fact and choosing the value shape it ranges over + if (facts.container.has_value()) { + if (!facts.types.empty() || facts.graph || facts.datatype.has_value() || + facts.language.has_value() || facts.direction.has_value() || + facts.json || facts.self.has_value()) { + return facet_error(pointer, sourcemeta::blaze::JSONLDFacet::Container, + "A JSON-LD container cannot be combined with any " + "other JSON-LD annotation"); + } + + if (const auto error{container_placement_error( + pointer, facts.container.value(), value)}; + error.has_value()) { + return error.value(); + } + } + + // A JSON literal preserves any value verbatim, so it stands alone and + // excludes every other fact + if (facts.json && + (!facts.types.empty() || facts.graph || facts.datatype.has_value() || + facts.language.has_value() || facts.direction.has_value() || + facts.self.has_value())) { + return facet_error(pointer, sourcemeta::blaze::JSONLDFacet::JSON, + "A JSON-LD JSON literal cannot be combined with any " + "other JSON-LD annotation"); + } + + // A self identity mints an @id, promoting a scalar to a reference and + // giving an object its identifier. It describes a node, so it excludes the + // literal facets and cannot apply to an array collection + if (facts.self.has_value()) { + if (facts.datatype.has_value() || facts.language.has_value() || + facts.direction.has_value()) { + return facet_error(pointer, sourcemeta::blaze::JSONLDFacet::Self, + "A JSON-LD self identity cannot carry a datatype, " + "language, or direction"); + } + + if (value.is_array()) { + return facet_error(pointer, sourcemeta::blaze::JSONLDFacet::Self, + "A JSON-LD self identity can only be assigned to an " + "object or scalar value"); + } + } + + if (const auto error{placement_error(pointer, facts, value)}; + error.has_value()) { + return error.value(); + } + + if (const auto error{literal_error(pointer, facts, value)}; + error.has_value()) { + return error.value(); + } + + // A predicate attaches to the parent node of an object property. The + // document root has no parent and an array element inherits the enclosing + // edge, so neither can carry one + if (!facts.edges.empty() && + (pointer.empty() || !pointer.back().is_property())) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Predicate, + pointer.empty() + ? "A JSON-LD predicate cannot be assigned to the document root" + : "A JSON-LD predicate cannot be assigned to an array element"); + } + + // A container member belongs to a collection, not a node, so it has no + // parent to attach a predicate to + if (!facts.edges.empty() && !pointer.empty()) { + auto parent{pointer}; + parent.pop_back(); + const auto parent_facts{accumulator.find(parent)}; + if (parent_facts != accumulator.cend() && + parent_facts->second.container.has_value()) { + return facet_error( + pointer, sourcemeta::blaze::JSONLDFacet::Predicate, + "A JSON-LD predicate cannot be assigned to a container member"); + } + } + + // A reverse predicate makes its value the subject, so the value must be a + // node or an array of nodes. A literal cannot be a subject + if (std::ranges::any_of( + facts.edges, [](const sourcemeta::core::JSONLDEdge &edge) -> bool { + return edge.reverse; + })) { + const bool points_to_node{ + !facts.json && !facts.container.has_value() && + (value.is_object() || facts.self.has_value() || + (value.is_array() && array_of_nodes(accumulator, pointer, value)))}; + if (!points_to_node) { + return facet_error(pointer, sourcemeta::blaze::JSONLDFacet::Predicate, + "A JSON-LD reverse predicate can only point to a " + "node or an array of nodes"); + } + } + + std::optional identifier; + if (facts.self.has_value()) { + auto expanded{expand_self(pointer, facts.self.value(), value)}; + if (std::holds_alternative( + expanded)) { + return std::get( + std::move(expanded)); + } + + identifier = + std::get(std::move(expanded)); + } + + sourcemeta::core::JSONLDDescriptor descriptor; + descriptor.edges = std::move(facts.edges); + if (facts.json) { + descriptor.value = sourcemeta::core::JSONLDLiteral{.json = true}; + } else if (facts.container.has_value()) { + descriptor.value = sourcemeta::core::JSONLDCollection{ + .container = facts.container.value()}; + } else if (value.is_object()) { + descriptor.value = + sourcemeta::core::JSONLDNode{.id = std::move(identifier), + .types = std::move(facts.types), + .graph = facts.graph}; + } else if (value.is_array()) { + descriptor.value = sourcemeta::core::JSONLDCollection{}; + } else if (identifier.has_value()) { + descriptor.value = sourcemeta::core::JSONLDReference{ + .id = std::move(identifier.value()), .types = std::move(facts.types)}; + } else { + descriptor.value = + sourcemeta::core::JSONLDLiteral{.datatype = std::move(facts.datatype), + .language = std::move(facts.language), + .direction = facts.direction}; + } + + map.emplace(pointer, std::move(descriptor)); + + // A language container reads its object members directly, but every other + // collection needs its undescribed members defaulted so they materialize + if (facts.container.has_value()) { + if (facts.container.value() != + sourcemeta::core::JSONLDContainer::Language) { + fill_collection(map, accumulator, pointer, value); + } + } else if (value.is_array() && !facts.json) { + fill_collection(map, accumulator, pointer, value); + } + } + + return map; +} + +} // namespace + +namespace sourcemeta::blaze { + +auto jsonld(Evaluator &evaluator, const Template &schema, + const sourcemeta::core::JSON &instance) -> JSONLDOutcome { + SimpleOutput output{instance}; + const auto valid{evaluator.validate(schema, instance, std::ref(output))}; + if (!valid) { + return std::move(output).release(); + } + + auto resolved{resolve(instance, output)}; + if (std::holds_alternative(resolved)) { + return std::get(std::move(resolved)); + } + + const auto &map{ + std::get(resolved)}; + return sourcemeta::core::jsonld_materialize(instance, map); +} + +} // namespace sourcemeta::blaze diff --git a/vendor/core/CMakeLists.txt b/vendor/core/CMakeLists.txt index f57bbdd3a..928273ed8 100644 --- a/vendor/core/CMakeLists.txt +++ b/vendor/core/CMakeLists.txt @@ -57,6 +57,13 @@ if(PROJECT_IS_TOP_LEVEL) sourcemeta_enable_simd() endif() +# Enable the sanitizers before defining any target +if(SOURCEMETA_CORE_ADDRESS_SANITIZER) + sourcemeta_sanitizer(TYPE address) +elseif(SOURCEMETA_CORE_UNDEFINED_SANITIZER) + sourcemeta_sanitizer(TYPE undefined) +endif() + # TODO: Turn this into a re-usable utility CMake function if(SOURCEMETA_CORE_INSTALL) include(GNUInstallDirs) @@ -226,12 +233,6 @@ if(SOURCEMETA_CORE_MARKDOWN) add_subdirectory(src/core/markdown) endif() -if(SOURCEMETA_CORE_ADDRESS_SANITIZER) - sourcemeta_sanitizer(TYPE address) -elseif(SOURCEMETA_CORE_UNDEFINED_SANITIZER) - sourcemeta_sanitizer(TYPE undefined) -endif() - if(SOURCEMETA_CORE_DOCS) sourcemeta_target_doxygen(CONFIG "${PROJECT_SOURCE_DIR}/doxygen/Doxyfile.in" OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/website") diff --git a/vendor/core/DEPENDENCIES b/vendor/core/DEPENDENCIES index b0bedf0ac..29ef05774 100644 --- a/vendor/core/DEPENDENCIES +++ b/vendor/core/DEPENDENCIES @@ -11,3 +11,4 @@ libdeflate https://github.com/ebiggers/libdeflate v1.25 unicodetools https://github.com/unicode-org/unicodetools final-17.0-20250910 jose-cookbook https://github.com/ietf-jose/cookbook 13692b68bfc18b99557a5b1ed311fd5077bfff04 w3c-json-ld https://github.com/w3c/json-ld-api 8654ac22b6cf4f441d2fee915ae634d36b5a8067 +aws-sig-v4-test-suite https://github.com/saibotsivad/aws-sig-v4-test-suite 38179d1173c9547a964e7f92e9308e73795534ff diff --git a/vendor/core/cmake/common/targets/clang-format.cmake b/vendor/core/cmake/common/targets/clang-format.cmake index 7868fceb0..e5507aa41 100644 --- a/vendor/core/cmake/common/targets/clang-format.cmake +++ b/vendor/core/cmake/common/targets/clang-format.cmake @@ -101,6 +101,12 @@ function(sourcemeta_target_clang_format) file(GLOB_RECURSE SOURCEMETA_TARGET_CLANG_FORMAT_FILES ${SOURCEMETA_TARGET_CLANG_FORMAT_SOURCES}) + # Fail fast rather than silently skipping formatting, since batching an empty + # file list would otherwise produce targets that do nothing but succeed + if(NOT SOURCEMETA_TARGET_CLANG_FORMAT_FILES) + message(FATAL_ERROR "The ClangFormat file globs did not match any files") + endif() + set(CLANG_FORMAT_CONFIG "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/clang-format.json") if(CMAKE_SYSTEM_NAME STREQUAL "MSYS") # Because `clang-format` is typically a Windows `.exe`, transform the path accordingly @@ -110,18 +116,58 @@ function(sourcemeta_target_clang_format) if(CLANG_FORMAT_BIN) message(STATUS "Using `clang-format` from ${CLANG_FORMAT_BIN}") + + # Invoke `clang-format` over the files in batches whose combined argument + # length is bounded well below the operating system command-line length + # limit (notably the Windows `CreateProcess` limit of around 32767 + # characters), so the command stays runnable however many sources exist and + # however long their paths are + set(SOURCEMETA_CLANG_FORMAT_BATCH_LIMIT 8000) + set(SOURCEMETA_CLANG_FORMAT_COMMANDS) + set(SOURCEMETA_CLANG_FORMAT_TEST_COMMANDS) + set(SOURCEMETA_CLANG_FORMAT_BATCH) + set(SOURCEMETA_CLANG_FORMAT_BATCH_LENGTH 0) + foreach(SOURCEMETA_CLANG_FORMAT_FILE IN LISTS + SOURCEMETA_TARGET_CLANG_FORMAT_FILES) + string(LENGTH "${SOURCEMETA_CLANG_FORMAT_FILE}" + SOURCEMETA_CLANG_FORMAT_FILE_LENGTH) + math(EXPR SOURCEMETA_CLANG_FORMAT_NEXT_LENGTH + "${SOURCEMETA_CLANG_FORMAT_BATCH_LENGTH} + ${SOURCEMETA_CLANG_FORMAT_FILE_LENGTH} + 1") + # Flush the current batch before it would cross the bound, keeping at + # least one file per batch so an unusually long path still makes progress + if(SOURCEMETA_CLANG_FORMAT_BATCH AND SOURCEMETA_CLANG_FORMAT_NEXT_LENGTH + GREATER SOURCEMETA_CLANG_FORMAT_BATCH_LIMIT) + list(APPEND SOURCEMETA_CLANG_FORMAT_COMMANDS + COMMAND "${CLANG_FORMAT_BIN}" "--style=file:${CLANG_FORMAT_CONFIG}" + -i ${SOURCEMETA_CLANG_FORMAT_BATCH}) + list(APPEND SOURCEMETA_CLANG_FORMAT_TEST_COMMANDS + COMMAND "${CLANG_FORMAT_BIN}" "--style=file:${CLANG_FORMAT_CONFIG}" + --dry-run -Werror -i ${SOURCEMETA_CLANG_FORMAT_BATCH}) + set(SOURCEMETA_CLANG_FORMAT_BATCH) + set(SOURCEMETA_CLANG_FORMAT_BATCH_LENGTH 0) + endif() + list(APPEND SOURCEMETA_CLANG_FORMAT_BATCH "${SOURCEMETA_CLANG_FORMAT_FILE}") + math(EXPR SOURCEMETA_CLANG_FORMAT_BATCH_LENGTH + "${SOURCEMETA_CLANG_FORMAT_BATCH_LENGTH} + ${SOURCEMETA_CLANG_FORMAT_FILE_LENGTH} + 1") + endforeach() + if(SOURCEMETA_CLANG_FORMAT_BATCH) + list(APPEND SOURCEMETA_CLANG_FORMAT_COMMANDS + COMMAND "${CLANG_FORMAT_BIN}" "--style=file:${CLANG_FORMAT_CONFIG}" + -i ${SOURCEMETA_CLANG_FORMAT_BATCH}) + list(APPEND SOURCEMETA_CLANG_FORMAT_TEST_COMMANDS + COMMAND "${CLANG_FORMAT_BIN}" "--style=file:${CLANG_FORMAT_CONFIG}" + --dry-run -Werror -i ${SOURCEMETA_CLANG_FORMAT_BATCH}) + endif() + add_custom_target(clang_format WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" VERBATIM - COMMAND "${CLANG_FORMAT_BIN}" "--style=file:${CLANG_FORMAT_CONFIG}" - -i ${SOURCEMETA_TARGET_CLANG_FORMAT_FILES} + ${SOURCEMETA_CLANG_FORMAT_COMMANDS} COMMENT "Formatting sources using ClangFormat") add_custom_target(clang_format_test WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" VERBATIM - COMMAND "${CLANG_FORMAT_BIN}" "--style=file:${CLANG_FORMAT_CONFIG}" - --dry-run -Werror - -i ${SOURCEMETA_TARGET_CLANG_FORMAT_FILES} + ${SOURCEMETA_CLANG_FORMAT_TEST_COMMANDS} COMMENT "Checking for ClangFormat compliance") else() add_custom_target(clang_format diff --git a/vendor/core/config.cmake.in b/vendor/core/config.cmake.in index 50a91e0bb..4bcd0ab16 100644 --- a/vendor/core/config.cmake.in +++ b/vendor/core/config.cmake.in @@ -76,8 +76,11 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_crypto.cmake") elseif(component STREQUAL "regex") find_dependency(PCRE2 CONFIG) + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_regex.cmake") elseif(component STREQUAL "ip") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_ip.cmake") elseif(component STREQUAL "idna") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") @@ -108,6 +111,8 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) elseif(component STREQUAL "uritemplate") find_dependency(PCRE2 CONFIG) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_regex.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_uritemplate.cmake") elseif(component STREQUAL "json") @@ -115,6 +120,7 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") elseif(component STREQUAL "jsonl") find_dependency(LibDeflate CONFIG) @@ -137,6 +143,7 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonpointer.cmake") elseif(component STREQUAL "jsonld") find_dependency(PCRE2 CONFIG) @@ -163,6 +170,7 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonpointer.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_yaml.cmake") elseif(component STREQUAL "jsonrpc") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") @@ -170,6 +178,7 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonrpc.cmake") elseif(component STREQUAL "mcp") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") @@ -177,6 +186,7 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.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/CMakeLists.txt b/vendor/core/src/core/crypto/CMakeLists.txt index 679bfd55f..338f485e7 100644 --- a/vendor/core/src/core/crypto/CMakeLists.txt +++ b/vendor/core/src/core/crypto/CMakeLists.txt @@ -1,18 +1,28 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME crypto - PRIVATE_HEADERS sha256.h sha384.h sha512.h sha1.h fnv128.h uuid.h crc32.h - base64.h verify.h - SOURCES crypto_sha256.cc crypto_sha384.cc crypto_sha512.cc crypto_sha1.cc + PRIVATE_HEADERS sha256.h hmac_sha256.h hmac_sha384.h hmac_sha512.h sha384.h + sha512.h sha1.h + fnv128.h + uuid.h crc32.h base64.h verify.h sign.h aes_gcm.h + SOURCES crypto_sha256.cc crypto_hmac_sha256.cc crypto_hmac_sha384.cc + crypto_hmac_sha512.cc + crypto_sha384.cc + crypto_sha512.cc crypto_sha1.cc crypto_uuid.cc crypto_fnv128.cc crypto_crc32.cc crypto_base64.cc - crypto_sha2_64.h crypto_random.h crypto_helpers.h) + crypto_random.cc crypto_sign_rsa.cc crypto_aes_gcm.cc + crypto_sha2_64.h crypto_random.h crypto_helpers.h crypto_der.h crypto_aes.h) target_link_libraries(sourcemeta_core_crypto PRIVATE sourcemeta::core::text sourcemeta::core::numeric) if(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) target_sources(sourcemeta_core_crypto PRIVATE - crypto_sha1_openssl.cc crypto_sha256_openssl.cc crypto_sha384_openssl.cc + crypto_sha1_openssl.cc crypto_sha256_openssl.cc + crypto_hmac_sha256_openssl.cc crypto_hmac_sha384_openssl.cc + crypto_hmac_sha512_openssl.cc + crypto_sha384_openssl.cc crypto_sha512_openssl.cc crypto_random_openssl.cc - crypto_verify_openssl.cc) + crypto_verify_openssl.cc crypto_secure_equals_openssl.cc + crypto_sign_openssl.cc crypto_aes_gcm_openssl.cc) target_link_libraries(sourcemeta_core_crypto PRIVATE OpenSSL::Crypto) elseif(APPLE) enable_language(OBJCXX) @@ -96,11 +106,15 @@ elseif(APPLE) PROPERTIES OBJECT_DEPENDS "${CRYPTOKIT_HEADER}") target_sources(sourcemeta_core_crypto PRIVATE - crypto_sha1_apple.cc crypto_sha256_apple.cc crypto_sha384_apple.cc + crypto_sha1_apple.cc crypto_sha256_apple.cc crypto_hmac_sha256_apple.cc + crypto_hmac_sha384_apple.cc crypto_hmac_sha512_apple.cc + crypto_sha384_apple.cc crypto_sha512_apple.cc crypto_random_apple.cc - crypto_verify_apple.cc + crypto_verify_apple.cc crypto_secure_equals_apple.cc + crypto_sign_apple.cc crypto_aes_gcm_apple.cc crypto_eddsa_cryptokit.mm "${CRYPTOKIT_OBJECT}" - crypto_eddsa.h crypto_eddsa_apple.h crypto_bignum.h crypto_shake256.h) + crypto_eddsa.h crypto_eddsa_apple.h crypto_aes_apple.h crypto_bignum.h + crypto_shake256.h crypto_pkcs8.h) # The generated Objective-C interface header lives in the build tree target_include_directories(sourcemeta_core_crypto @@ -121,18 +135,27 @@ elseif(APPLE) "SHELL:-Xlinker -rpath -Xlinker /usr/lib/swift") elseif(WIN32) target_sources(sourcemeta_core_crypto PRIVATE - crypto_sha1_windows.cc crypto_sha256_windows.cc crypto_sha384_windows.cc + crypto_sha1_windows.cc crypto_sha256_windows.cc + crypto_hmac_sha256_windows.cc crypto_hmac_sha384_windows.cc + crypto_hmac_sha512_windows.cc + crypto_sha384_windows.cc crypto_sha512_windows.cc crypto_random_windows.cc - crypto_verify_windows.cc crypto_eddsa.h - crypto_bignum.h crypto_shake256.h) + crypto_verify_windows.cc crypto_secure_equals_generic.cc + crypto_sign_windows.cc crypto_aes_gcm_windows.cc crypto_eddsa.h + crypto_bignum.h crypto_ecc.h crypto_shake256.h crypto_pkcs8.h) target_link_libraries(sourcemeta_core_crypto PRIVATE bcrypt) else() message(WARNING "Building the reference cryptography backend, instead of the " "OpenSSL recommended production one") target_sources(sourcemeta_core_crypto PRIVATE - crypto_sha1_other.cc crypto_sha256_other.cc crypto_sha384_other.cc + crypto_sha1_other.cc crypto_sha256_other.cc crypto_hmac_sha256_other.cc + crypto_hmac_sha384_other.cc crypto_hmac_sha512_other.cc + crypto_sha384_other.cc crypto_sha512_other.cc crypto_random_other.cc crypto_verify_other.cc - crypto_bignum.h crypto_ecc.h crypto_eddsa.h crypto_shake256.h) + crypto_secure_equals_generic.cc crypto_sign_other.cc + crypto_aes_gcm_other.cc + crypto_bignum.h crypto_ecc.h crypto_eddsa.h crypto_shake256.h + crypto_pkcs8.h) endif() if(SOURCEMETA_CORE_INSTALL) diff --git a/vendor/core/src/core/crypto/crypto_aes.h b/vendor/core/src/core/crypto/crypto_aes.h new file mode 100644 index 000000000..97dc20efb --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_aes.h @@ -0,0 +1,31 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_AES_H_ +#define SOURCEMETA_CORE_CRYPTO_AES_H_ + +#include // std::optional +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +// The raw AES-256 Galois/Counter Mode primitive (NIST SP 800-38D) with a 96-bit +// nonce, no associated data, and a 128-bit tag, defined once per backend. The +// framing that adds the nonce and length checks lives in the shared seal and +// unseal functions, so these assume a 32-byte key and a 12-byte nonce + +// Encrypt the plaintext, returning the ciphertext followed by the 16-byte +// authentication tag +auto aes_256_gcm_encrypt(const std::string_view key, + const std::string_view nonce, + const std::string_view plaintext) + -> std::optional; + +// Decrypt the ciphertext, which is followed by its 16-byte tag, verifying the +// tag before returning the plaintext and rejecting a mismatch +auto aes_256_gcm_decrypt(const std::string_view key, + const std::string_view nonce, + const std::string_view ciphertext) + -> std::optional; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/crypto_aes_apple.h b/vendor/core/src/core/crypto/crypto_aes_apple.h new file mode 100644 index 000000000..3a403bb5e --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_aes_apple.h @@ -0,0 +1,24 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_AES_APPLE_H_ +#define SOURCEMETA_CORE_CRYPTO_AES_APPLE_H_ + +#include // std::size_t + +// Seal a plaintext with AES-256 in Galois/Counter Mode through CryptoKit, +// writing the ciphertext followed by the 16-byte tag into the output buffer, +// which the caller sizes to the plaintext length plus 16. Returns false when +// the key or nonce is not a valid length +extern "C" auto sourcemeta_core_aes_256_gcm_seal_cryptokit( + const unsigned char *key, std::size_t key_size, const unsigned char *nonce, + std::size_t nonce_size, const unsigned char *plaintext, + std::size_t plaintext_size, unsigned char *output) -> bool; + +// Open a message sealed with the function above, writing the plaintext into the +// output buffer, which the caller sizes to the ciphertext length. Returns false +// when the tag does not verify or the input is malformed +extern "C" auto sourcemeta_core_aes_256_gcm_open_cryptokit( + const unsigned char *key, std::size_t key_size, const unsigned char *nonce, + std::size_t nonce_size, const unsigned char *ciphertext, + std::size_t ciphertext_size, const unsigned char *tag, std::size_t tag_size, + unsigned char *output) -> bool; + +#endif diff --git a/vendor/core/src/core/crypto/crypto_aes_gcm.cc b/vendor/core/src/core/crypto/crypto_aes_gcm.cc new file mode 100644 index 000000000..68dd4c482 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_aes_gcm.cc @@ -0,0 +1,66 @@ +#include + +#include "crypto_aes.h" +#include "crypto_random.h" + +#include // std::size_t +#include // std::uint8_t +#include // std::exception +#include // std::numeric_limits +#include // std::optional, std::nullopt +#include // std::span +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +namespace { +// A 256-bit key, a 96-bit nonce, and a 128-bit tag (NIST SP 800-38D) +constexpr std::size_t KEY_BYTES{32}; +constexpr std::size_t NONCE_BYTES{12}; +constexpr std::size_t TAG_BYTES{16}; +// An upper bound the backends can all process without narrowing their lengths +constexpr std::size_t MAX_INPUT_BYTES{ + static_cast(std::numeric_limits::max())}; +} // namespace + +auto aes_256_gcm_seal(const std::string_view key, + const std::string_view plaintext) + -> std::optional { + if (key.size() != KEY_BYTES || plaintext.size() > MAX_INPUT_BYTES) { + return std::nullopt; + } + + std::string nonce(NONCE_BYTES, '\x00'); + try { + fill_random_bytes(std::span{ + reinterpret_cast(nonce.data()), NONCE_BYTES}); + } catch (const std::exception &) { + return std::nullopt; + } + + const auto ciphertext{aes_256_gcm_encrypt(key, nonce, plaintext)}; + if (!ciphertext.has_value()) { + return std::nullopt; + } + + // The sealed message is the nonce followed by the ciphertext and its tag + std::string sealed{std::move(nonce)}; + sealed.append(ciphertext.value()); + return sealed; +} + +auto aes_256_gcm_unseal(const std::string_view key, + const std::string_view sealed) + -> std::optional { + if (key.size() != KEY_BYTES || sealed.size() < NONCE_BYTES + TAG_BYTES || + sealed.size() - NONCE_BYTES - TAG_BYTES > MAX_INPUT_BYTES) { + return std::nullopt; + } + + const auto nonce{sealed.substr(0, NONCE_BYTES)}; + const auto ciphertext{sealed.substr(NONCE_BYTES)}; + return aes_256_gcm_decrypt(key, nonce, ciphertext); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_aes_gcm_apple.cc b/vendor/core/src/core/crypto/crypto_aes_gcm_apple.cc new file mode 100644 index 000000000..e4c40acf5 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_aes_gcm_apple.cc @@ -0,0 +1,55 @@ +#include "crypto_aes.h" + +#include "crypto_aes_apple.h" + +#include // std::size_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view + +namespace { +constexpr std::size_t TAG_BYTES{16}; +} // namespace + +namespace sourcemeta::core { + +auto aes_256_gcm_encrypt(const std::string_view key, + const std::string_view nonce, + const std::string_view plaintext) + -> std::optional { + std::string output(plaintext.size() + TAG_BYTES, '\x00'); + if (!sourcemeta_core_aes_256_gcm_seal_cryptokit( + reinterpret_cast(key.data()), key.size(), + reinterpret_cast(nonce.data()), nonce.size(), + reinterpret_cast(plaintext.data()), + plaintext.size(), reinterpret_cast(output.data()))) { + return std::nullopt; + } + + return output; +} + +auto aes_256_gcm_decrypt(const std::string_view key, + const std::string_view nonce, + const std::string_view ciphertext) + -> std::optional { + if (ciphertext.size() < TAG_BYTES) { + return std::nullopt; + } + + const auto message{ciphertext.substr(0, ciphertext.size() - TAG_BYTES)}; + const auto tag{ciphertext.substr(ciphertext.size() - TAG_BYTES)}; + std::string output(message.size(), '\x00'); + if (!sourcemeta_core_aes_256_gcm_open_cryptokit( + reinterpret_cast(key.data()), key.size(), + reinterpret_cast(nonce.data()), nonce.size(), + reinterpret_cast(message.data()), + message.size(), reinterpret_cast(tag.data()), + tag.size(), reinterpret_cast(output.data()))) { + return std::nullopt; + } + + return output; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_aes_gcm_openssl.cc b/vendor/core/src/core/crypto/crypto_aes_gcm_openssl.cc new file mode 100644 index 000000000..17e23de52 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_aes_gcm_openssl.cc @@ -0,0 +1,106 @@ +#include "crypto_aes.h" + +#include // EVP_* + +#include // std::size_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move + +namespace { +constexpr std::size_t TAG_BYTES{16}; +} // namespace + +namespace sourcemeta::core { + +auto aes_256_gcm_encrypt(const std::string_view key, + const std::string_view nonce, + const std::string_view plaintext) + -> std::optional { + auto *context{EVP_CIPHER_CTX_new()}; + if (context == nullptr) { + return std::nullopt; + } + + std::optional result; + std::string ciphertext(plaintext.size(), '\x00'); + std::string tag(TAG_BYTES, '\x00'); + int length{0}; + int final_length{0}; + if (EVP_EncryptInit_ex(context, EVP_aes_256_gcm(), nullptr, nullptr, + nullptr) == 1 && + EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_SET_IVLEN, + static_cast(nonce.size()), nullptr) == 1 && + EVP_EncryptInit_ex( + context, nullptr, nullptr, + reinterpret_cast(key.data()), + reinterpret_cast(nonce.data())) == 1 && + (plaintext.empty() || + EVP_EncryptUpdate( + context, reinterpret_cast(ciphertext.data()), + &length, reinterpret_cast(plaintext.data()), + static_cast(plaintext.size())) == 1) && + EVP_EncryptFinal_ex(context, + reinterpret_cast(ciphertext.data()) + + length, + &final_length) == 1 && + EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_GET_TAG, + static_cast(TAG_BYTES), + reinterpret_cast(tag.data())) == 1) { + ciphertext.append(tag); + result = std::move(ciphertext); + } + + EVP_CIPHER_CTX_free(context); + return result; +} + +auto aes_256_gcm_decrypt(const std::string_view key, + const std::string_view nonce, + const std::string_view ciphertext) + -> std::optional { + if (ciphertext.size() < TAG_BYTES) { + return std::nullopt; + } + + const auto message{ciphertext.substr(0, ciphertext.size() - TAG_BYTES)}; + const auto tag{ciphertext.substr(ciphertext.size() - TAG_BYTES)}; + auto *context{EVP_CIPHER_CTX_new()}; + if (context == nullptr) { + return std::nullopt; + } + + std::optional result; + std::string plaintext(message.size(), '\x00'); + int length{0}; + int final_length{0}; + if (EVP_DecryptInit_ex(context, EVP_aes_256_gcm(), nullptr, nullptr, + nullptr) == 1 && + EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_SET_IVLEN, + static_cast(nonce.size()), nullptr) == 1 && + EVP_DecryptInit_ex( + context, nullptr, nullptr, + reinterpret_cast(key.data()), + reinterpret_cast(nonce.data())) == 1 && + (message.empty() || + EVP_DecryptUpdate( + context, reinterpret_cast(plaintext.data()), + &length, reinterpret_cast(message.data()), + static_cast(message.size())) == 1) && + EVP_CIPHER_CTX_ctrl( + context, EVP_CTRL_GCM_SET_TAG, static_cast(TAG_BYTES), + const_cast( + reinterpret_cast(tag.data()))) == 1 && + // The final step verifies the tag and reports failure for a mismatch + EVP_DecryptFinal_ex( + context, reinterpret_cast(plaintext.data()) + length, + &final_length) == 1) { + result = std::move(plaintext); + } + + EVP_CIPHER_CTX_free(context); + return result; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_aes_gcm_other.cc b/vendor/core/src/core/crypto/crypto_aes_gcm_other.cc new file mode 100644 index 000000000..f37e2f188 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_aes_gcm_other.cc @@ -0,0 +1,303 @@ +#include "crypto_aes.h" + +#include // std::array +#include // std::size_t +#include // std::uint8_t, std::uint64_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view + +// A from-scratch AES-256 in Galois/Counter Mode (FIPS 197 and NIST SP 800-38D) +// for the reference backend. This is not constant-time, which is acceptable +// only because this backend is the non-production fallback + +namespace sourcemeta::core { +namespace { + +// The Rijndael substitution box (FIPS 197 Figure 7) +constexpr std::array SUBSTITUTION{ + {0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, + 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, + 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, + 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, + 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, + 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, + 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, + 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, + 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, + 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, + 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, + 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, + 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, + 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, + 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, + 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, + 0xb0, 0x54, 0xbb, 0x16}}; + +using Block = std::array; +using RoundKeys = std::array; + +// Multiply by two in GF(2^8) with the AES reduction polynomial (FIPS 197 +// Section 4.2) +auto xtime(const std::uint8_t value) -> std::uint8_t { + return static_cast((static_cast(value) << 1u) ^ + (((value & 0x80u) != 0) ? 0x1bu : 0x00u)); +} + +auto multiply(const std::uint8_t left, const std::uint8_t right) + -> std::uint8_t { + std::uint8_t product{0}; + std::uint8_t factor{left}; + for (std::uint8_t bit{right}; bit != 0; bit >>= 1u) { + if ((bit & 1u) != 0) { + product ^= factor; + } + + factor = xtime(factor); + } + + return product; +} + +// AES-256 key expansion (FIPS 197 Section 5.2, with the 256-bit key schedule) +auto expand_key(const std::string_view key) -> RoundKeys { + RoundKeys round_keys{}; + for (std::size_t index = 0; index < 32; ++index) { + round_keys[index] = static_cast(key[index]); + } + + std::uint8_t round_constant{0x01}; + for (std::size_t index = 32; index < round_keys.size(); index += 4) { + std::array word{ + {round_keys[index - 4], round_keys[index - 3], round_keys[index - 2], + round_keys[index - 1]}}; + const auto position{index / 4}; + if (position % 8 == 0) { + const auto first{word[0]}; + word[0] = SUBSTITUTION[word[1]] ^ round_constant; + word[1] = SUBSTITUTION[word[2]]; + word[2] = SUBSTITUTION[word[3]]; + word[3] = SUBSTITUTION[first]; + round_constant = xtime(round_constant); + } else if (position % 8 == 4) { + for (auto &byte : word) { + byte = SUBSTITUTION[byte]; + } + } + + for (std::size_t offset = 0; offset < 4; ++offset) { + round_keys[index + offset] = + round_keys[index - 32 + offset] ^ word[offset]; + } + } + + return round_keys; +} + +auto encrypt_block(const RoundKeys &round_keys, Block state) -> Block { + 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); + for (std::size_t round = 1; round <= 14; ++round) { + for (auto &byte : state) { + byte = SUBSTITUTION[byte]; + } + + // ShiftRows over the column-major state (FIPS 197 Section 5.1.2) + const Block shifted{{state[0], state[5], state[10], state[15], state[4], + state[9], state[14], state[3], state[8], state[13], + state[2], state[7], state[12], state[1], state[6], + state[11]}}; + state = shifted; + + if (round != 14) { + // MixColumns (FIPS 197 Section 5.1.3) + for (std::size_t column = 0; column < 4; ++column) { + const auto base{column * 4}; + const auto first{state[base]}; + const auto second{state[base + 1]}; + const auto third{state[base + 2]}; + const auto fourth{state[base + 3]}; + state[base] = multiply(first, 2) ^ multiply(second, 3) ^ third ^ fourth; + state[base + 1] = + first ^ multiply(second, 2) ^ multiply(third, 3) ^ fourth; + state[base + 2] = + first ^ second ^ multiply(third, 2) ^ multiply(fourth, 3); + state[base + 3] = + multiply(first, 3) ^ second ^ third ^ multiply(fourth, 2); + } + } + + add_round_key(round); + } + + return state; +} + +// Multiply two blocks in GF(2^128) with the GCM reduction polynomial (NIST SP +// 800-38D Section 6.3) +auto gf_multiply(const Block &left, const Block &right) -> Block { + Block product{}; + Block value{left}; + for (std::size_t bit = 0; bit < 128; ++bit) { + if (((right[bit / 8] >> (7 - (bit % 8))) & 1u) != 0) { + for (std::size_t index = 0; index < 16; ++index) { + product[index] ^= value[index]; + } + } + + const auto carry_out{static_cast(value[15] & 1u)}; + std::uint8_t carry_in{0}; + for (auto &byte : value) { + const auto next_carry{static_cast(byte & 1u)}; + byte = static_cast((byte >> 1u) | (carry_in << 7u)); + carry_in = next_carry; + } + + if (carry_out != 0) { + value[0] ^= 0xe1u; + } + } + + return product; +} + +// GHASH the data padded to whole blocks (NIST SP 800-38D Section 6.4) +auto ghash(const Block &key, const std::string_view data, Block accumulator) + -> Block { + for (std::size_t offset = 0; offset < data.size(); offset += 16) { + for (std::size_t index = 0; index < 16 && offset + index < data.size(); + ++index) { + accumulator[index] ^= static_cast(data[offset + index]); + } + + accumulator = gf_multiply(accumulator, key); + } + + return accumulator; +} + +struct Context { + RoundKeys round_keys; + Block hash_key; + Block counter_zero; + Block tag_mask; +}; + +auto make_context(const std::string_view key, const std::string_view nonce) + -> Context { + Context context{.round_keys = expand_key(key), + .hash_key = {}, + .counter_zero = {}, + .tag_mask = {}}; + context.hash_key = encrypt_block(context.round_keys, Block{}); + for (std::size_t index = 0; index < 12; ++index) { + context.counter_zero[index] = static_cast(nonce[index]); + } + + // The 96-bit nonce yields the pre-counter block nonce || 0x00000001 + context.counter_zero[15] = 1; + context.tag_mask = encrypt_block(context.round_keys, context.counter_zero); + return context; +} + +auto increment_counter(Block &counter) -> void { + for (std::size_t index = 16; index-- > 12;) { + if (++counter[index] != 0) { + break; + } + } +} + +// Counter-mode keystream applied to the input, symmetric for both directions +auto counter_mode(const Context &context, const std::string_view input) + -> std::string { + std::string output(input.size(), '\x00'); + Block counter{context.counter_zero}; + for (std::size_t offset = 0; offset < input.size(); offset += 16) { + increment_counter(counter); + const auto keystream{encrypt_block(context.round_keys, counter)}; + for (std::size_t index = 0; index < 16 && offset + index < input.size(); + ++index) { + output[offset + index] = static_cast( + static_cast(input[offset + index]) ^ keystream[index]); + } + } + + return output; +} + +// The authentication tag over an empty associated data and the ciphertext +auto compute_tag(const Context &context, const std::string_view ciphertext) + -> Block { + auto accumulator{ghash(context.hash_key, ciphertext, Block{})}; + Block lengths{}; + const auto ciphertext_bits{static_cast(ciphertext.size()) * 8}; + for (std::size_t index = 0; index < 8; ++index) { + lengths[15 - index] = + static_cast((ciphertext_bits >> (8 * index)) & 0xffu); + } + + for (std::size_t index = 0; index < 16; ++index) { + accumulator[index] ^= lengths[index]; + } + + accumulator = gf_multiply(accumulator, context.hash_key); + Block tag{}; + for (std::size_t index = 0; index < 16; ++index) { + tag[index] = accumulator[index] ^ context.tag_mask[index]; + } + + return tag; +} + +} // namespace + +auto aes_256_gcm_encrypt(const std::string_view key, + const std::string_view nonce, + const std::string_view plaintext) + -> std::optional { + const auto context{make_context(key, nonce)}; + std::string ciphertext{counter_mode(context, plaintext)}; + const auto tag{compute_tag(context, ciphertext)}; + ciphertext.append(reinterpret_cast(tag.data()), tag.size()); + return ciphertext; +} + +auto aes_256_gcm_decrypt(const std::string_view key, + const std::string_view nonce, + const std::string_view ciphertext) + -> std::optional { + if (ciphertext.size() < 16) { + return std::nullopt; + } + + const auto message{ciphertext.substr(0, ciphertext.size() - 16)}; + const auto received_tag{ciphertext.substr(ciphertext.size() - 16)}; + const auto context{make_context(key, nonce)}; + const auto tag{compute_tag(context, message)}; + + std::uint8_t difference{0}; + for (std::size_t index = 0; index < 16; ++index) { + difference = static_cast( + difference | + (tag[index] ^ static_cast(received_tag[index]))); + } + + if (difference != 0) { + return std::nullopt; + } + + return counter_mode(context, message); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_aes_gcm_windows.cc b/vendor/core/src/core/crypto/crypto_aes_gcm_windows.cc new file mode 100644 index 000000000..a8e8bf9c2 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_aes_gcm_windows.cc @@ -0,0 +1,127 @@ +#include "crypto_aes.h" + +#include // ULONG, PUCHAR +// clang-format off +#include // BCrypt*, BCRYPT_* +// clang-format on + +#include // std::size_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move + +namespace { +constexpr std::size_t TAG_BYTES{16}; + +auto as_buffer(const std::string_view value) -> PUCHAR { + return reinterpret_cast(const_cast( + value.data())); // NOLINT(cppcoreguidelines-pro-type-const-cast) +} + +auto as_buffer(std::string &value) -> PUCHAR { + return reinterpret_cast(value.data()); +} + +// The chaining mode is a wide string constant rather than caller-owned bytes +auto as_buffer(const wchar_t *const value) -> PUCHAR { + return reinterpret_cast(const_cast( + value)); // NOLINT(cppcoreguidelines-pro-type-const-cast) +} +} // namespace + +namespace sourcemeta::core { + +auto aes_256_gcm_encrypt(const std::string_view key, + const std::string_view nonce, + const std::string_view plaintext) + -> std::optional { + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( + &algorithm, BCRYPT_AES_ALGORITHM, nullptr, 0))) { + return std::nullopt; + } + + std::optional result; + BCRYPT_KEY_HANDLE key_handle{nullptr}; + if (BCRYPT_SUCCESS(BCryptSetProperty(algorithm, BCRYPT_CHAINING_MODE, + as_buffer(BCRYPT_CHAIN_MODE_GCM), + sizeof(BCRYPT_CHAIN_MODE_GCM), 0)) && + BCRYPT_SUCCESS(BCryptGenerateSymmetricKey( + algorithm, &key_handle, nullptr, 0, as_buffer(key), + static_cast(key.size()), 0))) { + BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO info; + BCRYPT_INIT_AUTH_MODE_INFO(info); + info.pbNonce = as_buffer(nonce); + info.cbNonce = static_cast(nonce.size()); + std::string tag(TAG_BYTES, '\x00'); + info.pbTag = as_buffer(tag); + info.cbTag = static_cast(TAG_BYTES); + + std::string ciphertext(plaintext.size(), '\x00'); + ULONG written{0}; + if (BCRYPT_SUCCESS(BCryptEncrypt(key_handle, as_buffer(plaintext), + static_cast(plaintext.size()), + &info, nullptr, 0, as_buffer(ciphertext), + static_cast(ciphertext.size()), + &written, 0))) { + ciphertext.append(tag); + result = std::move(ciphertext); + } + + BCryptDestroyKey(key_handle); + } + + BCryptCloseAlgorithmProvider(algorithm, 0); + return result; +} + +auto aes_256_gcm_decrypt(const std::string_view key, + const std::string_view nonce, + const std::string_view ciphertext) + -> std::optional { + if (ciphertext.size() < TAG_BYTES) { + return std::nullopt; + } + + const auto message{ciphertext.substr(0, ciphertext.size() - TAG_BYTES)}; + auto tag{std::string{ciphertext.substr(ciphertext.size() - TAG_BYTES)}}; + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( + &algorithm, BCRYPT_AES_ALGORITHM, nullptr, 0))) { + return std::nullopt; + } + + std::optional result; + BCRYPT_KEY_HANDLE key_handle{nullptr}; + if (BCRYPT_SUCCESS(BCryptSetProperty(algorithm, BCRYPT_CHAINING_MODE, + as_buffer(BCRYPT_CHAIN_MODE_GCM), + sizeof(BCRYPT_CHAIN_MODE_GCM), 0)) && + BCRYPT_SUCCESS(BCryptGenerateSymmetricKey( + algorithm, &key_handle, nullptr, 0, as_buffer(key), + static_cast(key.size()), 0))) { + BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO info; + BCRYPT_INIT_AUTH_MODE_INFO(info); + info.pbNonce = as_buffer(nonce); + info.cbNonce = static_cast(nonce.size()); + info.pbTag = as_buffer(tag); + info.cbTag = static_cast(TAG_BYTES); + + std::string plaintext(message.size(), '\x00'); + ULONG written{0}; + // Reports a tag mismatch through the status, so a tampered message fails + if (BCRYPT_SUCCESS(BCryptDecrypt( + key_handle, as_buffer(message), static_cast(message.size()), + &info, nullptr, 0, as_buffer(plaintext), + static_cast(plaintext.size()), &written, 0))) { + result = std::move(plaintext); + } + + BCryptDestroyKey(key_handle); + } + + BCryptCloseAlgorithmProvider(algorithm, 0); + return result; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_bignum.h b/vendor/core/src/core/crypto/crypto_bignum.h index 5244bc253..932e779cb 100644 --- a/vendor/core/src/core/crypto/crypto_bignum.h +++ b/vendor/core/src/core/crypto/crypto_bignum.h @@ -1,16 +1,21 @@ #ifndef SOURCEMETA_CORE_CRYPTO_BIGNUM_H_ #define SOURCEMETA_CORE_CRYPTO_BIGNUM_H_ -// Fixed-capacity unsigned big integer arithmetic for the reference -// signature verification backend. Capacity fits 4096-bit RSA operands -// and their double-width products. Constant-time execution is not -// required, since verification consumes only public inputs +// Fixed-capacity unsigned big integer arithmetic for the reference signature +// backend. Capacity fits 4096-bit RSA operands and their double-width products. +// The verification paths consume only public inputs and stay variable time; the +// signing paths use the constant-time layer below (fixed-width multiply, +// Barrett reduction, masked select and inverse) on their secret operands. Only +// the Barrett context precompute stays variable time, and it touches the public +// modulus alone #include +#include #include // std::array #include // std::size_t #include // std::uint8_t, std::uint64_t +#include // std::optional #include // std::string #include // std::string_view @@ -25,6 +30,32 @@ struct Bignum { std::size_t size{0}; }; +// Overwrite the whole word buffer of a big integer that held secret material, +// including the words past the current size that intermediate operations wrote, +// so it does not linger in freed memory. The volatile access stops the compiler +// from eliding the write as a dead store +inline auto secure_zero(Bignum &value) noexcept -> void { + auto *pointer{reinterpret_cast(value.words.data())}; + for (std::size_t index{0}; index < sizeof(value.words); index += 1) { + pointer[index] = 0; + } + + value.size = 0; +} + +// Overwrite the referenced big integer when leaving the current scope, so a +// secret value a local holds is wiped across every return path without +// threading a manual call through each one +struct SecureBignumScope { + explicit SecureBignumScope(Bignum &value) noexcept : target{value} {} + SecureBignumScope(const SecureBignumScope &) = delete; + auto operator=(const SecureBignumScope &) -> SecureBignumScope & = delete; + SecureBignumScope(SecureBignumScope &&) = delete; + auto operator=(SecureBignumScope &&) -> SecureBignumScope & = delete; + ~SecureBignumScope() { secure_zero(this->target); } + Bignum ⌖ +}; + inline auto bignum_normalize(Bignum &value) noexcept -> void { while (value.size > 0 && value.words[value.size - 1] == 0) { value.size -= 1; @@ -62,33 +93,13 @@ inline auto bignum_from_u64(const std::uint64_t value) noexcept -> Bignum { } inline auto bignum_from_hex(const std::string_view hex) -> Bignum { - const auto nibble{[](const char character) noexcept -> std::uint8_t { - if (character >= '0' && character <= '9') { - return static_cast(character - '0'); - } else if (character >= 'a' && character <= 'f') { - return static_cast(character - 'a' + 10); - } else { - return static_cast(character - 'A' + 10); - } - }}; - - std::string bytes; - bytes.reserve((hex.size() + 1) / 2); - - // An odd length means the leading nibble forms a byte on its own, as if a - // zero had been prepended - std::size_t index{0}; - if (hex.size() % 2 != 0) { - bytes.push_back(static_cast(nibble(hex[0]))); - index = 1; + // An odd length is decoded as if a zero nibble had been prepended + const auto bytes{hex_to_bytes(hex, true)}; + if (!bytes.has_value()) { + return Bignum{}; } - for (; index + 1 < hex.size(); index += 2) { - bytes.push_back( - static_cast((nibble(hex[index]) << 4u) | nibble(hex[index + 1]))); - } - - return bignum_from_bytes(bytes); + return bignum_from_bytes(bytes.value()); } inline auto bignum_is_zero(const Bignum &value) noexcept -> bool { @@ -135,6 +146,16 @@ inline auto bignum_get_bit(const Bignum &value, const std::size_t bit) noexcept return ((value.words[word] >> (bit % 64)) & 1u) != 0; } +// The same read without the size-dependent early return, so the signing ladders +// do not reveal the secret scalar's length through the branch. The bit index is +// bounded by the public curve size, so the guard is on public data +inline auto bignum_get_bit_fixed(const Bignum &value, + const std::size_t bit) noexcept -> bool { + const auto word{bit / 64}; + return word < Bignum::capacity && + ((value.words[word] >> (bit % 64)) & 1u) != 0; +} + // Assumes the result fits in the capacity inline auto bignum_shift_left(const Bignum &value, const std::size_t bits) noexcept -> Bignum { @@ -485,6 +506,277 @@ inline auto bignum_mod_inverse(const Bignum &value, : second_coefficient; } +// A branch-free select, so a secret condition does not steer control flow. The +// whole capacity is blended so the running size does not leak the condition +inline auto bignum_conditional_select(const bool condition, + const Bignum &when_true, + const Bignum &when_false) noexcept + -> Bignum { + const std::uint64_t mask{std::uint64_t{0} - + static_cast(condition)}; + Bignum result; + for (std::size_t index = 0; index < Bignum::capacity; ++index) { + result.words[index] = + (when_true.words[index] & mask) | (when_false.words[index] & ~mask); + } + + const std::size_t size_mask{std::size_t{0} - + static_cast(condition)}; + result.size = (when_true.size & size_mask) | (when_false.size & ~size_mask); + return result; +} + +// Fixed-width subtraction over the given number of words, returning the final +// borrow. It always visits every word, so its timing does not depend on where +// the operands' significant words fall the way the size-driven routines above +// do +inline auto bignum_subtract_fixed(const Bignum &left, const Bignum &right, + const std::size_t words, Bignum &out) noexcept + -> std::uint64_t { + std::uint64_t borrow{0}; + for (std::size_t index = 0; index < words; ++index) { + const auto left_word{left.words[index]}; + const auto right_word{right.words[index]}; + const auto without_right{left_word - right_word}; + const std::uint64_t borrow_from_right{left_word < right_word ? 1u : 0u}; + const auto result_word{without_right - borrow}; + const std::uint64_t borrow_from_previous{without_right < borrow ? 1u : 0u}; + out.words[index] = result_word; + borrow = borrow_from_right | borrow_from_previous; + } + + out.size = words; + return borrow; +} + +// Multiply visiting exactly the given word counts, so timing does not reveal +// where either operand's significant words fall +inline auto bignum_multiply_fixed(const Bignum &left, const Bignum &right, + const std::size_t left_words, + const std::size_t right_words) noexcept + -> Bignum { + Bignum result; + for (std::size_t left_index = 0; left_index < left_words; ++left_index) { + std::uint64_t carry{0}; + for (std::size_t right_index = 0; right_index < right_words; + ++right_index) { + const auto destination{left_index + right_index}; + const auto product{static_cast(left.words[left_index]) * + right.words[right_index] + + result.words[destination] + carry}; + result.words[destination] = static_cast(product); + carry = static_cast(product >> 64u); + } + + result.words[left_index + right_words] = carry; + } + + result.size = left_words + right_words; + return result; +} + +// A word-granular right shift dropping the low words, and its counterpart that +// keeps them, for the word-aligned truncations the Barrett reduction needs +inline auto bignum_drop_low_words(const Bignum &value, + const std::size_t words) noexcept -> Bignum { + Bignum result; + for (std::size_t index = 0; index + words < Bignum::capacity; ++index) { + result.words[index] = value.words[index + words]; + } + + result.size = value.size > words ? value.size - words : 0; + return result; +} + +inline auto bignum_keep_low_words(const Bignum &value, + const std::size_t words) noexcept -> Bignum { + Bignum result; + for (std::size_t index = 0; index < words; ++index) { + result.words[index] = value.words[index]; + } + + result.size = words; + return result; +} + +// Quotient of a division, for building a public reduction constant only, so a +// plain bit-by-bit long division is enough +inline auto bignum_divide(const Bignum &numerator, + const Bignum &denominator) noexcept -> Bignum { + Bignum quotient; + Bignum remainder; + const auto bits{bignum_bit_length(numerator)}; + for (std::size_t index = bits; index > 0; --index) { + remainder = bignum_shift_left(remainder, 1); + if (bignum_get_bit(numerator, index - 1)) { + remainder.words[0] |= 1u; + if (remainder.size == 0) { + remainder.size = 1; + } + } + + if (bignum_compare(remainder, denominator) >= 0) { + bignum_subtract_in_place(remainder, denominator); + quotient.words[(index - 1) / 64] |= std::uint64_t{1} + << ((index - 1) % 64); + if (quotient.size < ((index - 1) / 64) + 1) { + quotient.size = ((index - 1) / 64) + 1; + } + } + } + + return quotient; +} + +// Precomputed constants for Barrett reduction modulo a fixed modulus. Built +// from the public modulus alone, so the setup itself need not be constant time +struct BarrettContext { + Bignum modulus; + std::size_t words; + Bignum factor; +}; + +inline auto barrett_context(const Bignum &modulus) noexcept -> BarrettContext { + BarrettContext context; + context.modulus = modulus; + context.words = modulus.size; + const auto power{bignum_shift_left(bignum_from_u64(1), 128 * context.words)}; + context.factor = bignum_divide(power, modulus); + return context; +} + +// If the value is at least the modulus over the given width, subtract it. The +// choice is a masked select rather than a branch +inline auto bignum_conditional_subtract(const Bignum &value, + const Bignum &modulus, + const std::size_t words) noexcept + -> Bignum { + Bignum reduced; + const auto borrow{bignum_subtract_fixed(value, modulus, words, reduced)}; + return bignum_conditional_select(borrow == 0, reduced, value); +} + +// Reduce a value below the square of the modulus down to the modulus in +// constant time (Handbook of Applied Cryptography Algorithm 14.42). The +// estimate is at most two too small, so three masked subtractions always finish +// the reduction +inline auto barrett_reduce(const Bignum &value, + const BarrettContext &context) noexcept -> Bignum { + const auto width{context.words}; + const auto high{bignum_drop_low_words(value, width - 1)}; + const auto estimate{ + bignum_multiply_fixed(high, context.factor, width + 1, width + 1)}; + const auto quotient{bignum_drop_low_words(estimate, width + 1)}; + const auto value_low{bignum_keep_low_words(value, width + 1)}; + const auto product{ + bignum_multiply_fixed(quotient, context.modulus, width + 1, width)}; + const auto product_low{bignum_keep_low_words(product, width + 1)}; + Bignum remainder; + bignum_subtract_fixed(value_low, product_low, width + 1, remainder); + remainder = + bignum_conditional_subtract(remainder, context.modulus, width + 1); + remainder = + bignum_conditional_subtract(remainder, context.modulus, width + 1); + remainder = + bignum_conditional_subtract(remainder, context.modulus, width + 1); + remainder.size = width; + return remainder; +} + +inline auto field_mod_multiply_ct(const Bignum &left, const Bignum &right, + const BarrettContext &context) noexcept + -> Bignum { + return barrett_reduce( + bignum_multiply_fixed(left, right, context.words, context.words), + context); +} + +inline auto field_add_ct(const Bignum &left, const Bignum &right, + const BarrettContext &context) noexcept -> Bignum { + const auto width{context.words}; + Bignum sum; + std::uint64_t carry{0}; + for (std::size_t index = 0; index < width; ++index) { + const auto total{static_cast(left.words[index]) + + right.words[index] + carry}; + sum.words[index] = static_cast(total); + carry = static_cast(total >> 64u); + } + + sum.words[width] = carry; + sum.size = width + 1; + auto reduced{bignum_conditional_subtract(sum, context.modulus, width + 1)}; + reduced.size = width; + return reduced; +} + +inline auto field_subtract_ct(const Bignum &left, const Bignum &right, + const BarrettContext &context) noexcept + -> Bignum { + const auto width{context.words}; + Bignum difference; + const auto borrow{bignum_subtract_fixed(left, right, width, difference)}; + Bignum wrapped; + std::uint64_t carry{0}; + for (std::size_t index = 0; index < width; ++index) { + const auto total{static_cast(difference.words[index]) + + context.modulus.words[index] + carry}; + wrapped.words[index] = static_cast(total); + carry = static_cast(total >> 64u); + } + + wrapped.size = width; + auto result{bignum_conditional_select(borrow != 0, wrapped, difference)}; + result.size = width; + return result; +} + +// Fermat inverse over the field in constant time. The exponent is the public +// modulus minus two, so its bit pattern reveals nothing secret, and the field +// multiplications underneath do not depend on the value being inverted. The +// modulus must be prime +inline auto field_inverse_ct(const Bignum &value, + const BarrettContext &context) noexcept -> Bignum { + auto exponent{context.modulus}; + bignum_subtract_in_place(exponent, bignum_from_u64(2)); + Bignum result; + result.words[0] = 1; + result.size = context.words; + const auto base{barrett_reduce(value, context)}; + const auto exponent_bits{bignum_bit_length(exponent)}; + for (std::size_t index = exponent_bits; index > 0; --index) { + result = field_mod_multiply_ct(result, result, context); + if (bignum_get_bit(exponent, index - 1)) { + result = field_mod_multiply_ct(result, base, context); + } + } + + return result; +} + +// Modular exponentiation for a secret exponent (the RSA private key), in +// constant time. Unlike the inverse above, the exponent is secret, so the +// ladder runs a fixed number of steps fixed by the public modulus and blends +// the per-bit multiply with a masked select rather than a branch. The modulus +// need not be prime +inline auto bignum_mod_exp_ct(const Bignum &base, const Bignum &exponent, + const BarrettContext &context) noexcept + -> Bignum { + Bignum result; + result.words[0] = 1; + result.size = context.words; + const auto reduced_base{barrett_reduce(base, context)}; + const auto exponent_bits{bignum_bit_length(context.modulus)}; + for (std::size_t index = exponent_bits; index > 0; --index) { + result = field_mod_multiply_ct(result, result, context); + const auto product{field_mod_multiply_ct(result, reduced_base, context)}; + result = bignum_conditional_select( + bignum_get_bit_fixed(exponent, index - 1), product, result); + } + + return result; +} + inline auto bignum_to_bytes(const Bignum &value, const std::size_t length) -> std::string { std::string result(length, '\x00'); diff --git a/vendor/core/src/core/crypto/crypto_der.h b/vendor/core/src/core/crypto/crypto_der.h new file mode 100644 index 000000000..ba47bee01 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_der.h @@ -0,0 +1,121 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_DER_H_ +#define SOURCEMETA_CORE_CRYPTO_DER_H_ + +// Minimal DER (ITU-T X.690 distinguished encoding rules) reading and building +// primitives shared by the key parsing and native key construction paths + +#include // std::size_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +// A single parsed DER type-length-value element, along with the bytes that +// follow it within the same sequence +struct DERElement { + unsigned char tag; + std::string_view content; + std::string_view rest; +}; + +// Read one DER element from the front of the input, following the definite +// length encoding of ITU-T X.690 +inline auto der_read(const std::string_view input) + -> std::optional { + if (input.size() < 2) { + return std::nullopt; + } + + const auto tag{static_cast(input[0])}; + std::size_t position{1}; + const auto first{static_cast(input[position])}; + position += 1; + std::size_t length{0}; + if (first < 0x80) { + length = first; + } else { + const std::size_t count{first & 0x7fu}; + if (count == 0 || count > 4 || position + count > input.size()) { + return std::nullopt; + } + + // The distinguished encoding rules require the minimal number of length + // octets (ITU-T X.690 Section 10.1), so a leading zero octet is not + // canonical + if (static_cast(input[position]) == 0) { + return std::nullopt; + } + + for (std::size_t index{0}; index < count; ++index) { + length = (length << 8u) | static_cast(input[position]); + position += 1; + } + + // A value below the short-form limit must use the short form + if (length < 0x80) { + return std::nullopt; + } + } + + // Written so that the addition cannot overflow, as position never exceeds + // the input size at this point + if (length > input.size() - position) { + return std::nullopt; + } + + return DERElement{.tag = tag, + .content = input.substr(position, length), + .rest = input.substr(position + length)}; +} + +// Append the definite-form length octets of a DER element, using the minimal +// number of long-form octets for any length (ITU-T X.690 Section 10.1) +inline auto der_append_length(std::string &output, const std::size_t length) + -> void { + if (length < 128) { + output.push_back(static_cast(length)); + return; + } + + std::string octets; + for (std::size_t remaining{length}; remaining > 0; remaining >>= 8u) { + octets.insert(octets.begin(), static_cast(remaining & 0xffu)); + } + + output.push_back(static_cast(0x80u | octets.size())); + output.append(octets); +} + +// Append a complete DER element, wrapping the content in its tag and length +inline auto der_append_element(std::string &output, const unsigned char tag, + const std::string_view content) -> void { + output.push_back(static_cast(tag)); + der_append_length(output, content.size()); + output.append(content); +} + +// Append a DER INTEGER holding the big-endian unsigned value +inline auto der_append_unsigned_integer(std::string &output, + std::string_view value) -> void { + while (!value.empty() && value.front() == '\x00') { + value.remove_prefix(1); + } + + // A leading zero byte keeps the value positive when its high bit is set, + // and represents the value zero when nothing remains + const auto needs_zero_prefix{ + value.empty() || + (static_cast(value.front()) & 0x80u) != 0}; + output.push_back('\x02'); + der_append_length(output, value.size() + (needs_zero_prefix ? 1 : 0)); + if (needs_zero_prefix) { + output.push_back('\x00'); + } + + output.append(value); +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/crypto_ecc.h b/vendor/core/src/core/crypto/crypto_ecc.h index 2a4de5e0d..ea361c59d 100644 --- a/vendor/core/src/core/crypto/crypto_ecc.h +++ b/vendor/core/src/core/crypto/crypto_ecc.h @@ -1,11 +1,13 @@ #ifndef SOURCEMETA_CORE_CRYPTO_ECC_H_ #define SOURCEMETA_CORE_CRYPTO_ECC_H_ -// Short Weierstrass elliptic curve arithmetic over the NIST prime curves -// for the reference signature verification backend. Points are kept in -// Jacobian coordinates so that scalar multiplication needs a single modular -// inversion at the end rather than one per step. Constant time execution is -// not required, since verification consumes only public inputs +// Short Weierstrass elliptic curve arithmetic over the NIST prime curves for +// the reference signature backend. Verification keeps points in Jacobian +// coordinates so that scalar multiplication needs a single modular inversion at +// the end, and consumes only public inputs, so it stays variable time. Signing +// uses the constant-time scalar multiplication below, a fixed-length ladder +// over the complete projective formula evaluated with the constant-time field +// layer, so it does not depend on the secret scalar #include "crypto_bignum.h" @@ -504,6 +506,102 @@ inline auto point_affine_x(const JacobianPoint &point, return field_mod_multiply(point.x, z_inverse_squared, curve); } +inline auto point_conditional_select(const bool condition, + const JacobianPoint &when_true, + const JacobianPoint &when_false) noexcept + -> JacobianPoint { + return {.x = bignum_conditional_select(condition, when_true.x, when_false.x), + .y = bignum_conditional_select(condition, when_true.y, when_false.y), + .z = bignum_conditional_select(condition, when_true.z, when_false.z)}; +} + +// Complete projective point addition for the prime-order NIST curves, whose +// coefficient a is negative three (Renes, Costello, and Batina 2016, Algorithm +// 4). It has no exceptional cases, so the signing ladder needs no +// secret-dependent branch, and it also doubles, so one routine serves both +// ladder steps. Coordinates here are projective, so the affine point is X / Z +inline auto point_complete_add(const JacobianPoint &left, + const JacobianPoint &right, + const Bignum &coefficient_b, + const BarrettContext &field) noexcept + -> JacobianPoint { + auto t0{field_mod_multiply_ct(left.x, right.x, field)}; + auto t1{field_mod_multiply_ct(left.y, right.y, field)}; + auto t2{field_mod_multiply_ct(left.z, right.z, field)}; + auto t3{field_add_ct(left.x, left.y, field)}; + auto t4{field_add_ct(right.x, right.y, field)}; + t3 = field_mod_multiply_ct(t3, t4, field); + t4 = field_add_ct(t0, t1, field); + t3 = field_subtract_ct(t3, t4, field); + t4 = field_add_ct(left.y, left.z, field); + auto x3{field_add_ct(right.y, right.z, field)}; + t4 = field_mod_multiply_ct(t4, x3, field); + x3 = field_add_ct(t1, t2, field); + t4 = field_subtract_ct(t4, x3, field); + x3 = field_add_ct(left.x, left.z, field); + auto y3{field_add_ct(right.x, right.z, field)}; + x3 = field_mod_multiply_ct(x3, y3, field); + y3 = field_add_ct(t0, t2, field); + y3 = field_subtract_ct(x3, y3, field); + auto z3{field_mod_multiply_ct(coefficient_b, t2, field)}; + x3 = field_subtract_ct(y3, z3, field); + z3 = field_add_ct(x3, x3, field); + x3 = field_add_ct(x3, z3, field); + z3 = field_subtract_ct(t1, x3, field); + x3 = field_add_ct(t1, x3, field); + y3 = field_mod_multiply_ct(coefficient_b, y3, field); + t1 = field_add_ct(t2, t2, field); + t2 = field_add_ct(t1, t2, field); + y3 = field_subtract_ct(y3, t2, field); + y3 = field_subtract_ct(y3, t0, field); + t1 = field_add_ct(y3, y3, field); + y3 = field_add_ct(t1, y3, field); + t1 = field_add_ct(t0, t0, field); + t0 = field_add_ct(t1, t0, field); + t0 = field_subtract_ct(t0, t2, field); + t1 = field_mod_multiply_ct(t4, y3, field); + t2 = field_mod_multiply_ct(t0, y3, field); + y3 = field_mod_multiply_ct(x3, z3, field); + y3 = field_add_ct(y3, t2, field); + x3 = field_mod_multiply_ct(t3, x3, field); + x3 = field_subtract_ct(x3, t1, field); + z3 = field_mod_multiply_ct(t4, z3, field); + t1 = field_mod_multiply_ct(t3, t0, field); + z3 = field_add_ct(z3, t1, field); + return {.x = x3, .y = y3, .z = z3}; +} + +// For the signing path, where the scalar is the secret nonce: a fixed-length +// double-and-add-always ladder over the complete formula with a masked +// selection, so neither the per-bit branch nor the field arithmetic underneath +// depends on the scalar. The input point and the result are projective +inline auto point_scalar_multiply_constant_time( + const Bignum &scalar, const JacobianPoint &point, + const EllipticCurveParameters &curve) -> JacobianPoint { + const auto field{barrett_context(curve.prime)}; + JacobianPoint result{.x = Bignum{}, .y = bignum_from_u64(1), .z = Bignum{}}; + const auto scalar_bits{bignum_bit_length(curve.order)}; + for (std::size_t index = scalar_bits; index > 0; --index) { + result = point_complete_add(result, result, curve.coefficient_b, field); + const auto sum{ + point_complete_add(result, point, curve.coefficient_b, field)}; + result = point_conditional_select(bignum_get_bit_fixed(scalar, index - 1), + sum, result); + } + + return result; +} + +inline auto point_affine_x_constant_time(const JacobianPoint &point, + const EllipticCurveParameters &curve) + -> Bignum { + const auto field{barrett_context(curve.prime)}; + const auto z_inverse{field_inverse_ct(point.z, field)}; + auto result{field_mod_multiply_ct(point.x, z_inverse, field)}; + bignum_normalize(result); + return result; +} + // Whether the affine point satisfies y^2 = x^3 + a*x + b (mod p) inline auto point_on_curve(const Bignum &x, const Bignum &y, const EllipticCurveParameters &curve) -> bool { diff --git a/vendor/core/src/core/crypto/crypto_eddsa.h b/vendor/core/src/core/crypto/crypto_eddsa.h index 1ff0da48a..81ae154ce 100644 --- a/vendor/core/src/core/crypto/crypto_eddsa.h +++ b/vendor/core/src/core/crypto/crypto_eddsa.h @@ -1,15 +1,18 @@ #ifndef SOURCEMETA_CORE_CRYPTO_EDDSA_H_ #define SOURCEMETA_CORE_CRYPTO_EDDSA_H_ -// Edwards-curve signature verification (Ed25519 and Ed448, RFC 8032 Section 5, -// the pure variants) for the backends without a native EdDSA primitive. Points -// are kept in extended Edwards coordinates, so that the group law is a single -// set of complete formulas shared by both curves. Constant time execution is -// not required, since verification consumes only public inputs +// Edwards-curve signatures (Ed25519 and Ed448, RFC 8032 Section 5, the pure +// variants) for the backends without a native EdDSA primitive. Points are kept +// in extended Edwards coordinates, so that the group law is a single set of +// complete formulas shared by both curves. Verification consumes only public +// inputs and stays variable time; the signing paths use the constant-time +// scalar multiplication, inverse, and encoding below, evaluated with the +// constant-time field layer, so they do not depend on the secret scalar #include #include "crypto_bignum.h" +#include "crypto_helpers.h" #include "crypto_shake256.h" #include // std::size_t @@ -95,6 +98,66 @@ inline auto edwards_point_scalar_multiply(const Bignum &scalar, return result; } +inline auto edwards_point_conditional_select( + const bool condition, const EdwardsPoint &when_true, + const EdwardsPoint &when_false) noexcept -> EdwardsPoint { + return {.x = bignum_conditional_select(condition, when_true.x, when_false.x), + .y = bignum_conditional_select(condition, when_true.y, when_false.y), + .z = bignum_conditional_select(condition, when_true.z, when_false.z), + .t = bignum_conditional_select(condition, when_true.t, when_false.t)}; +} + +// The same complete addition as above evaluated over the constant-time field +// arithmetic, for the signing path where the operands derive from the secret +// scalar +inline auto edwards_point_add_constant_time( + const EdwardsPoint &left, const EdwardsPoint &right, + const EdwardsParameters ¶meters, const BarrettContext &field) noexcept + -> EdwardsPoint { + const auto a{field_mod_multiply_ct(left.x, right.x, field)}; + const auto b{field_mod_multiply_ct(left.y, right.y, field)}; + const auto c{field_mod_multiply_ct( + field_mod_multiply_ct(parameters.coefficient_d, left.t, field), right.t, + field)}; + const auto d{field_mod_multiply_ct(left.z, right.z, field)}; + const auto e{field_subtract_ct( + field_mod_multiply_ct(field_add_ct(left.x, left.y, field), + field_add_ct(right.x, right.y, field), field), + field_add_ct(a, b, field), field)}; + const auto f{field_subtract_ct(d, c, field)}; + const auto g{field_add_ct(d, c, field)}; + const auto h{field_subtract_ct( + b, field_mod_multiply_ct(parameters.coefficient_a, a, field), field)}; + return EdwardsPoint{.x = field_mod_multiply_ct(e, f, field), + .y = field_mod_multiply_ct(g, h, field), + .z = field_mod_multiply_ct(f, g, field), + .t = field_mod_multiply_ct(e, h, field)}; +} + +// For the signing path, where the scalar is secret: a fixed-length +// double-and-add-always ladder with a masked selection over the complete +// Edwards formulas evaluated in constant time, so neither the per-bit branch +// nor the field arithmetic underneath depends on the scalar +inline auto edwards_point_scalar_multiply_constant_time( + const Bignum &scalar, const EdwardsPoint &point, + const EdwardsParameters ¶meters) -> EdwardsPoint { + const auto field{barrett_context(parameters.prime)}; + EdwardsPoint result{.x = Bignum{}, + .y = bignum_from_u64(1), + .z = bignum_from_u64(1), + .t = Bignum{}}; + const auto scalar_bits{bignum_bit_length(parameters.prime)}; + for (std::size_t index = scalar_bits; index > 0; --index) { + result = edwards_point_add_constant_time(result, result, parameters, field); + const auto sum{ + edwards_point_add_constant_time(result, point, parameters, field)}; + result = edwards_point_conditional_select( + bignum_get_bit_fixed(scalar, index - 1), sum, result); + } + + return result; +} + // Whether two points are equal, compared without leaving projective space by // cross-multiplying through the Z factors inline auto edwards_point_equal(const EdwardsPoint &left, @@ -106,6 +169,26 @@ inline auto edwards_point_equal(const EdwardsPoint &left, bignum_mod_multiply(right.y, left.z, prime)) == 0; } +// Encode a point into the little-endian y coordinate with the low bit of x in +// the final bit (RFC 8032 Section 5.1.2), the inverse of the point decoding +inline auto edwards_point_encode(const EdwardsPoint &point, const Bignum &prime, + const std::size_t length) -> std::string { + // Only the signing path encodes points, and its projective z derives from the + // secret scalar, so the coordinate recovery is taken in constant time + const auto field{barrett_context(prime)}; + const auto z_inverse{field_inverse_ct(point.z, field)}; + const auto x{field_mod_multiply_ct(point.x, z_inverse, field)}; + const auto y{field_mod_multiply_ct(point.y, z_inverse, field)}; + const auto big_endian{bignum_to_bytes(y, length)}; + std::string encoding{big_endian.rbegin(), big_endian.rend()}; + if (bignum_get_bit(x, 0)) { + encoding.back() = + static_cast(static_cast(encoding.back()) | 0x80u); + } + + return encoding; +} + // Recover an Ed25519 point from its 32-byte encoding (RFC 8032 Section 5.1.3), // returning no value when the encoding does not name a point on the curve inline auto edwards25519_decode_point(const std::string_view encoding, @@ -287,6 +370,87 @@ inline auto edwards25519_verify(const std::string_view public_key, return edwards_point_equal(left, right, parameters.prime); } +// Produce an Ed25519 signature over a message (RFC 8032 Section 5.1.6), given +// the 32-byte secret seed +inline auto edwards25519_sign(const std::string_view secret, + const std::string_view message) + -> std::optional { + if (secret.size() != 32) { + return std::nullopt; + } + + const auto parameters{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 + // returning + auto hashed{sha512_digest(secret)}; + const SecureBufferScope hashed_scope{hashed.data(), hashed.size()}; + const std::string_view digest{reinterpret_cast(hashed.data()), + hashed.size()}; + + // The secret scalar is the pruned first half, the prefix the second half + std::string scalar_bytes{digest.substr(0, 32)}; + const SecureScope scalar_bytes_scope{scalar_bytes}; + scalar_bytes.front() = static_cast( + static_cast(scalar_bytes.front()) & 0xf8u); + scalar_bytes.back() = static_cast( + (static_cast(scalar_bytes.back()) & 0x7fu) | 0x40u); + auto scalar_a{bignum_from_bytes_little_endian(scalar_bytes)}; + const SecureBignumScope scalar_a_scope{scalar_a}; + const auto prefix{digest.substr(32)}; + + const auto public_key{ + edwards_point_encode(edwards_point_scalar_multiply_constant_time( + scalar_a, parameters.base, parameters), + parameters.prime, 32)}; + + // r = SHA-512(prefix || M) reduced, then R = [r]B + std::string nonce_preimage{prefix}; + const SecureScope nonce_preimage_scope{nonce_preimage}; + nonce_preimage.append(message); + auto nonce_digest{sha512_digest(nonce_preimage)}; + const SecureBufferScope nonce_digest_scope{nonce_digest.data(), + nonce_digest.size()}; + auto scalar_r{bignum_from_bytes_little_endian( + std::string_view{reinterpret_cast(nonce_digest.data()), + nonce_digest.size()})}; + const SecureBignumScope scalar_r_scope{scalar_r}; + bignum_reduce(scalar_r, parameters.order); + const auto encoded_r{ + edwards_point_encode(edwards_point_scalar_multiply_constant_time( + scalar_r, parameters.base, parameters), + parameters.prime, 32)}; + + // k = SHA-512(R || A || M) reduced, then S = (r + k * a) mod L. The k * a + // product carries the secret scalar, so it is wiped; r, k, and the resulting + // S are the public signature material + std::string challenge_preimage{encoded_r}; + challenge_preimage.append(public_key); + challenge_preimage.append(message); + const auto challenge_digest{sha512_digest(challenge_preimage)}; + auto scalar_k{bignum_from_bytes_little_endian( + std::string_view{reinterpret_cast(challenge_digest.data()), + challenge_digest.size()})}; + bignum_reduce(scalar_k, parameters.order); + // The k * a product and its sum with the nonce carry the secret scalar and + // nonce, so both run over the constant-time field arithmetic modulo the + // order; k is public and r, k, and the resulting S are the public signature + // material + const auto order_field{barrett_context(parameters.order)}; + auto scalar_a_reduced{barrett_reduce(scalar_a, order_field)}; + const SecureBignumScope scalar_a_reduced_scope{scalar_a_reduced}; + auto challenge_product{ + field_mod_multiply_ct(scalar_k, scalar_a_reduced, order_field)}; + const SecureBignumScope challenge_product_scope{challenge_product}; + auto scalar_s{field_add_ct(scalar_r, challenge_product, order_field)}; + bignum_normalize(scalar_s); + + const auto scalar_s_big_endian{bignum_to_bytes(scalar_s, 32)}; + std::string signature{encoded_r}; + signature.append(scalar_s_big_endian.rbegin(), scalar_s_big_endian.rend()); + return signature; +} + // Recover an Ed448 point from its 57-byte encoding (RFC 8032 Section 5.2.3), // returning no value when the encoding does not name a point on the curve inline auto edwards448_decode_point(const std::string_view encoding, @@ -443,6 +607,88 @@ inline auto edwards448_verify(const std::string_view public_key, return edwards_point_equal(left, right, parameters.prime); } +// Produce an Ed448 signature over a message (RFC 8032 Section 5.2.6), given the +// 57-byte secret seed +inline auto edwards448_sign(const std::string_view secret, + const std::string_view message) + -> std::optional { + if (secret.size() != 57) { + return std::nullopt; + } + + const auto parameters{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 + // returning + auto digest{shake256(secret, 114)}; + const SecureScope digest_scope{digest}; + + // The secret scalar is the pruned first half, the prefix the second half + std::string scalar_bytes{digest.substr(0, 57)}; + const SecureScope scalar_bytes_scope{scalar_bytes}; + scalar_bytes.front() = static_cast( + static_cast(scalar_bytes.front()) & 0xfcu); + scalar_bytes[55] = + static_cast(static_cast(scalar_bytes[55]) | 0x80u); + scalar_bytes[56] = '\x00'; + auto scalar_a{bignum_from_bytes_little_endian(scalar_bytes)}; + const SecureBignumScope scalar_a_scope{scalar_a}; + const auto prefix{std::string_view{digest}.substr(57)}; + + const auto public_key{ + edwards_point_encode(edwards_point_scalar_multiply_constant_time( + scalar_a, parameters.base, parameters), + parameters.prime, 57)}; + + // dom4 is "SigEd448" followed by the zero pre-hash flag and an empty context + std::string domain{"SigEd448"}; + domain.push_back('\x00'); + domain.push_back('\x00'); + + // r = SHAKE256(dom4 || prefix || M) reduced, then R = [r]B + std::string nonce_preimage{domain}; + const SecureScope nonce_preimage_scope{nonce_preimage}; + nonce_preimage.append(prefix); + nonce_preimage.append(message); + auto nonce_hash{shake256(nonce_preimage, 114)}; + const SecureScope nonce_hash_scope{nonce_hash}; + auto scalar_r{bignum_from_bytes_little_endian(nonce_hash)}; + const SecureBignumScope scalar_r_scope{scalar_r}; + bignum_reduce(scalar_r, parameters.order); + const auto encoded_r{ + edwards_point_encode(edwards_point_scalar_multiply_constant_time( + scalar_r, parameters.base, parameters), + parameters.prime, 57)}; + + // k = SHAKE256(dom4 || R || A || M) reduced, then S = (r + k * a) mod L. The + // k * a product carries the secret scalar, so it is wiped; r, k, and the + // resulting S are the public signature material + std::string challenge_preimage{domain}; + challenge_preimage.append(encoded_r); + challenge_preimage.append(public_key); + challenge_preimage.append(message); + auto scalar_k{ + bignum_from_bytes_little_endian(shake256(challenge_preimage, 114))}; + bignum_reduce(scalar_k, parameters.order); + // The k * a product and its sum with the nonce carry the secret scalar and + // nonce, so both run over the constant-time field arithmetic modulo the + // order; k is public and r, k, and the resulting S are the public signature + // material + const auto order_field{barrett_context(parameters.order)}; + auto scalar_a_reduced{barrett_reduce(scalar_a, order_field)}; + const SecureBignumScope scalar_a_reduced_scope{scalar_a_reduced}; + auto challenge_product{ + field_mod_multiply_ct(scalar_k, scalar_a_reduced, order_field)}; + const SecureBignumScope challenge_product_scope{challenge_product}; + auto scalar_s{field_add_ct(scalar_r, challenge_product, order_field)}; + bignum_normalize(scalar_s); + + const auto scalar_s_big_endian{bignum_to_bytes(scalar_s, 57)}; + std::string signature{encoded_r}; + signature.append(scalar_s_big_endian.rbegin(), scalar_s_big_endian.rend()); + return signature; +} + } // namespace sourcemeta::core #endif diff --git a/vendor/core/src/core/crypto/crypto_eddsa_apple.h b/vendor/core/src/core/crypto/crypto_eddsa_apple.h index 7426649a4..063bfd15a 100644 --- a/vendor/core/src/core/crypto/crypto_eddsa_apple.h +++ b/vendor/core/src/core/crypto/crypto_eddsa_apple.h @@ -12,4 +12,11 @@ extern "C" auto sourcemeta_core_eddsa_ed25519_verify_cryptokit( const unsigned char *message, std::size_t message_size, const unsigned char *signature, std::size_t signature_size) -> bool; +// Produce an Ed25519 signature through CryptoKit, writing the 64-byte signature +// into the output buffer. Returns false when the seed is not a valid key +extern "C" auto sourcemeta_core_eddsa_ed25519_sign_cryptokit( + const unsigned char *seed, std::size_t seed_size, + const unsigned char *message, std::size_t message_size, + unsigned char *signature) -> bool; + #endif diff --git a/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.mm b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.mm index 814b6b505..33bf6ffdc 100644 --- a/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.mm +++ b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.mm @@ -1,5 +1,8 @@ +#include "crypto_aes_apple.h" #include "crypto_eddsa_apple.h" +#include // std::memcpy + #import // NSData // The Objective-C interface generated from the Swift shim @@ -18,3 +21,80 @@ signature:tag] == YES; } } + +extern "C" auto sourcemeta_core_eddsa_ed25519_sign_cryptokit( + const unsigned char *seed, std::size_t seed_size, + const unsigned char *message, std::size_t message_size, + unsigned char *signature) -> bool { + @autoreleasepool { + NSData *const key{[NSData dataWithBytes:seed length:seed_size]}; + NSData *const payload{[NSData dataWithBytes:message length:message_size]}; + NSData *const result{[SourcemetaCoreEd25519 signWithSeed:key + message:payload]}; + if (signature == nullptr || result == nil || result.length != 64) { + return false; + } + + std::memcpy(signature, result.bytes, 64); + return true; + } +} + +extern "C" auto sourcemeta_core_aes_256_gcm_seal_cryptokit( + const unsigned char *key, std::size_t key_size, const unsigned char *nonce, + std::size_t nonce_size, const unsigned char *plaintext, + std::size_t plaintext_size, unsigned char *output) -> bool { + @autoreleasepool { + NSData *const key_data{[NSData dataWithBytes:key length:key_size]}; + NSData *const nonce_data{[NSData dataWithBytes:nonce length:nonce_size]}; + NSData *const plaintext_data{[NSData dataWithBytes:plaintext + length:plaintext_size]}; + NSData *const result{[SourcemetaCoreAESGCM sealWithKey:key_data + nonce:nonce_data + plaintext:plaintext_data]}; + if (output == nullptr || result == nil || + result.length != plaintext_size + 16) { + return false; + } + + std::memcpy(output, result.bytes, result.length); + return true; + } +} + +extern "C" auto sourcemeta_core_aes_256_gcm_open_cryptokit( + const unsigned char *key, std::size_t key_size, const unsigned char *nonce, + std::size_t nonce_size, const unsigned char *ciphertext, + std::size_t ciphertext_size, const unsigned char *tag, + std::size_t tag_size, unsigned char *output) -> bool { + @autoreleasepool { + NSData *const key_data{[NSData dataWithBytes:key length:key_size]}; + NSData *const nonce_data{[NSData dataWithBytes:nonce length:nonce_size]}; + NSData *const ciphertext_data{[NSData dataWithBytes:ciphertext + length:ciphertext_size]}; + NSData *const tag_data{[NSData dataWithBytes:tag length:tag_size]}; + NSData *const result{[SourcemetaCoreAESGCM openWithKey:key_data + nonce:nonce_data + ciphertext:ciphertext_data + tag:tag_data]}; + if (result == nil) { + return false; + } + + // The plaintext length always equals the ciphertext length for GCM, so a + // differing result length would overflow the caller's output buffer + if (result.length != ciphertext_size) { + return false; + } + + if (result.length > 0) { + if (output == nullptr) { + return false; + } + + std::memcpy(output, result.bytes, result.length); + } + + return true; + } +} diff --git a/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.swift b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.swift index 0ab74c742..4a9dd6ed7 100644 --- a/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.swift +++ b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.swift @@ -16,4 +16,52 @@ public final class SourcemetaCoreEd25519: NSObject { return key.isValidSignature(signature, for: message) } + + @objc public static func sign(seed: Data, message: Data) -> Data? { + guard let key = try? Curve25519.Signing.PrivateKey( + rawRepresentation: seed) else { + return nil + } + + return try? key.signature(for: message) + } +} + +// AES-256 in Galois/Counter Mode, which the Apple Security framework does not +// expose through its C API either. CryptoKit provides it since macOS 10.15 +@objc(SourcemetaCoreAESGCM) +public final class SourcemetaCoreAESGCM: NSObject { + @objc public static func seal(key: Data, nonce: Data, + plaintext: Data) -> Data? { + guard key.count == 32 else { + return nil + } + + guard let sealingNonce = try? AES.GCM.Nonce(data: nonce) else { + return nil + } + + guard let box = try? AES.GCM.seal( + plaintext, using: SymmetricKey(data: key), + nonce: sealingNonce) else { + return nil + } + + return box.ciphertext + box.tag + } + + @objc public static func open(key: Data, nonce: Data, ciphertext: Data, + tag: Data) -> Data? { + guard key.count == 32 else { + return nil + } + + guard let openingNonce = try? AES.GCM.Nonce(data: nonce), + let box = try? AES.GCM.SealedBox( + nonce: openingNonce, ciphertext: ciphertext, tag: tag) else { + return nil + } + + return try? AES.GCM.open(box, using: SymmetricKey(data: key)) + } } diff --git a/vendor/core/src/core/crypto/crypto_helpers.h b/vendor/core/src/core/crypto/crypto_helpers.h index e1751d464..9c2e2e787 100644 --- a/vendor/core/src/core/crypto/crypto_helpers.h +++ b/vendor/core/src/core/crypto/crypto_helpers.h @@ -8,6 +8,7 @@ #include #include // std::size_t +#include // std::uint8_t #include // std::string #include // std::string_view #include // std::unreachable @@ -18,6 +19,89 @@ namespace sourcemeta::core { // the range of valid key sizes inline constexpr std::size_t MAXIMUM_KEY_BYTES{512}; +// Overwrite a buffer that held secret material so it does not linger in freed +// memory. The volatile access stops the compiler from eliding the write as a +// dead store +inline auto secure_zero(void *const data, const std::size_t size) noexcept + -> void { + if (data == nullptr) { + return; + } + + auto *pointer{static_cast(data)}; + for (std::size_t index{0}; index < size; index += 1) { + pointer[index] = 0; + } +} + +inline auto secure_zero(std::string &value) noexcept -> void { + secure_zero(value.data(), value.size()); +} + +// Overwrite the referenced buffer when leaving the current scope, so secret +// material a local holds is wiped across every return path without threading a +// manual call through each one. It clears the buffer the string owns at scope +// exit, so a reassignment or a growth that reallocates before then can still +// leave an earlier copy in freed memory, a residual that a wiping allocator +// would be needed to close +struct SecureScope { + explicit SecureScope(std::string &value) noexcept : target{value} {} + SecureScope(const SecureScope &) = delete; + auto operator=(const SecureScope &) -> SecureScope & = delete; + SecureScope(SecureScope &&) = delete; + auto operator=(SecureScope &&) -> SecureScope & = delete; + ~SecureScope() { secure_zero(this->target); } + std::string ⌖ +}; + +// The same guard for a raw buffer, so a secret that a fixed-size digest array +// holds is wiped when leaving the current scope +struct SecureBufferScope { + SecureBufferScope(void *const buffer, const std::size_t length) noexcept + : data{buffer}, size{length} {} + SecureBufferScope(const SecureBufferScope &) = delete; + auto operator=(const SecureBufferScope &) -> SecureBufferScope & = delete; + SecureBufferScope(SecureBufferScope &&) = delete; + auto operator=(SecureBufferScope &&) -> SecureBufferScope & = delete; + ~SecureBufferScope() { secure_zero(this->data, this->size); } + void *const data; + const std::size_t size; +}; + +// Whether one big-endian integer is strictly less than another, comparing by +// significant length first and then lexicographically once the leading zeros +// are stripped. Only for public operands, since it short-circuits on the first +// differing byte +inline auto octets_below(const std::string_view value, + const std::string_view bound) noexcept -> bool { + const auto stripped_value{strip_left(value, '\x00')}; + const auto stripped_bound{strip_left(bound, '\x00')}; + if (stripped_value.size() != stripped_bound.size()) { + return stripped_value.size() < stripped_bound.size(); + } + + return stripped_value < stripped_bound; +} + +// The same comparison over two equal-length big-endian integers in constant +// time, by a full-width subtract that borrows out exactly when the first +// operand is smaller. It inspects every byte, so the timing does not reveal +// where the operands first differ, which matters when one of them is a secret +// scalar +inline auto octets_below_fixed(const std::string_view value, + const std::string_view bound) noexcept -> bool { + unsigned int borrow{0}; + for (std::size_t index = value.size(); index > 0; --index) { + const auto value_byte{ + static_cast(static_cast(value[index - 1]))}; + const auto bound_byte{ + static_cast(static_cast(bound[index - 1]))}; + borrow = ((value_byte - bound_byte - borrow) >> 8U) & 1U; + } + + return borrow == 1; +} + // Whether a signature representative, as a big-endian integer, is strictly // less than the modulus. RFC 8017 Section 5.2.2 requires this range check, so // that an unreduced signature, which an attacker forges by adding the modulus @@ -25,13 +109,27 @@ inline constexpr std::size_t MAXIMUM_KEY_BYTES{512}; inline auto rsa_signature_in_range(const std::string_view signature, const std::string_view modulus) noexcept -> bool { - const auto value{strip_left(signature, '\x00')}; - const auto bound{strip_left(modulus, '\x00')}; - if (value.size() != bound.size()) { - return value.size() < bound.size(); + return octets_below(signature, modulus); +} + +// Whether an RSA public exponent is acceptable for the modulus: odd and in the +// range [3, n) (RFC 8017 Section 3.1). An even exponent is not invertible +// modulo the totient, e = 1 leaves the signature equal to the padded message +// and forges trivially, and an exponent at or above the modulus is not a valid +// key. Shared so every backend rejects such keys, not just the reference one +inline auto +rsa_public_exponent_acceptable(const std::string_view exponent, + const std::string_view modulus) noexcept + -> bool { + const auto stripped{strip_left(exponent, '\x00')}; + if (stripped.empty() || + (static_cast(stripped.back()) & 1U) == 0 || + (stripped.size() == 1 && + static_cast(stripped.back()) < 3)) { + return false; } - return value < bound; + return octets_below(exponent, modulus); } inline auto curve_field_bytes(const EllipticCurve curve) noexcept @@ -48,6 +146,65 @@ inline auto curve_field_bytes(const EllipticCurve curve) noexcept std::unreachable(); } +// The group order of each NIST prime curve as big-endian octets (FIPS 186-4 +// Appendix D.1.2), so a private scalar can be range-checked against it +inline auto curve_order_bytes(const EllipticCurve curve) -> std::string { + switch (curve) { + case EllipticCurve::P256: + return hex_to_bytes("ffffffff00000000ffffffffffffffffbce6faada7179e84" + "f3b9cac2fc632551") + .value(); + case EllipticCurve::P384: + return hex_to_bytes("ffffffffffffffffffffffffffffffffffffffffffffffff" + "c7634d81f4372ddf581a0db248b0a77aecec196accc52973") + .value(); + case EllipticCurve::P521: + return hex_to_bytes("01fffffffffffffffffffffffffffffffffffffffffffffff" + "ffffffffffffffffffa51868783bf2f966b7fcc0148f709a5" + "d03bb5c9b8899c47aebb6fb71e91386409") + .value(); + } + + std::unreachable(); +} + +// Whether an elliptic curve private scalar lies in the valid range [1, n) +// (SEC 1 Section 3.2.1). Shared so every backend rejects an out-of-range +// scalar. The scalar is secret, so it is folded into a fixed-width buffer and +// compared against the order without any value-dependent short-circuit: the +// loops always run their full length and accumulate the zero and overflow flags +inline auto ec_private_scalar_in_range(const std::string_view scalar, + const EllipticCurve curve) -> bool { + const auto width{curve_field_bytes(curve)}; + std::string folded(width, '\x00'); + // The buffer holds a copy of the secret scalar, so it is wiped on return + const SecureScope folded_scope{folded}; + std::uint8_t overflow{0}; + for (std::size_t index = 0; index < scalar.size(); ++index) { + const auto byte{ + static_cast(scalar[scalar.size() - 1 - index])}; + if (index < width) { + folded[width - 1 - index] = static_cast(byte); + } else { + overflow = static_cast(overflow | byte); + } + } + + std::uint8_t nonzero{0}; + for (const auto byte : folded) { + nonzero = + static_cast(nonzero | static_cast(byte)); + } + + // The three conditions are combined bitwise rather than with a short-circuit, + // so the secret overflow and zero flags do not steer a branch + const auto below{octets_below_fixed(folded, curve_order_bytes(curve))}; + const auto within{static_cast(overflow == 0) & + static_cast(nonzero != 0) & + static_cast(below)}; + return within != 0; +} + // The public key and signature octet lengths are fixed per curve (RFC 8032 // Section 5.1.2 and Section 5.1.6) inline auto eddsa_public_key_bytes(const EdwardsCurve curve) noexcept @@ -94,40 +251,6 @@ inline auto digest_message(const SignatureHashFunction hash, std::unreachable(); } -inline auto der_append_length(std::string &output, const std::size_t length) - -> void { - if (length < 128) { - output.push_back(static_cast(length)); - } else if (length < 256) { - output.push_back('\x81'); - output.push_back(static_cast(length)); - } else { - output.push_back('\x82'); - output.push_back(static_cast((length >> 8u) & 0xffu)); - output.push_back(static_cast(length & 0xffu)); - } -} - -inline auto der_append_unsigned_integer(std::string &output, - std::string_view value) -> void { - while (!value.empty() && value.front() == '\x00') { - value.remove_prefix(1); - } - - // A leading zero byte keeps the value positive when its high bit is set, - // and represents the value zero when nothing remains - const auto needs_zero_prefix{ - value.empty() || - (static_cast(value.front()) & 0x80u) != 0}; - output.push_back('\x02'); - der_append_length(output, value.size() + (needs_zero_prefix ? 1 : 0)); - if (needs_zero_prefix) { - output.push_back('\x00'); - } - - output.append(value); -} - } // namespace sourcemeta::core #endif diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha256.cc b/vendor/core/src/core/crypto/crypto_hmac_sha256.cc new file mode 100644 index 000000000..1ddf2f290 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha256.cc @@ -0,0 +1,23 @@ +#include +#include + +#include // std::ostream, std::streamsize +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +auto hmac_sha256(const std::string_view key, const std::string_view message) + -> std::string { + const auto digest{hmac_sha256_digest(key, message)}; + return bytes_to_hex( + {reinterpret_cast(digest.data()), digest.size()}); +} + +auto hmac_sha256(const std::string_view key, const std::string_view message, + std::ostream &output) -> void { + const auto result{hmac_sha256(key, message)}; + output.write(result.data(), static_cast(result.size())); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha256_apple.cc b/vendor/core/src/core/crypto/crypto_hmac_sha256_apple.cc new file mode 100644 index 000000000..4c8e3a3b2 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha256_apple.cc @@ -0,0 +1,19 @@ +#include + +#include // CCHmac, kCCHmacAlgSHA256 + +#include // std::array +#include // std::uint8_t + +namespace sourcemeta::core { + +auto hmac_sha256_digest(const std::string_view key, + const std::string_view message) + -> std::array { + std::array digest{}; + CCHmac(kCCHmacAlgSHA256, key.data(), key.size(), message.data(), + message.size(), digest.data()); + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha256_openssl.cc b/vendor/core/src/core/crypto/crypto_hmac_sha256_openssl.cc new file mode 100644 index 000000000..6e1deb16a --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha256_openssl.cc @@ -0,0 +1,41 @@ +#include +#include + +#include // EVP_sha256 +#include // HMAC + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::runtime_error + +namespace sourcemeta::core { + +auto hmac_sha256_digest(const std::string_view key, + const std::string_view message) + -> std::array { + // A key longer than the block size is hashed first (RFC 2104 Section 2), + // which also keeps the key length within the OpenSSL length parameter + constexpr std::size_t block_size{64}; + std::array key_digest{}; + const unsigned char *key_data{ + reinterpret_cast(key.data())}; + auto key_size{key.size()}; + if (key_size > block_size) { + key_digest = sha256_digest(key); + key_data = key_digest.data(); + key_size = key_digest.size(); + } + + std::array digest{}; + unsigned int length{0}; + if (HMAC(EVP_sha256(), key_data, static_cast(key_size), + reinterpret_cast(message.data()), + message.size(), digest.data(), &length) == nullptr) { + throw std::runtime_error("Could not compute HMAC-SHA256 digest"); + } + + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha256_other.cc b/vendor/core/src/core/crypto/crypto_hmac_sha256_other.cc new file mode 100644 index 000000000..93432d42b --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha256_other.cc @@ -0,0 +1,45 @@ +#include +#include + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::memcpy +#include // std::string + +namespace sourcemeta::core { + +auto hmac_sha256_digest(const std::string_view key, + const std::string_view message) + -> std::array { + // SHA-256 operates on 64-byte blocks (FIPS 180-4 Section 5.1.1), which is the + // block size the HMAC construction pads the key to (RFC 2104 Section 2) + constexpr std::size_t block_size{64}; + + // "If the key is longer than the block size, hash it and use the result" + // (RFC 2104 Section 2) + std::array padded_key{}; + if (key.size() > block_size) { + const auto digest{sha256_digest(key)}; + std::memcpy(padded_key.data(), digest.data(), digest.size()); + } else if (!key.empty()) { + std::memcpy(padded_key.data(), key.data(), key.size()); + } + + // H((K XOR opad) || H((K XOR ipad) || message)) with ipad = 0x36 and + // opad = 0x5c repeated to the block size (RFC 2104 Section 2) + std::string inner_input(block_size, '\x00'); + std::string outer_input(block_size, '\x00'); + for (std::size_t index = 0; index < block_size; ++index) { + inner_input[index] = static_cast(padded_key[index] ^ 0x36); + outer_input[index] = static_cast(padded_key[index] ^ 0x5c); + } + + inner_input.append(message); + const auto inner_digest{sha256_digest(inner_input)}; + outer_input.append(reinterpret_cast(inner_digest.data()), + inner_digest.size()); + return sha256_digest(outer_input); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha256_windows.cc b/vendor/core/src/core/crypto/crypto_hmac_sha256_windows.cc new file mode 100644 index 000000000..f12d69941 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha256_windows.cc @@ -0,0 +1,83 @@ +#include +#include + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG + +#include // BCrypt*, BCRYPT_* + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::numeric_limits +#include // std::runtime_error + +namespace sourcemeta::core { + +auto hmac_sha256_digest(const std::string_view key, + const std::string_view message) + -> std::array { + // A key longer than the block size is hashed first (RFC 2104 Section 2), + // which also keeps the key length within the CNG length parameter. The + // prehash runs before the provider is opened so that a throwing digest cannot + // leak the handle. The secret interface is not const-qualified but never + // writes through the pointer + constexpr std::size_t block_size{64}; + std::array key_digest{}; + auto *secret{ + reinterpret_cast(const_cast(key.data()))}; + auto secret_size{key.size()}; + if (secret_size > block_size) { + key_digest = sha256_digest(key); + secret = key_digest.data(); + secret_size = key_digest.size(); + } + + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, + nullptr, BCRYPT_ALG_HANDLE_HMAC_FLAG))) { + throw std::runtime_error("Could not open the CNG HMAC-SHA256 provider"); + } + + BCRYPT_HASH_HANDLE hash{nullptr}; + if (!BCRYPT_SUCCESS(BCryptCreateHash(algorithm, &hash, nullptr, 0, secret, + static_cast(secret_size), 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + throw std::runtime_error("Could not create the CNG HMAC-SHA256 hash"); + } + + // The data interface is not const-qualified but never writes through + // the pointer, and it takes a 32-bit length, so larger inputs must be + // fed in chunks + auto *remaining_data{ + reinterpret_cast(const_cast(message.data()))}; + auto remaining_size{message.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + auto success{true}; + while (remaining_size > 0 && success) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + success = BCRYPT_SUCCESS(BCryptHashData(hash, remaining_data, + static_cast(chunk_size), 0)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + if (success) { + success = BCRYPT_SUCCESS(BCryptFinishHash( + hash, digest.data(), static_cast(digest.size()), 0)); + } + + BCryptDestroyHash(hash); + BCryptCloseAlgorithmProvider(algorithm, 0); + if (!success) { + throw std::runtime_error("Could not compute the CNG HMAC-SHA256 digest"); + } + + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha384.cc b/vendor/core/src/core/crypto/crypto_hmac_sha384.cc new file mode 100644 index 000000000..55bc4bd54 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha384.cc @@ -0,0 +1,23 @@ +#include +#include + +#include // std::ostream, std::streamsize +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +auto hmac_sha384(const std::string_view key, const std::string_view message) + -> std::string { + const auto digest{hmac_sha384_digest(key, message)}; + return bytes_to_hex( + {reinterpret_cast(digest.data()), digest.size()}); +} + +auto hmac_sha384(const std::string_view key, const std::string_view message, + std::ostream &output) -> void { + const auto result{hmac_sha384(key, message)}; + output.write(result.data(), static_cast(result.size())); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha384_apple.cc b/vendor/core/src/core/crypto/crypto_hmac_sha384_apple.cc new file mode 100644 index 000000000..cce65a868 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha384_apple.cc @@ -0,0 +1,19 @@ +#include + +#include // CCHmac, kCCHmacAlgSHA384 + +#include // std::array +#include // std::uint8_t + +namespace sourcemeta::core { + +auto hmac_sha384_digest(const std::string_view key, + const std::string_view message) + -> std::array { + std::array digest{}; + CCHmac(kCCHmacAlgSHA384, key.data(), key.size(), message.data(), + message.size(), digest.data()); + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha384_openssl.cc b/vendor/core/src/core/crypto/crypto_hmac_sha384_openssl.cc new file mode 100644 index 000000000..af9391936 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha384_openssl.cc @@ -0,0 +1,41 @@ +#include +#include + +#include // EVP_sha384 +#include // HMAC + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::runtime_error + +namespace sourcemeta::core { + +auto hmac_sha384_digest(const std::string_view key, + const std::string_view message) + -> std::array { + // A key longer than the block size is hashed first (RFC 2104 Section 2), + // which also keeps the key length within the OpenSSL length parameter + constexpr std::size_t block_size{128}; + std::array key_digest{}; + const unsigned char *key_data{ + reinterpret_cast(key.data())}; + auto key_size{key.size()}; + if (key_size > block_size) { + key_digest = sha384_digest(key); + key_data = key_digest.data(); + key_size = key_digest.size(); + } + + std::array digest{}; + unsigned int length{0}; + if (HMAC(EVP_sha384(), key_data, static_cast(key_size), + reinterpret_cast(message.data()), + message.size(), digest.data(), &length) == nullptr) { + throw std::runtime_error("Could not compute HMAC-SHA384 digest"); + } + + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha384_other.cc b/vendor/core/src/core/crypto/crypto_hmac_sha384_other.cc new file mode 100644 index 000000000..2c3b71f5e --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha384_other.cc @@ -0,0 +1,45 @@ +#include +#include + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::memcpy +#include // std::string + +namespace sourcemeta::core { + +auto hmac_sha384_digest(const std::string_view key, + const std::string_view message) + -> std::array { + // SHA-384 operates on 128-byte blocks (FIPS 180-4 Section 5.1.2), which is + // the block size the HMAC construction pads the key to (RFC 2104 Section 2) + constexpr std::size_t block_size{128}; + + // "If the key is longer than the block size, hash it and use the result" + // (RFC 2104 Section 2) + std::array padded_key{}; + if (key.size() > block_size) { + const auto digest{sha384_digest(key)}; + std::memcpy(padded_key.data(), digest.data(), digest.size()); + } else if (!key.empty()) { + std::memcpy(padded_key.data(), key.data(), key.size()); + } + + // H((K XOR opad) || H((K XOR ipad) || message)) with ipad = 0x36 and + // opad = 0x5c repeated to the block size (RFC 2104 Section 2) + std::string inner_input(block_size, '\x00'); + std::string outer_input(block_size, '\x00'); + for (std::size_t index = 0; index < block_size; ++index) { + inner_input[index] = static_cast(padded_key[index] ^ 0x36); + outer_input[index] = static_cast(padded_key[index] ^ 0x5c); + } + + inner_input.append(message); + const auto inner_digest{sha384_digest(inner_input)}; + outer_input.append(reinterpret_cast(inner_digest.data()), + inner_digest.size()); + return sha384_digest(outer_input); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha384_windows.cc b/vendor/core/src/core/crypto/crypto_hmac_sha384_windows.cc new file mode 100644 index 000000000..ea55cd454 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha384_windows.cc @@ -0,0 +1,83 @@ +#include +#include + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG + +#include // BCrypt*, BCRYPT_* + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::numeric_limits +#include // std::runtime_error + +namespace sourcemeta::core { + +auto hmac_sha384_digest(const std::string_view key, + const std::string_view message) + -> std::array { + // A key longer than the block size is hashed first (RFC 2104 Section 2), + // which also keeps the key length within the CNG length parameter. The + // prehash runs before the provider is opened so that a throwing digest cannot + // leak the handle. The secret interface is not const-qualified but never + // writes through the pointer + constexpr std::size_t block_size{128}; + std::array key_digest{}; + auto *secret{ + reinterpret_cast(const_cast(key.data()))}; + auto secret_size{key.size()}; + if (secret_size > block_size) { + key_digest = sha384_digest(key); + secret = key_digest.data(); + secret_size = key_digest.size(); + } + + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA384_ALGORITHM, + nullptr, BCRYPT_ALG_HANDLE_HMAC_FLAG))) { + throw std::runtime_error("Could not open the CNG HMAC-SHA384 provider"); + } + + BCRYPT_HASH_HANDLE hash{nullptr}; + if (!BCRYPT_SUCCESS(BCryptCreateHash(algorithm, &hash, nullptr, 0, secret, + static_cast(secret_size), 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + throw std::runtime_error("Could not create the CNG HMAC-SHA384 hash"); + } + + // The data interface is not const-qualified but never writes through + // the pointer, and it takes a 32-bit length, so larger inputs must be + // fed in chunks + auto *remaining_data{ + reinterpret_cast(const_cast(message.data()))}; + auto remaining_size{message.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + auto success{true}; + while (remaining_size > 0 && success) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + success = BCRYPT_SUCCESS(BCryptHashData(hash, remaining_data, + static_cast(chunk_size), 0)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + if (success) { + success = BCRYPT_SUCCESS(BCryptFinishHash( + hash, digest.data(), static_cast(digest.size()), 0)); + } + + BCryptDestroyHash(hash); + BCryptCloseAlgorithmProvider(algorithm, 0); + if (!success) { + throw std::runtime_error("Could not compute the CNG HMAC-SHA384 digest"); + } + + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha512.cc b/vendor/core/src/core/crypto/crypto_hmac_sha512.cc new file mode 100644 index 000000000..8f19be3a5 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha512.cc @@ -0,0 +1,23 @@ +#include +#include + +#include // std::ostream, std::streamsize +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +auto hmac_sha512(const std::string_view key, const std::string_view message) + -> std::string { + const auto digest{hmac_sha512_digest(key, message)}; + return bytes_to_hex( + {reinterpret_cast(digest.data()), digest.size()}); +} + +auto hmac_sha512(const std::string_view key, const std::string_view message, + std::ostream &output) -> void { + const auto result{hmac_sha512(key, message)}; + output.write(result.data(), static_cast(result.size())); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha512_apple.cc b/vendor/core/src/core/crypto/crypto_hmac_sha512_apple.cc new file mode 100644 index 000000000..846b13359 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha512_apple.cc @@ -0,0 +1,19 @@ +#include + +#include // CCHmac, kCCHmacAlgSHA512 + +#include // std::array +#include // std::uint8_t + +namespace sourcemeta::core { + +auto hmac_sha512_digest(const std::string_view key, + const std::string_view message) + -> std::array { + std::array digest{}; + CCHmac(kCCHmacAlgSHA512, key.data(), key.size(), message.data(), + message.size(), digest.data()); + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha512_openssl.cc b/vendor/core/src/core/crypto/crypto_hmac_sha512_openssl.cc new file mode 100644 index 000000000..ec3eea298 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha512_openssl.cc @@ -0,0 +1,41 @@ +#include +#include + +#include // EVP_sha512 +#include // HMAC + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::runtime_error + +namespace sourcemeta::core { + +auto hmac_sha512_digest(const std::string_view key, + const std::string_view message) + -> std::array { + // A key longer than the block size is hashed first (RFC 2104 Section 2), + // which also keeps the key length within the OpenSSL length parameter + constexpr std::size_t block_size{128}; + std::array key_digest{}; + const unsigned char *key_data{ + reinterpret_cast(key.data())}; + auto key_size{key.size()}; + if (key_size > block_size) { + key_digest = sha512_digest(key); + key_data = key_digest.data(); + key_size = key_digest.size(); + } + + std::array digest{}; + unsigned int length{0}; + if (HMAC(EVP_sha512(), key_data, static_cast(key_size), + reinterpret_cast(message.data()), + message.size(), digest.data(), &length) == nullptr) { + throw std::runtime_error("Could not compute HMAC-SHA512 digest"); + } + + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha512_other.cc b/vendor/core/src/core/crypto/crypto_hmac_sha512_other.cc new file mode 100644 index 000000000..052e5c6a4 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha512_other.cc @@ -0,0 +1,45 @@ +#include +#include + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::memcpy +#include // std::string + +namespace sourcemeta::core { + +auto hmac_sha512_digest(const std::string_view key, + const std::string_view message) + -> std::array { + // SHA-512 operates on 128-byte blocks (FIPS 180-4 Section 5.1.2), which is + // the block size the HMAC construction pads the key to (RFC 2104 Section 2) + constexpr std::size_t block_size{128}; + + // "If the key is longer than the block size, hash it and use the result" + // (RFC 2104 Section 2) + std::array padded_key{}; + if (key.size() > block_size) { + const auto digest{sha512_digest(key)}; + std::memcpy(padded_key.data(), digest.data(), digest.size()); + } else if (!key.empty()) { + std::memcpy(padded_key.data(), key.data(), key.size()); + } + + // H((K XOR opad) || H((K XOR ipad) || message)) with ipad = 0x36 and + // opad = 0x5c repeated to the block size (RFC 2104 Section 2) + std::string inner_input(block_size, '\x00'); + std::string outer_input(block_size, '\x00'); + for (std::size_t index = 0; index < block_size; ++index) { + inner_input[index] = static_cast(padded_key[index] ^ 0x36); + outer_input[index] = static_cast(padded_key[index] ^ 0x5c); + } + + inner_input.append(message); + const auto inner_digest{sha512_digest(inner_input)}; + outer_input.append(reinterpret_cast(inner_digest.data()), + inner_digest.size()); + return sha512_digest(outer_input); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_hmac_sha512_windows.cc b/vendor/core/src/core/crypto/crypto_hmac_sha512_windows.cc new file mode 100644 index 000000000..df25c822e --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_hmac_sha512_windows.cc @@ -0,0 +1,83 @@ +#include +#include + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG + +#include // BCrypt*, BCRYPT_* + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::numeric_limits +#include // std::runtime_error + +namespace sourcemeta::core { + +auto hmac_sha512_digest(const std::string_view key, + const std::string_view message) + -> std::array { + // A key longer than the block size is hashed first (RFC 2104 Section 2), + // which also keeps the key length within the CNG length parameter. The + // prehash runs before the provider is opened so that a throwing digest cannot + // leak the handle. The secret interface is not const-qualified but never + // writes through the pointer + constexpr std::size_t block_size{128}; + std::array key_digest{}; + auto *secret{ + reinterpret_cast(const_cast(key.data()))}; + auto secret_size{key.size()}; + if (secret_size > block_size) { + key_digest = sha512_digest(key); + secret = key_digest.data(); + secret_size = key_digest.size(); + } + + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA512_ALGORITHM, + nullptr, BCRYPT_ALG_HANDLE_HMAC_FLAG))) { + throw std::runtime_error("Could not open the CNG HMAC-SHA512 provider"); + } + + BCRYPT_HASH_HANDLE hash{nullptr}; + if (!BCRYPT_SUCCESS(BCryptCreateHash(algorithm, &hash, nullptr, 0, secret, + static_cast(secret_size), 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + throw std::runtime_error("Could not create the CNG HMAC-SHA512 hash"); + } + + // The data interface is not const-qualified but never writes through + // the pointer, and it takes a 32-bit length, so larger inputs must be + // fed in chunks + auto *remaining_data{ + reinterpret_cast(const_cast(message.data()))}; + auto remaining_size{message.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + auto success{true}; + while (remaining_size > 0 && success) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + success = BCRYPT_SUCCESS(BCryptHashData(hash, remaining_data, + static_cast(chunk_size), 0)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + if (success) { + success = BCRYPT_SUCCESS(BCryptFinishHash( + hash, digest.data(), static_cast(digest.size()), 0)); + } + + BCryptDestroyHash(hash); + BCryptCloseAlgorithmProvider(algorithm, 0); + if (!success) { + throw std::runtime_error("Could not compute the CNG HMAC-SHA512 digest"); + } + + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_pkcs8.h b/vendor/core/src/core/crypto/crypto_pkcs8.h new file mode 100644 index 000000000..1a88cab3b --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_pkcs8.h @@ -0,0 +1,158 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_PKCS8_H_ +#define SOURCEMETA_CORE_CRYPTO_PKCS8_H_ + +#include +#include + +#include "crypto_der.h" +#include "crypto_helpers.h" + +#include // std::size_t +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +// Decode a single-block PEM document into its DER bytes +inline auto pem_to_der(const std::string_view pem) + -> std::optional { + const auto begin{pem.find("-----BEGIN")}; + if (begin == std::string_view::npos) { + return std::nullopt; + } + + const auto header_end{pem.find('\n', begin)}; + if (header_end == std::string_view::npos) { + return std::nullopt; + } + + const auto end{pem.find("-----END", header_end)}; + if (end == std::string_view::npos) { + return std::nullopt; + } + + std::string base64; + for (const auto character : + pem.substr(header_end + 1, end - header_end - 1)) { + if (character != '\n' && character != '\r' && character != ' ' && + character != '\t') { + base64.push_back(character); + } + } + + // The base64 body carries the whole private key, so it is wiped once decoded + const SecureScope base64_scope{base64}; + return base64_decode(base64); +} + +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 +struct PKCS8Key { + PKCS8KeyKind kind; + EllipticCurve curve; + EdwardsCurve edwards_curve; + std::string_view key; + // 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}; +}; + +// Parse an RFC 5958 PrivateKeyInfo, identifying the algorithm from its object +// identifier and returning the raw privateKey octets +inline auto parse_pkcs8(const std::string_view der) -> std::optional { + const auto outer{der_read(der)}; + // A canonical PrivateKeyInfo is exactly one SEQUENCE, so bytes trailing the + // outer structure mark a malformed encoding (X.690 Section 10.1) + if (!outer.has_value() || outer->tag != 0x30 || !outer->rest.empty()) { + return std::nullopt; + } + + // Only the two defined OneAsymmetricKey versions are accepted (RFC 5958 + // Section 2): v1 for the bare private key and v2 which may append a public + // key, both encoded as a single-octet INTEGER + const auto version{der_read(outer->content)}; + if (!version.has_value() || version->tag != 0x02 || + version->content.size() != 1 || + static_cast(version->content.front()) > 1) { + return std::nullopt; + } + + const auto algorithm{der_read(version->rest)}; + if (!algorithm.has_value() || algorithm->tag != 0x30) { + return std::nullopt; + } + + const auto private_key{der_read(algorithm->rest)}; + if (!private_key.has_value() || private_key->tag != 0x04) { + return std::nullopt; + } + + const auto oid{der_read(algorithm->content)}; + if (!oid.has_value() || oid->tag != 0x06) { + return std::nullopt; + } + + // The object identifiers are raw DER content bytes, kept as escaped literals + // NOLINTBEGIN(modernize-raw-string-literal) + constexpr std::string_view rsa{"\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01", 9}; + constexpr std::string_view rsa_pss{"\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0a", 9}; + constexpr std::string_view elliptic_curve{"\x2a\x86\x48\xce\x3d\x02\x01", 7}; + constexpr std::string_view ed25519{"\x2b\x65\x70", 3}; + constexpr std::string_view ed448{"\x2b\x65\x71", 3}; + // NOLINTEND(modernize-raw-string-literal) + + if (oid->content == rsa || oid->content == rsa_pss) { + return PKCS8Key{.kind = PKCS8KeyKind::RSA, + .curve = {}, + .edwards_curve = {}, + .key = private_key->content, + .rsa_pss_restricted = oid->content == rsa_pss}; + } + + if (oid->content == ed25519 || oid->content == ed448) { + return PKCS8Key{.kind = PKCS8KeyKind::Edwards, + .curve = {}, + .edwards_curve = oid->content == ed25519 + ? EdwardsCurve::Ed25519 + : EdwardsCurve::Ed448, + .key = private_key->content}; + } + + if (oid->content == elliptic_curve) { + const auto curve_oid{der_read(oid->rest)}; + if (!curve_oid.has_value() || curve_oid->tag != 0x06) { + return std::nullopt; + } + + // NOLINTBEGIN(modernize-raw-string-literal) + constexpr std::string_view p256{"\x2a\x86\x48\xce\x3d\x03\x01\x07", 8}; + constexpr std::string_view p384{"\x2b\x81\x04\x00\x22", 5}; + constexpr std::string_view p521{"\x2b\x81\x04\x00\x23", 5}; + // NOLINTEND(modernize-raw-string-literal) + EllipticCurve curve{}; + if (curve_oid->content == p256) { + curve = EllipticCurve::P256; + } else if (curve_oid->content == p384) { + curve = EllipticCurve::P384; + } else if (curve_oid->content == p521) { + curve = EllipticCurve::P521; + } else { + return std::nullopt; + } + + return PKCS8Key{.kind = PKCS8KeyKind::EllipticCurve, + .curve = curve, + .edwards_curve = {}, + .key = private_key->content}; + } + + return std::nullopt; +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/crypto_random.cc b/vendor/core/src/core/crypto/crypto_random.cc new file mode 100644 index 000000000..080d9a989 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_random.cc @@ -0,0 +1,33 @@ +#include + +#include "crypto_random.h" + +#include // std::size_t +#include // std::uint8_t +#include // std::span +#include // std::string +#include // std::is_same_v + +namespace sourcemeta::core { + +// Writing random bytes into the string's storage reinterprets its char buffer +// as bytes, which is well-defined only because std::uint8_t aliases unsigned +// char, the type permitted to alias any object representation +static_assert(std::is_same_v); + +auto random_bytes(std::span buffer) -> void { + if (buffer.empty()) { + return; + } + + fill_random_bytes(buffer); +} + +auto random_bytes(const std::size_t length) -> std::string { + std::string result(length, '\0'); + random_bytes(std::span{ + reinterpret_cast(result.data()), length}); + return result; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_random.h b/vendor/core/src/core/crypto/crypto_random.h index a6a220c65..b370bdb20 100644 --- a/vendor/core/src/core/crypto/crypto_random.h +++ b/vendor/core/src/core/crypto/crypto_random.h @@ -1,13 +1,14 @@ #ifndef SOURCEMETA_CORE_CRYPTO_RANDOM_H_ #define SOURCEMETA_CORE_CRYPTO_RANDOM_H_ -#include // std::array +#include // std::uint8_t +#include // std::span namespace sourcemeta::core { // Fill the given buffer with random bytes from the system provider where // available. Defined once per backend -auto fill_random_bytes(std::array &bytes) -> void; +auto fill_random_bytes(std::span bytes) -> void; } // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_random_apple.cc b/vendor/core/src/core/crypto/crypto_random_apple.cc index aafa7f6ae..972c18ccd 100644 --- a/vendor/core/src/core/crypto/crypto_random_apple.cc +++ b/vendor/core/src/core/crypto/crypto_random_apple.cc @@ -3,12 +3,13 @@ #include // errSecSuccess #include // SecRandomCopyBytes, kSecRandomDefault -#include // std::array +#include // std::uint8_t +#include // std::span #include // std::runtime_error namespace sourcemeta::core { -auto fill_random_bytes(std::array &bytes) -> void { +auto fill_random_bytes(std::span bytes) -> void { if (SecRandomCopyBytes(kSecRandomDefault, bytes.size(), bytes.data()) != errSecSuccess) { throw std::runtime_error( diff --git a/vendor/core/src/core/crypto/crypto_random_openssl.cc b/vendor/core/src/core/crypto/crypto_random_openssl.cc index ac4c63257..0a7bd9ce4 100644 --- a/vendor/core/src/core/crypto/crypto_random_openssl.cc +++ b/vendor/core/src/core/crypto/crypto_random_openssl.cc @@ -2,14 +2,26 @@ #include // RAND_bytes -#include // std::array +#include // std::min +#include // std::size_t +#include // std::uint8_t +#include // std::numeric_limits +#include // std::span #include // std::runtime_error namespace sourcemeta::core { -auto fill_random_bytes(std::array &bytes) -> void { - if (RAND_bytes(bytes.data(), static_cast(bytes.size())) != 1) { - throw std::runtime_error("Could not generate random bytes with OpenSSL"); +auto fill_random_bytes(std::span bytes) -> void { + // RAND_bytes takes a signed int length, so fill in chunks to avoid narrowing + // a larger span into a truncated or negative length + while (!bytes.empty()) { + const auto chunk{static_cast( + std::min(bytes.size(), std::numeric_limits::max()))}; + if (RAND_bytes(bytes.data(), chunk) != 1) { + throw std::runtime_error("Could not generate random bytes with OpenSSL"); + } + + bytes = bytes.subspan(static_cast(chunk)); } } diff --git a/vendor/core/src/core/crypto/crypto_random_other.cc b/vendor/core/src/core/crypto/crypto_random_other.cc index 72e2a83c0..f402579b6 100644 --- a/vendor/core/src/core/crypto/crypto_random_other.cc +++ b/vendor/core/src/core/crypto/crypto_random_other.cc @@ -1,18 +1,59 @@ #include "crypto_random.h" -#include // std::array -#include // std::random_device, std::mt19937, std::uniform_int_distribution +#include // getentropy + +#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 namespace sourcemeta::core { -auto fill_random_bytes(std::array &bytes) -> void { - // Not a cryptographically secure generator. This fallback only exists to - // keep the module buildable on platforms without a system provider - thread_local std::random_device device; - thread_local std::mt19937 generator{device()}; - std::uniform_int_distribution distribution{0, 255}; - for (auto &byte : bytes) { - byte = static_cast(distribution(generator)); +namespace { + +// Read from the kernel random device, used only on the rare platforms where +// getentropy is present but the kernel call is unavailable +auto fill_from_random_device(const std::span bytes) -> void { + 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 + std::size_t offset{0}; + 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(); + } +} + +} // namespace + +auto fill_random_bytes(std::span bytes) -> void { + // getentropy draws from the kernel cryptographic generator and fails closed, + // never returning low-quality bytes, but fills at most 256 bytes per call + 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) { + fill_from_random_device(bytes.subspan(offset)); + return; + } + + offset += chunk; } } diff --git a/vendor/core/src/core/crypto/crypto_random_windows.cc b/vendor/core/src/core/crypto/crypto_random_windows.cc index 695da9908..59f90d9e8 100644 --- a/vendor/core/src/core/crypto/crypto_random_windows.cc +++ b/vendor/core/src/core/crypto/crypto_random_windows.cc @@ -6,16 +6,27 @@ #include // BCrypt*, BCRYPT_* -#include // std::array +#include // std::min +#include // std::size_t +#include // std::uint8_t +#include // std::numeric_limits +#include // std::span #include // std::runtime_error namespace sourcemeta::core { -auto fill_random_bytes(std::array &bytes) -> void { - if (!BCRYPT_SUCCESS(BCryptGenRandom(nullptr, bytes.data(), - static_cast(bytes.size()), - BCRYPT_USE_SYSTEM_PREFERRED_RNG))) { - throw std::runtime_error("Could not generate random bytes with CNG"); +auto fill_random_bytes(std::span bytes) -> void { + // BCryptGenRandom takes a ULONG length, so fill in chunks to avoid narrowing + // a larger span into a wrapped length that would leave a suffix unfilled + while (!bytes.empty()) { + const auto chunk{static_cast(std::min( + bytes.size(), std::numeric_limits::max()))}; + if (!BCRYPT_SUCCESS(BCryptGenRandom(nullptr, bytes.data(), chunk, + BCRYPT_USE_SYSTEM_PREFERRED_RNG))) { + throw std::runtime_error("Could not generate random bytes with CNG"); + } + + bytes = bytes.subspan(chunk); } } diff --git a/vendor/core/src/core/crypto/crypto_secure_equals_apple.cc b/vendor/core/src/core/crypto/crypto_secure_equals_apple.cc new file mode 100644 index 000000000..8a25cf1fb --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_secure_equals_apple.cc @@ -0,0 +1,24 @@ +#include + +#include // timingsafe_bcmp + +#include // std::string_view + +namespace sourcemeta::core { + +auto secure_equals(const std::string_view left, + const std::string_view right) noexcept -> bool { + if (left.size() != right.size()) { + return false; + } + + // An empty view may hold a null data pointer, which is undefined to pass to + // the C API even with a zero length + if (left.empty()) { + return true; + } + + return timingsafe_bcmp(left.data(), right.data(), left.size()) == 0; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_secure_equals_generic.cc b/vendor/core/src/core/crypto/crypto_secure_equals_generic.cc new file mode 100644 index 000000000..9977bc404 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_secure_equals_generic.cc @@ -0,0 +1,28 @@ +#include + +#include // std::size_t +#include // std::string_view + +namespace sourcemeta::core { + +auto secure_equals(const std::string_view left, + const std::string_view right) noexcept -> bool { + if (left.size() != right.size()) { + return false; + } + + // Accumulate the difference across every byte rather than returning on the + // first mismatch, so that the running time depends only on the length. The + // accumulator is volatile so that the compiler cannot reintroduce a + // short-circuit that would leak the position of the first difference + volatile unsigned char difference{0}; + for (std::size_t index = 0; index < left.size(); ++index) { + difference = static_cast( + difference | (static_cast(left[index]) ^ + static_cast(right[index]))); + } + + return difference == 0; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_secure_equals_openssl.cc b/vendor/core/src/core/crypto/crypto_secure_equals_openssl.cc new file mode 100644 index 000000000..ec700cb3e --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_secure_equals_openssl.cc @@ -0,0 +1,24 @@ +#include + +#include // CRYPTO_memcmp + +#include // std::string_view + +namespace sourcemeta::core { + +auto secure_equals(const std::string_view left, + const std::string_view right) noexcept -> bool { + if (left.size() != right.size()) { + return false; + } + + // An empty view may hold a null data pointer, which is undefined to pass to + // the C API even with a zero length + if (left.empty()) { + return true; + } + + return CRYPTO_memcmp(left.data(), right.data(), left.size()) == 0; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sign_apple.cc b/vendor/core/src/core/crypto/crypto_sign_apple.cc new file mode 100644 index 000000000..5cd617c36 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sign_apple.cc @@ -0,0 +1,464 @@ +#include +#include + +#include "crypto_eddsa.h" +#include "crypto_eddsa_apple.h" +#include "crypto_helpers.h" +#include "crypto_pkcs8.h" + +#include // CF*, kCF* +#include // Sec*, kSec* + +#include // std::array +#include // assert +#include // std::size_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move, std::unreachable + +namespace sourcemeta::core { + +// The parsed key keeps the platform key object alive for reuse. The Edwards +// curves have no Security framework primitive, so they keep the raw seed and +// sign through CryptoKit or the reference implementation +struct PrivateKey::Internal { + PrivateKey::Type kind; + SecKeyRef key; + std::size_t field_bytes; + std::string edwards_seed; + EdwardsCurve edwards_curve; + bool rsa_pss_restricted{false}; +}; + +} // namespace sourcemeta::core + +namespace { + +auto to_sec_key_pkcs1_v15_algorithm( + const sourcemeta::core::SignatureHashFunction hash) noexcept + -> SecKeyAlgorithm { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256; + case sourcemeta::core::SignatureHashFunction::SHA384: + return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA384; + case sourcemeta::core::SignatureHashFunction::SHA512: + return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA512; + } + + std::unreachable(); +} + +// These algorithm variants fix the salt length to the hash function output, +// which is exactly what RFC 7518 Section 3.5 requires +auto to_sec_key_pss_algorithm( + const sourcemeta::core::SignatureHashFunction hash) noexcept + -> SecKeyAlgorithm { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return kSecKeyAlgorithmRSASignatureMessagePSSSHA256; + case sourcemeta::core::SignatureHashFunction::SHA384: + return kSecKeyAlgorithmRSASignatureMessagePSSSHA384; + case sourcemeta::core::SignatureHashFunction::SHA512: + return kSecKeyAlgorithmRSASignatureMessagePSSSHA512; + } + + std::unreachable(); +} + +auto to_sec_key_ecdsa_algorithm( + const sourcemeta::core::SignatureHashFunction hash) noexcept + -> SecKeyAlgorithm { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return kSecKeyAlgorithmECDSASignatureMessageX962SHA256; + case sourcemeta::core::SignatureHashFunction::SHA384: + return kSecKeyAlgorithmECDSASignatureMessageX962SHA384; + case sourcemeta::core::SignatureHashFunction::SHA512: + return kSecKeyAlgorithmECDSASignatureMessageX962SHA512; + } + + std::unreachable(); +} + +auto make_data(const std::string_view value) -> CFDataRef { + return CFDataCreate(kCFAllocatorDefault, + reinterpret_cast(value.data()), + static_cast(value.size())); +} + +auto native_private_key(const void *type, const std::string_view key) + -> SecKeyRef { + auto key_data{make_data(key)}; + if (key_data == nullptr) { + return nullptr; + } + + std::array attribute_keys{ + {kSecAttrKeyType, kSecAttrKeyClass}}; + std::array attribute_values{{type, kSecAttrKeyClassPrivate}}; + auto attributes{CFDictionaryCreate( + kCFAllocatorDefault, attribute_keys.data(), attribute_values.data(), + static_cast(attribute_keys.size()), + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)}; + if (attributes == nullptr) { + CFRelease(key_data); + return nullptr; + } + + auto key_reference{SecKeyCreateWithData(key_data, attributes, nullptr)}; + CFRelease(attributes); + CFRelease(key_data); + return key_reference; +} + +// The platform expects an elliptic curve private key as the X9.63 public point +// followed by the private scalar, both recovered from the SEC1 ECPrivateKey +auto native_ec_private_key(const std::string_view sec1, + const std::size_t field_bytes) -> SecKeyRef { + const auto sequence{sourcemeta::core::der_read(sec1)}; + if (!sequence.has_value() || sequence->tag != 0x30) { + return nullptr; + } + + const auto version{sourcemeta::core::der_read(sequence->content)}; + if (!version.has_value() || version->tag != 0x02) { + return nullptr; + } + + const auto scalar{sourcemeta::core::der_read(version->rest)}; + if (!scalar.has_value() || scalar->tag != 0x04) { + return nullptr; + } + + std::string_view point; + auto rest{scalar->rest}; + while (!rest.empty()) { + const auto element{sourcemeta::core::der_read(rest)}; + if (!element.has_value()) { + return nullptr; + } + + if (element->tag == 0xa1) { + const auto bit_string{sourcemeta::core::der_read(element->content)}; + if (!bit_string.has_value() || bit_string->tag != 0x03 || + bit_string->content.empty()) { + return nullptr; + } + + // The BIT STRING opens with an unused-bits count, then the point + point = bit_string->content.substr(1); + break; + } + + 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) { + return nullptr; + } + + std::string data{point}; + data.append(sourcemeta::core::pad_left(stripped_scalar, field_bytes, '\x00')); + return native_private_key(kSecAttrKeyTypeECSECPrimeRandom, data); +} + +// The platform expects the X9.63 public point followed by the private scalar, +// built here directly from the caller-provided components +auto native_ec_private_key_components(const std::size_t field_bytes, + const std::string_view scalar, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> SecKeyRef { + const auto stripped_x{sourcemeta::core::strip_left(coordinate_x, '\x00')}; + const auto stripped_y{sourcemeta::core::strip_left(coordinate_y, '\x00')}; + const auto stripped_scalar{sourcemeta::core::strip_left(scalar, '\x00')}; + if (stripped_scalar.empty() || stripped_x.size() > field_bytes || + stripped_y.size() > field_bytes || stripped_scalar.size() > field_bytes) { + return nullptr; + } + + std::string data; + data.push_back('\x04'); + data.append(sourcemeta::core::pad_left(stripped_x, field_bytes, '\x00')); + data.append(sourcemeta::core::pad_left(stripped_y, field_bytes, '\x00')); + data.append(sourcemeta::core::pad_left(stripped_scalar, field_bytes, '\x00')); + return native_private_key(kSecAttrKeyTypeECSECPrimeRandom, data); +} + +auto sign_with_algorithm(SecKeyRef key, const SecKeyAlgorithm algorithm, + const std::string_view message) + -> std::optional { + auto message_data{make_data(message)}; + if (message_data == nullptr) { + return std::nullopt; + } + + auto signature{SecKeyCreateSignature(key, algorithm, message_data, nullptr)}; + CFRelease(message_data); + if (signature == nullptr) { + return std::nullopt; + } + + std::string result{ + reinterpret_cast(CFDataGetBytePtr(signature)), + static_cast(CFDataGetLength(signature))}; + CFRelease(signature); + return result; +} + +// Convert the X9.62 DER signature that the platform produces into the raw +// fixed-width R || S concatenation that JWS mandates (RFC 7518 Section 3.4) +auto encode_ecdsa_signature(const std::string_view der, + const std::size_t field_bytes) + -> std::optional { + const auto sequence{sourcemeta::core::der_read(der)}; + if (!sequence.has_value() || sequence->tag != 0x30) { + return std::nullopt; + } + + const auto r{sourcemeta::core::der_read(sequence->content)}; + if (!r.has_value() || r->tag != 0x02) { + return std::nullopt; + } + + const auto s{sourcemeta::core::der_read(r->rest)}; + if (!s.has_value() || s->tag != 0x02) { + return std::nullopt; + } + + std::string raw; + raw.append(sourcemeta::core::pad_left( + sourcemeta::core::strip_left(r->content, '\x00'), field_bytes, '\x00')); + raw.append(sourcemeta::core::pad_left( + sourcemeta::core::strip_left(s->content, '\x00'), field_bytes, '\x00')); + return raw; +} + +} // namespace + +namespace sourcemeta::core { + +PrivateKey::PrivateKey(Internal *internal) noexcept : internal_{internal} {} + +PrivateKey::~PrivateKey() { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + CFRelease(internal_->key); + } + + secure_zero(internal_->edwards_seed); + delete internal_; + } +} + +PrivateKey::PrivateKey(PrivateKey &&other) noexcept + : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PrivateKey::operator=(PrivateKey &&other) noexcept -> PrivateKey & { + if (this != &other) { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + CFRelease(internal_->key); + } + + secure_zero(internal_->edwards_seed); + delete internal_; + } + + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PrivateKey::type() const noexcept -> Type { + // A moved-from key holds no state, so reading its kind is a use-after-move + assert(internal_ != nullptr); + return internal_->kind; +} + +auto make_private_key(const std::string_view pem) -> std::optional { + auto der{pem_to_der(pem)}; + if (!der.has_value()) { + return std::nullopt; + } + + // The decoded PKCS#8 holds the whole private key, so it is wiped on return + const SecureScope der_scope{der.value()}; + const auto parsed{parse_pkcs8(der.value())}; + if (!parsed.has_value()) { + return std::nullopt; + } + + switch (parsed->kind) { + case PKCS8KeyKind::RSA: { + auto *key{native_private_key(kSecAttrKeyTypeRSA, parsed->key)}; + if (key == nullptr) { + return std::nullopt; + } + + return PrivateKey{new PrivateKey::Internal{ + .kind = PrivateKey::Type::RSA, + .key = key, + .field_bytes = 0, + .edwards_seed = {}, + .edwards_curve = {}, + .rsa_pss_restricted = parsed->rsa_pss_restricted}}; + } + case PKCS8KeyKind::EllipticCurve: { + const auto field_bytes{curve_field_bytes(parsed->curve)}; + auto *key{native_ec_private_key(parsed->key, field_bytes)}; + if (key == nullptr) { + return std::nullopt; + } + + return PrivateKey{ + new PrivateKey::Internal{.kind = PrivateKey::Type::EllipticCurve, + .key = key, + .field_bytes = field_bytes, + .edwards_seed = {}, + .edwards_curve = {}}}; + } + case PKCS8KeyKind::Edwards: { + const auto seed{der_read(parsed->key)}; + if (!seed.has_value() || seed->tag != 0x04 || + seed->content.size() != + eddsa_public_key_bytes(parsed->edwards_curve)) { + return std::nullopt; + } + + return PrivateKey{ + new PrivateKey::Internal{.kind = PrivateKey::Type::Edwards, + .key = nullptr, + .field_bytes = 0, + .edwards_seed = std::string{seed->content}, + .edwards_curve = parsed->edwards_curve}}; + } + } + + std::unreachable(); +} + +auto make_ec_private_key(const EllipticCurve curve, + const std::string_view scalar, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + if (!ec_private_scalar_in_range(scalar, curve)) { + return std::nullopt; + } + + const auto field_bytes{curve_field_bytes(curve)}; + auto *key{native_ec_private_key_components(field_bytes, scalar, coordinate_x, + coordinate_y)}; + if (key == nullptr) { + return std::nullopt; + } + + return PrivateKey{ + new PrivateKey::Internal{.kind = PrivateKey::Type::EllipticCurve, + .key = key, + .field_bytes = field_bytes, + .edwards_seed = {}, + .edwards_curve = {}}}; +} + +auto make_edwards_private_key(const EdwardsCurve curve, + const std::string_view seed) + -> std::optional { + if (seed.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + return PrivateKey{new PrivateKey::Internal{.kind = PrivateKey::Type::Edwards, + .key = nullptr, + .field_bytes = 0, + .edwards_seed = std::string{seed}, + .edwards_curve = curve}}; +} + +auto rsassa_pkcs1_v15_sign(const PrivateKey &key, + const SignatureHashFunction hash, + const std::string_view message) + -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + return std::nullopt; + } + + // An id-RSASSA-PSS key is restricted to PSS and must not sign PKCS1v15 + if (internal->rsa_pss_restricted) { + return std::nullopt; + } + + return sign_with_algorithm(internal->key, + to_sec_key_pkcs1_v15_algorithm(hash), message); +} + +auto rsassa_pss_sign(const PrivateKey &key, const SignatureHashFunction hash, + const std::string_view message) + -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + return std::nullopt; + } + + return sign_with_algorithm(internal->key, to_sec_key_pss_algorithm(hash), + message); +} + +auto ecdsa_sign(const PrivateKey &key, const SignatureHashFunction hash, + const std::string_view message) -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || + internal->kind != PrivateKey::Type::EllipticCurve) { + return std::nullopt; + } + + const auto der{sign_with_algorithm( + internal->key, to_sec_key_ecdsa_algorithm(hash), message)}; + if (!der.has_value()) { + return std::nullopt; + } + + return encode_ecdsa_signature(der.value(), internal->field_bytes); +} + +auto eddsa_sign(const PrivateKey &key, const std::string_view message) + -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::Edwards) { + return std::nullopt; + } + + switch (internal->edwards_curve) { + case EdwardsCurve::Ed25519: { + std::string signature(64, '\x00'); + if (!sourcemeta_core_eddsa_ed25519_sign_cryptokit( + reinterpret_cast( + internal->edwards_seed.data()), + internal->edwards_seed.size(), + reinterpret_cast(message.data()), + message.size(), + reinterpret_cast(signature.data()))) { + return std::nullopt; + } + + return signature; + } + case EdwardsCurve::Ed448: + return edwards448_sign(internal->edwards_seed, message); + } + + std::unreachable(); +} + +} // 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 new file mode 100644 index 000000000..a10c7a098 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sign_openssl.cc @@ -0,0 +1,456 @@ +#include +#include + +#include "crypto_helpers.h" +#include "crypto_pkcs8.h" + +#include // BN_bn2binpad, BN_bin2bn +#include // OSSL_PKEY_PARAM_* +#include // ECDSA_SIG, d2i_ECDSA_SIG, ECDSA_SIG_get0_* +#include // EVP_* +#include // OSSL_PARAM_* +#include // PEM_read_bio_PrivateKey, BIO_* +#include // RSA_PKCS1_PSS_PADDING, RSA_PSS_SALTLEN_DIGEST + +#include // std::array +#include // assert +#include // std::size_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move, std::unreachable + +namespace sourcemeta::core { + +// The parsed key keeps the native handle alive so that many signatures are +// produced without rebuilding it +struct PrivateKey::Internal { + PrivateKey::Type kind; + EVP_PKEY *key; + // The field width for the elliptic curve raw signature encoding + std::size_t field_bytes; + // Set for an id-RSASSA-PSS key, which is refused for PKCS1v15 signing + bool rsa_pss_restricted{false}; +}; + +} // namespace sourcemeta::core + +namespace { + +auto to_message_digest( + const sourcemeta::core::SignatureHashFunction hash) noexcept + -> const EVP_MD * { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return EVP_sha256(); + case sourcemeta::core::SignatureHashFunction::SHA384: + return EVP_sha384(); + case sourcemeta::core::SignatureHashFunction::SHA512: + return EVP_sha512(); + } + + std::unreachable(); +} + +// Refuse encrypted documents by offering no password, rather than letting the +// default callback prompt on the terminal +auto no_password(char *, int, int, void *) -> int { return 0; } + +auto to_group_name(const sourcemeta::core::EllipticCurve curve) noexcept + -> const char * { + switch (curve) { + case sourcemeta::core::EllipticCurve::P256: + return "P-256"; + case sourcemeta::core::EllipticCurve::P384: + return "P-384"; + case sourcemeta::core::EllipticCurve::P521: + return "P-521"; + } + + std::unreachable(); +} + +auto to_pkey_id(const sourcemeta::core::EdwardsCurve curve) noexcept -> int { + switch (curve) { + case sourcemeta::core::EdwardsCurve::Ed25519: + return EVP_PKEY_ED25519; + case sourcemeta::core::EdwardsCurve::Ed448: + return EVP_PKEY_ED448; + } + + std::unreachable(); +} + +// The signing functions only cover the three NIST prime curves, so a parsed key +// on any other group is rejected here rather than producing a signature that no +// matching public key can be built for +auto supported_ec_field_bytes(EVP_PKEY *key) -> std::optional { + std::array group{}; + std::size_t length{0}; + if (EVP_PKEY_get_utf8_string_param(key, OSSL_PKEY_PARAM_GROUP_NAME, + group.data(), group.size(), + &length) != 1) { + return std::nullopt; + } + + const std::string_view name{group.data(), length}; + if (name == "prime256v1" || name == "P-256") { + return 32; + } + if (name == "secp384r1" || name == "P-384") { + return 48; + } + if (name == "secp521r1" || name == "P-521") { + return 66; + } + + return std::nullopt; +} + +auto native_ec_private_key(const sourcemeta::core::EllipticCurve curve, + const std::string_view scalar, + const std::string_view coordinate_x, + const std::string_view coordinate_y) -> EVP_PKEY * { + const auto width{sourcemeta::core::curve_field_bytes(curve)}; + const auto stripped_x{sourcemeta::core::strip_left(coordinate_x, '\x00')}; + const auto stripped_y{sourcemeta::core::strip_left(coordinate_y, '\x00')}; + const auto stripped_scalar{sourcemeta::core::strip_left(scalar, '\x00')}; + if (stripped_scalar.empty() || stripped_x.size() > width || + stripped_y.size() > width || stripped_scalar.size() > width) { + return nullptr; + } + + std::string point; + point.push_back('\x04'); + point.append(sourcemeta::core::pad_left(stripped_x, width, '\x00')); + point.append(sourcemeta::core::pad_left(stripped_y, width, '\x00')); + const auto padded_scalar{ + sourcemeta::core::pad_left(stripped_scalar, width, '\x00')}; + + EVP_PKEY *result{nullptr}; + auto *scalar_number{ + BN_bin2bn(reinterpret_cast(padded_scalar.data()), + static_cast(padded_scalar.size()), nullptr)}; + auto *builder{OSSL_PARAM_BLD_new()}; + if (scalar_number != nullptr && builder != nullptr && + OSSL_PARAM_BLD_push_utf8_string(builder, OSSL_PKEY_PARAM_GROUP_NAME, + to_group_name(curve), 0) == 1 && + OSSL_PARAM_BLD_push_octet_string( + builder, OSSL_PKEY_PARAM_PUB_KEY, + reinterpret_cast(point.data()), + point.size()) == 1 && + OSSL_PARAM_BLD_push_BN(builder, OSSL_PKEY_PARAM_PRIV_KEY, + scalar_number) == 1) { + auto *parameters{OSSL_PARAM_BLD_to_param(builder)}; + if (parameters != nullptr) { + auto *context{EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr)}; + if (context != nullptr) { + if (EVP_PKEY_fromdata_init(context) == 1) { + EVP_PKEY_fromdata(context, &result, EVP_PKEY_KEYPAIR, parameters); + } + + EVP_PKEY_CTX_free(context); + } + + OSSL_PARAM_free(parameters); + } + } + + OSSL_PARAM_BLD_free(builder); + BN_free(scalar_number); + return result; +} + +auto sign_digest(EVP_MD_CTX *context, const std::string_view message) + -> std::optional { + std::size_t length{0}; + if (EVP_DigestSign(context, nullptr, &length, + reinterpret_cast(message.data()), + message.size()) != 1) { + return std::nullopt; + } + + std::string signature(length, '\x00'); + if (EVP_DigestSign( + context, reinterpret_cast(signature.data()), &length, + reinterpret_cast(message.data()), + message.size()) != 1) { + return std::nullopt; + } + + signature.resize(length); + return signature; +} + +auto sign_rsa(EVP_PKEY *key, const sourcemeta::core::SignatureHashFunction hash, + const std::string_view message, const bool probabilistic) + -> std::optional { + std::optional result; + auto *context{EVP_MD_CTX_new()}; + if (context != nullptr) { + EVP_PKEY_CTX *key_context{nullptr}; + auto ready{EVP_DigestSignInit(context, &key_context, + to_message_digest(hash), nullptr, key) == 1}; + // The padding is set explicitly rather than left to the provider default, + // so a key restricted to one scheme cleanly rejects the other + if (ready && probabilistic) { + ready = EVP_PKEY_CTX_set_rsa_padding(key_context, + RSA_PKCS1_PSS_PADDING) == 1 && + EVP_PKEY_CTX_set_rsa_pss_saltlen(key_context, + RSA_PSS_SALTLEN_DIGEST) == 1; + } else if (ready) { + ready = EVP_PKEY_CTX_set_rsa_padding(key_context, RSA_PKCS1_PADDING) == 1; + } + + if (ready) { + result = sign_digest(context, message); + } + + EVP_MD_CTX_free(context); + } + + return result; +} + +// Convert the DER ECDSA signature that the platform produces into the raw +// fixed-width R || S concatenation that JWS mandates (RFC 7518 Section 3.4) +auto encode_ecdsa_signature(const std::string_view der, + const std::size_t field_bytes) + -> std::optional { + const auto *pointer{reinterpret_cast(der.data())}; + auto *signature{ + d2i_ECDSA_SIG(nullptr, &pointer, static_cast(der.size()))}; + if (signature == nullptr) { + return std::nullopt; + } + + const auto width{static_cast(field_bytes)}; + std::string raw(field_bytes * 2, '\x00'); + auto *raw_data{reinterpret_cast(raw.data())}; + const auto ok{ + BN_bn2binpad(ECDSA_SIG_get0_r(signature), raw_data, width) == width && + BN_bn2binpad(ECDSA_SIG_get0_s(signature), raw_data + width, width) == + width}; + ECDSA_SIG_free(signature); + if (!ok) { + return std::nullopt; + } + + return raw; +} + +} // namespace + +namespace sourcemeta::core { + +PrivateKey::PrivateKey(Internal *internal) noexcept : internal_{internal} {} + +PrivateKey::~PrivateKey() { + if (internal_ != nullptr) { + EVP_PKEY_free(internal_->key); + delete internal_; + } +} + +PrivateKey::PrivateKey(PrivateKey &&other) noexcept + : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PrivateKey::operator=(PrivateKey &&other) noexcept -> PrivateKey & { + if (this != &other) { + if (internal_ != nullptr) { + EVP_PKEY_free(internal_->key); + delete internal_; + } + + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PrivateKey::type() const noexcept -> Type { + // A moved-from key holds no state, so reading its kind is a use-after-move + assert(internal_ != nullptr); + return internal_->kind; +} + +auto make_private_key(const std::string_view pem) -> std::optional { + // The OpenSSL PEM reader accepts non-canonical PKCS#8 such as trailing bytes + // past the outer SEQUENCE, so the structure is validated up front to match + // the other backends (X.690 Section 10.1) + auto der{pem_to_der(pem)}; + if (!der.has_value()) { + return std::nullopt; + } + + // The decoded PKCS#8 holds the whole private key, so it is wiped on return + const SecureScope der_scope{der.value()}; + if (!parse_pkcs8(der.value()).has_value()) { + return std::nullopt; + } + + auto *bio{BIO_new_mem_buf(pem.data(), static_cast(pem.size()))}; + if (bio == nullptr) { + return std::nullopt; + } + + auto *key{PEM_read_bio_PrivateKey(bio, nullptr, no_password, nullptr)}; + BIO_free(bio); + if (key == nullptr) { + return std::nullopt; + } + + const auto base_id{EVP_PKEY_get_base_id(key)}; + PrivateKey::Type kind{}; + std::size_t field_bytes{0}; + switch (base_id) { + case EVP_PKEY_RSA: + case EVP_PKEY_RSA_PSS: + kind = PrivateKey::Type::RSA; + break; + case EVP_PKEY_EC: { + const auto supported{supported_ec_field_bytes(key)}; + if (!supported.has_value()) { + EVP_PKEY_free(key); + return std::nullopt; + } + + kind = PrivateKey::Type::EllipticCurve; + field_bytes = supported.value(); + break; + } + case EVP_PKEY_ED25519: + case EVP_PKEY_ED448: + kind = PrivateKey::Type::Edwards; + break; + default: + EVP_PKEY_free(key); + return std::nullopt; + } + + return PrivateKey{new PrivateKey::Internal{.kind = kind, + .key = key, + .field_bytes = field_bytes, + .rsa_pss_restricted = + base_id == EVP_PKEY_RSA_PSS}}; +} + +auto make_ec_private_key(const EllipticCurve curve, + const std::string_view scalar, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + if (!ec_private_scalar_in_range(scalar, curve)) { + return std::nullopt; + } + + auto *key{native_ec_private_key(curve, scalar, coordinate_x, coordinate_y)}; + if (key == nullptr) { + return std::nullopt; + } + + return PrivateKey{ + new PrivateKey::Internal{.kind = PrivateKey::Type::EllipticCurve, + .key = key, + .field_bytes = curve_field_bytes(curve)}}; +} + +auto make_edwards_private_key(const EdwardsCurve curve, + const std::string_view seed) + -> std::optional { + if (seed.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + auto *key{EVP_PKEY_new_raw_private_key( + to_pkey_id(curve), nullptr, + reinterpret_cast(seed.data()), seed.size())}; + if (key == nullptr) { + return std::nullopt; + } + + return PrivateKey{new PrivateKey::Internal{ + .kind = PrivateKey::Type::Edwards, .key = key, .field_bytes = 0}}; +} + +auto rsassa_pkcs1_v15_sign(const PrivateKey &key, + const SignatureHashFunction hash, + const std::string_view message) + -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + return std::nullopt; + } + + // An id-RSASSA-PSS key is restricted to PSS and must not sign PKCS1v15 + if (internal->rsa_pss_restricted) { + return std::nullopt; + } + + return sign_rsa(internal->key, hash, message, false); +} + +auto rsassa_pss_sign(const PrivateKey &key, const SignatureHashFunction hash, + const std::string_view message) + -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + return std::nullopt; + } + + return sign_rsa(internal->key, hash, message, true); +} + +auto ecdsa_sign(const PrivateKey &key, const SignatureHashFunction hash, + const std::string_view message) -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || + internal->kind != PrivateKey::Type::EllipticCurve) { + return std::nullopt; + } + + std::optional der; + auto *context{EVP_MD_CTX_new()}; + if (context != nullptr) { + if (EVP_DigestSignInit(context, nullptr, to_message_digest(hash), nullptr, + internal->key) == 1) { + der = sign_digest(context, message); + } + + EVP_MD_CTX_free(context); + } + + if (!der.has_value()) { + return std::nullopt; + } + + return encode_ecdsa_signature(der.value(), internal->field_bytes); +} + +auto eddsa_sign(const PrivateKey &key, const std::string_view message) + -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::Edwards) { + return std::nullopt; + } + + std::optional result; + auto *context{EVP_MD_CTX_new()}; + if (context != nullptr) { + // EdDSA is a one-shot signature with a null digest, since the curve fixes + // the hash function internally + if (EVP_DigestSignInit(context, nullptr, nullptr, nullptr, internal->key) == + 1) { + result = sign_digest(context, message); + } + + EVP_MD_CTX_free(context); + } + + return result; +} + +} // 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 new file mode 100644 index 000000000..f796a3c8b --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sign_other.cc @@ -0,0 +1,645 @@ +#include +#include + +#include "crypto_bignum.h" +#include "crypto_ecc.h" +#include "crypto_eddsa.h" +#include "crypto_helpers.h" +#include "crypto_pkcs8.h" + +#include // std::array +#include // assert +#include // std::size_t +#include // std::uint8_t, std::uint32_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move, std::unreachable + +namespace sourcemeta::core { +namespace { + +// The DigestInfo prefixes for the EMSA-PKCS1-v1_5 encoding, taken verbatim +// from RFC 8017 Section 9.2 Note 1 +constexpr std::array DIGEST_INFO_SHA256{ + {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}}; +constexpr std::array DIGEST_INFO_SHA384{ + {0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30}}; +constexpr std::array DIGEST_INFO_SHA512{ + {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40}}; + +auto digest_info_prefix(const SignatureHashFunction hash) -> std::string_view { + switch (hash) { + case SignatureHashFunction::SHA256: + return {reinterpret_cast(DIGEST_INFO_SHA256.data()), + DIGEST_INFO_SHA256.size()}; + case SignatureHashFunction::SHA384: + return {reinterpret_cast(DIGEST_INFO_SHA384.data()), + DIGEST_INFO_SHA384.size()}; + case SignatureHashFunction::SHA512: + return {reinterpret_cast(DIGEST_INFO_SHA512.data()), + DIGEST_INFO_SHA512.size()}; + } + + std::unreachable(); +} + +// EMSA-PKCS1-v1_5 encoding (RFC 8017 Section 9.2) +auto build_encoded_message(const SignatureHashFunction hash, + const std::string_view message, + const std::size_t key_length) + -> std::optional { + const auto prefix{digest_info_prefix(hash)}; + const auto digest{digest_message(hash, message)}; + const auto encoded_length{prefix.size() + digest.size()}; + if (key_length < encoded_length + 11) { + return std::nullopt; + } + + std::string result; + result.reserve(key_length); + result.push_back('\x00'); + result.push_back('\x01'); + result.append(key_length - encoded_length - 3, '\xff'); + result.push_back('\x00'); + result.append(prefix); + result.append(digest); + return result; +} + +// MGF1 mask generation (RFC 8017 Appendix B.2.1) +auto mask_generation(const SignatureHashFunction hash, + const std::string_view seed, const std::size_t length) + -> std::string { + std::string result; + result.reserve(length + 64); + std::uint32_t counter{0}; + while (result.size() < length) { + std::string block{seed}; + block.push_back(static_cast((counter >> 24u) & 0xffu)); + block.push_back(static_cast((counter >> 16u) & 0xffu)); + block.push_back(static_cast((counter >> 8u) & 0xffu)); + block.push_back(static_cast(counter & 0xffu)); + result.append(digest_message(hash, block)); + counter += 1; + } + + result.resize(length); + return result; +} + +// EMSA-PSS encoding (RFC 8017 Section 9.1.1), with the salt length fixed to the +// hash function output as RFC 7518 Section 3.5 requires +auto emsa_pss_encode(const SignatureHashFunction hash, + const std::string_view message, + const std::size_t encoded_bits) + -> std::optional { + const auto message_digest{digest_message(hash, message)}; + const auto hash_length{message_digest.size()}; + const auto salt_length{hash_length}; + const auto encoded_length{(encoded_bits + 7) / 8}; + if (encoded_length < hash_length + salt_length + 2) { + return std::nullopt; + } + + const auto salt{random_bytes(salt_length)}; + std::string primed(8, '\x00'); + primed.append(message_digest); + primed.append(salt); + const auto hash_value{digest_message(hash, primed)}; + + const auto database_length{encoded_length - hash_length - 1}; + std::string database(database_length - salt_length - 1, '\x00'); + database.push_back('\x01'); + database.append(salt); + const auto mask{mask_generation(hash, hash_value, database_length)}; + for (std::size_t index = 0; index < database_length; ++index) { + database[index] = static_cast(database[index] ^ mask[index]); + } + + // RFC 8017 Section 9.1.1 step 11: clear the leftmost 8emLen - emBits bits + const auto unused_bits{(8 * encoded_length) - encoded_bits}; + const auto unused_mask{ + static_cast((0xff00u >> unused_bits) & 0xffu)}; + database[0] = static_cast(static_cast(database[0]) & + static_cast(~unused_mask)); + + std::string result{std::move(database)}; + result.append(hash_value); + result.push_back('\xbc'); + return result; +} + +auto to_curve_parameters(const EllipticCurve curve) -> EllipticCurveParameters { + switch (curve) { + case EllipticCurve::P256: + return curve_p256(); + case EllipticCurve::P384: + return curve_p384(); + case EllipticCurve::P521: + return curve_p521(); + } + + std::unreachable(); +} + +struct HashSizes { + std::size_t block_bytes; + std::size_t output_bytes; +}; + +auto hash_sizes(const SignatureHashFunction hash) -> HashSizes { + switch (hash) { + case SignatureHashFunction::SHA256: + return {.block_bytes = 64, .output_bytes = 32}; + case SignatureHashFunction::SHA384: + return {.block_bytes = 128, .output_bytes = 48}; + case SignatureHashFunction::SHA512: + return {.block_bytes = 128, .output_bytes = 64}; + } + + std::unreachable(); +} + +// HMAC (RFC 2104) keyed on the signature hash function, the primitive that the +// deterministic nonce generator is built on +auto hmac(const SignatureHashFunction hash, const std::string_view key, + const std::string_view message) -> std::string { + const auto block_bytes{hash_sizes(hash).block_bytes}; + std::string block_key{key.size() > block_bytes ? digest_message(hash, key) + : std::string{key}}; + block_key.resize(block_bytes, '\x00'); + + std::string inner_input(block_bytes, '\x00'); + std::string outer_input(block_bytes, '\x00'); + for (std::size_t index{0}; index < block_bytes; ++index) { + const auto byte{static_cast(block_key[index])}; + inner_input[index] = static_cast(byte ^ 0x36u); + outer_input[index] = static_cast(byte ^ 0x5cu); + } + + inner_input.append(message); + outer_input.append(digest_message(hash, inner_input)); + return 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 +auto bits2int(const std::string_view bits, const std::size_t order_bits) + -> Bignum { + auto value{bignum_from_bytes(bits)}; + const auto bit_length{bits.size() * 8}; + if (bit_length > order_bits) { + value = bignum_shift_right(value, bit_length - order_bits); + } + + return value; +} + +// RFC 6979 Section 2.3.4 bits2octets, reducing the truncated hash modulo the +// order and encoding it to the fixed octet width +auto bits2octets(const std::string_view bits, const Bignum &order, + const std::size_t order_bits, const std::size_t order_bytes) + -> std::string { + auto value{bits2int(bits, order_bits)}; + bignum_reduce(value, order); + return bignum_to_bytes(value, order_bytes); +} + +auto sign_rsa(const std::string_view modulus, + const std::string_view private_exponent, + const std::string_view encoded_message) -> std::string { + // The exponent is the secret private key, so the exponentiation runs in + // constant time; the exponent copy it consumes is wiped before returning + const auto context{barrett_context(bignum_from_bytes(modulus))}; + auto exponent{bignum_from_bytes(private_exponent)}; + const SecureBignumScope exponent_scope{exponent}; + auto representative{ + bignum_mod_exp_ct(bignum_from_bytes(encoded_message), exponent, context)}; + bignum_normalize(representative); + return bignum_to_bytes(representative, modulus.size()); +} + +// The signature for one nonce candidate (FIPS 186-4 Section 6.4.1), returning +// no value when the candidate must be rejected and a fresh one drawn +auto ecdsa_signature_for_nonce(const Bignum &nonce, + const Bignum &digest_integer, + const Bignum &private_scalar, + const JacobianPoint &generator, + const EllipticCurveParameters ¶meters, + const std::size_t field_bytes) + -> std::optional { + if (bignum_is_zero(nonce) || bignum_compare(nonce, parameters.order) >= 0) { + return std::nullopt; + } + + const auto point{ + point_scalar_multiply_constant_time(nonce, generator, parameters)}; + + // 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 + auto r{point_affine_x_constant_time(point, parameters)}; + bignum_reduce(r, parameters.order); + if (bignum_is_zero(r)) { + return std::nullopt; + } + + // s = k^-1 (z + r * d) mod n, evaluated over the constant-time field + // arithmetic modulo the order. The nonce inverse and every partial sum that + // mixes in the private scalar d carry secret material, so each is wiped + // before returning; the resulting r and s are the public signature + const auto order_field{barrett_context(parameters.order)}; + auto nonce_inverse{field_inverse_ct(nonce, order_field)}; + const SecureBignumScope nonce_inverse_scope{nonce_inverse}; + auto private_reduced{barrett_reduce(private_scalar, order_field)}; + const SecureBignumScope private_reduced_scope{private_reduced}; + auto private_product{field_mod_multiply_ct(r, private_reduced, order_field)}; + const SecureBignumScope private_product_scope{private_product}; + const auto digest_reduced{barrett_reduce(digest_integer, order_field)}; + auto shifted_digest{ + field_add_ct(digest_reduced, private_product, order_field)}; + const SecureBignumScope shifted_digest_scope{shifted_digest}; + auto s{field_mod_multiply_ct(nonce_inverse, shifted_digest, order_field)}; + bignum_normalize(s); + if (bignum_is_zero(s)) { + return std::nullopt; + } + + std::string signature{bignum_to_bytes(r, field_bytes)}; + signature.append(bignum_to_bytes(s, field_bytes)); + return signature; +} + +// ECDSA signature generation (FIPS 186-4 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) +auto sign_ecdsa(const EllipticCurve curve, const SignatureHashFunction hash, + const std::string_view scalar, const std::string_view message) + -> std::optional { + const auto parameters{to_curve_parameters(curve)}; + const auto field_bytes{parameters.field_bytes}; + const auto order_bits{bignum_bit_length(parameters.order)}; + const auto order_bytes{(order_bits + 7) / 8}; + const auto digest{digest_message(hash, message)}; + const auto digest_integer{bits2int(digest, order_bits)}; + auto private_scalar{bignum_from_bytes(scalar)}; + const SecureBignumScope private_scalar_scope{private_scalar}; + const JacobianPoint generator{.x = parameters.generator_x, + .y = parameters.generator_y, + .z = bignum_from_u64(1)}; + + auto private_octets{bignum_to_bytes(private_scalar, order_bytes)}; + const SecureScope private_octets_scope{private_octets}; + const auto hashed_octets{ + bits2octets(digest, parameters.order, order_bits, order_bytes)}; + + // RFC 6979 Section 3.2 steps b to g: seed the HMAC generator. The generator + // state and the private octets are derived from the private key, so each is + // wiped when leaving this function + const auto output_bytes{hash_sizes(hash).output_bytes}; + std::string hmac_value(output_bytes, '\x01'); + const SecureScope hmac_value_scope{hmac_value}; + std::string hmac_key(output_bytes, '\x00'); + const SecureScope hmac_key_scope{hmac_key}; + std::string seed{hmac_value}; + const SecureScope seed_scope{seed}; + seed.push_back('\x00'); + seed.append(private_octets); + seed.append(hashed_octets); + hmac_key = hmac(hash, hmac_key, seed); + hmac_value = hmac(hash, hmac_key, hmac_value); + seed = hmac_value; + seed.push_back('\x01'); + seed.append(private_octets); + seed.append(hashed_octets); + hmac_key = hmac(hash, hmac_key, seed); + hmac_value = hmac(hash, hmac_key, hmac_value); + + for (std::size_t attempt = 0; attempt < 256; ++attempt) { + // RFC 6979 Section 3.2 step h.2: draw enough output to cover the order. The + // candidate is the secret nonce, so it is wiped when the attempt ends + std::string candidate; + const SecureScope candidate_scope{candidate}; + while (candidate.size() * 8 < order_bits) { + hmac_value = hmac(hash, hmac_key, hmac_value); + candidate.append(hmac_value); + } + + auto nonce{bits2int(candidate, order_bits)}; + const SecureBignumScope nonce_scope{nonce}; + const auto signature{ecdsa_signature_for_nonce(nonce, digest_integer, + private_scalar, generator, + parameters, field_bytes)}; + if (signature.has_value()) { + return signature; + } + + // RFC 6979 Section 3.2 step h: reseed before the next candidate + std::string reseed{hmac_value}; + const SecureScope reseed_scope{reseed}; + reseed.push_back('\x00'); + hmac_key = hmac(hash, hmac_key, reseed); + hmac_value = hmac(hash, hmac_key, hmac_value); + } + + return std::nullopt; +} + +} // namespace + +// The reference backend parses the key material into big integers inside each +// signature, so the parsed key simply holds the raw material +struct PrivateKey::Internal { + PrivateKey::Type kind; + std::string modulus; + std::string public_exponent; + std::string private_exponent; + std::string scalar; + EllipticCurve elliptic_curve; + std::string edwards_seed; + EdwardsCurve edwards_curve; + bool rsa_pss_restricted{false}; +}; + +PrivateKey::PrivateKey(Internal *internal) noexcept : internal_{internal} {} + +PrivateKey::~PrivateKey() { + if (internal_ != nullptr) { + secure_zero(internal_->private_exponent); + secure_zero(internal_->scalar); + secure_zero(internal_->edwards_seed); + delete internal_; + } +} + +PrivateKey::PrivateKey(PrivateKey &&other) noexcept + : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PrivateKey::operator=(PrivateKey &&other) noexcept -> PrivateKey & { + if (this != &other) { + if (internal_ != nullptr) { + secure_zero(internal_->private_exponent); + secure_zero(internal_->scalar); + secure_zero(internal_->edwards_seed); + delete internal_; + } + + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PrivateKey::type() const noexcept -> Type { + // A moved-from key holds no state, so reading its kind is a use-after-move + assert(internal_ != nullptr); + return internal_->kind; +} + +auto make_private_key(const std::string_view pem) -> std::optional { + auto der{pem_to_der(pem)}; + if (!der.has_value()) { + return std::nullopt; + } + + // The decoded PKCS#8 holds the whole private key, so it is wiped on return + const SecureScope der_scope{der.value()}; + const auto parsed{parse_pkcs8(der.value())}; + if (!parsed.has_value()) { + return std::nullopt; + } + + 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; + } + + const auto stripped_modulus{strip_left(modulus->content, '\x00')}; + const auto stripped_private_exponent{ + strip_left(private_exponent->content, '\x00')}; + if (stripped_modulus.empty() || + stripped_modulus.size() > MAXIMUM_KEY_BYTES || + stripped_private_exponent.size() > MAXIMUM_KEY_BYTES) { + return std::nullopt; + } + + return PrivateKey{new PrivateKey::Internal{ + .kind = PrivateKey::Type::RSA, + .modulus = std::string{stripped_modulus}, + .public_exponent = + std::string{strip_left(public_exponent->content, '\x00')}, + .private_exponent = std::string{stripped_private_exponent}, + .scalar = {}, + .elliptic_curve = {}, + .edwards_seed = {}, + .edwards_curve = {}, + .rsa_pss_restricted = parsed->rsa_pss_restricted}}; + } + case PKCS8KeyKind::EllipticCurve: { + // RFC 5915 Section 3: ECPrivateKey is a SEQUENCE whose second field is + // the private scalar as an OCTET STRING + 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 scalar{der_read(version->rest)}; + if (!scalar.has_value() || scalar->tag != 0x04) { + return std::nullopt; + } + + 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; + } + + return PrivateKey{new PrivateKey::Internal{ + .kind = PrivateKey::Type::EllipticCurve, + .modulus = {}, + .public_exponent = {}, + .private_exponent = {}, + .scalar = + std::string{pad_left(stripped_scalar, scalar_width, '\x00')}, + .elliptic_curve = parsed->curve, + .edwards_seed = {}, + .edwards_curve = {}}}; + } + case PKCS8KeyKind::Edwards: { + const auto seed{der_read(parsed->key)}; + if (!seed.has_value() || seed->tag != 0x04 || + seed->content.size() != + eddsa_public_key_bytes(parsed->edwards_curve)) { + return std::nullopt; + } + + return PrivateKey{ + new PrivateKey::Internal{.kind = PrivateKey::Type::Edwards, + .modulus = {}, + .public_exponent = {}, + .private_exponent = {}, + .scalar = {}, + .elliptic_curve = {}, + .edwards_seed = std::string{seed->content}, + .edwards_curve = parsed->edwards_curve}}; + } + } + + std::unreachable(); +} + +auto make_ec_private_key(const EllipticCurve curve, + const std::string_view scalar, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + const auto stripped{strip_left(scalar, '\x00')}; + const auto width{curve_field_bytes(curve)}; + // The scalar alone drives signing here, but the public coordinates are still + // range-checked so that malformed input is rejected as on the other backends + if (stripped.empty() || stripped.size() > width || + strip_left(coordinate_x, '\x00').size() > width || + strip_left(coordinate_y, '\x00').size() > width || + !ec_private_scalar_in_range(scalar, curve)) { + return std::nullopt; + } + + return PrivateKey{new PrivateKey::Internal{ + .kind = PrivateKey::Type::EllipticCurve, + .modulus = {}, + .public_exponent = {}, + .private_exponent = {}, + .scalar = std::string{pad_left(stripped, width, '\x00')}, + .elliptic_curve = curve, + .edwards_seed = {}, + .edwards_curve = {}}}; +} + +auto make_edwards_private_key(const EdwardsCurve curve, + const std::string_view seed) + -> std::optional { + if (seed.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + return PrivateKey{new PrivateKey::Internal{.kind = PrivateKey::Type::Edwards, + .modulus = {}, + .public_exponent = {}, + .private_exponent = {}, + .scalar = {}, + .elliptic_curve = {}, + .edwards_seed = std::string{seed}, + .edwards_curve = curve}}; +} + +auto rsassa_pkcs1_v15_sign(const PrivateKey &key, + const SignatureHashFunction hash, + const std::string_view message) + -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + return std::nullopt; + } + + // An id-RSASSA-PSS key is restricted to PSS and must not sign PKCS1v15 + if (internal->rsa_pss_restricted) { + return std::nullopt; + } + + const auto encoded{ + build_encoded_message(hash, message, internal->modulus.size())}; + if (!encoded.has_value()) { + return std::nullopt; + } + + return sign_rsa(internal->modulus, internal->private_exponent, + encoded.value()); +} + +auto rsassa_pss_sign(const PrivateKey &key, const SignatureHashFunction hash, + const std::string_view message) + -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + return std::nullopt; + } + + const auto encoded_bits{ + bignum_bit_length(bignum_from_bytes(internal->modulus)) - 1}; + const auto encoded{emsa_pss_encode(hash, message, encoded_bits)}; + if (!encoded.has_value()) { + return std::nullopt; + } + + return sign_rsa(internal->modulus, internal->private_exponent, + encoded.value()); +} + +auto ecdsa_sign(const PrivateKey &key, const SignatureHashFunction hash, + const std::string_view message) -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || + internal->kind != PrivateKey::Type::EllipticCurve) { + return std::nullopt; + } + + return sign_ecdsa(internal->elliptic_curve, hash, internal->scalar, message); +} + +auto eddsa_sign(const PrivateKey &key, const std::string_view message) + -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::Edwards) { + return std::nullopt; + } + + switch (internal->edwards_curve) { + case EdwardsCurve::Ed25519: + return edwards25519_sign(internal->edwards_seed, message); + case EdwardsCurve::Ed448: + return edwards448_sign(internal->edwards_seed, message); + } + + std::unreachable(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sign_rsa.cc b/vendor/core/src/core/crypto/crypto_sign_rsa.cc new file mode 100644 index 000000000..da3701d4b --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sign_rsa.cc @@ -0,0 +1,96 @@ +#include +#include +#include + +#include "crypto_der.h" +#include "crypto_helpers.h" + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view + +namespace { + +// The rsaEncryption AlgorithmIdentifier (1.2.840.113549.1.1.1 with a NULL +// parameter), taken verbatim as it never varies for an RSA key +constexpr std::array RSA_ALGORITHM_IDENTIFIER{ + {0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, + 0x01, 0x05, 0x00}}; + +} // namespace + +namespace sourcemeta::core { + +// The private key components are assembled into a PKCS#8 document, which every +// backend already parses, rather than teaching each backend to build a native +// key from raw integers +auto make_rsa_private_key( + const std::string_view modulus, const std::string_view public_exponent, + const std::string_view private_exponent, const std::string_view prime1, + const std::string_view prime2, const std::string_view exponent1, + const std::string_view exponent2, const std::string_view coefficient) + -> std::optional { + // Reject empty or oversized components before they drive the document + // allocation, matching the key size limit the parsing paths enforce + const std::array components{ + {modulus, public_exponent, private_exponent, prime1, prime2, exponent1, + exponent2, coefficient}}; + for (const auto component : components) { + const auto stripped{strip_left(component, '\x00')}; + if (stripped.empty() || stripped.size() > MAXIMUM_KEY_BYTES) { + return std::nullopt; + } + } + + // Every intermediate below holds private key material, so each is guarded so + // that it is wiped when the function unwinds, including on an exception from + // a later allocation. This does not reach copies left behind by earlier + // string reallocations, which would need a secret-zeroing allocator to + // eliminate + + // RSAPrivateKey (RFC 8017 Appendix A.1.2), whose version is zero for the + // two-prime form + std::string rsa_private_key_body; + const SecureScope rsa_private_key_body_scope{rsa_private_key_body}; + der_append_unsigned_integer(rsa_private_key_body, std::string_view{}); + der_append_unsigned_integer(rsa_private_key_body, modulus); + der_append_unsigned_integer(rsa_private_key_body, public_exponent); + der_append_unsigned_integer(rsa_private_key_body, private_exponent); + der_append_unsigned_integer(rsa_private_key_body, prime1); + der_append_unsigned_integer(rsa_private_key_body, prime2); + der_append_unsigned_integer(rsa_private_key_body, exponent1); + der_append_unsigned_integer(rsa_private_key_body, exponent2); + der_append_unsigned_integer(rsa_private_key_body, coefficient); + std::string rsa_private_key; + const SecureScope rsa_private_key_scope{rsa_private_key}; + der_append_element(rsa_private_key, 0x30, rsa_private_key_body); + + // PrivateKeyInfo (RFC 5958 Section 2), whose version is likewise zero + std::string document_body; + const SecureScope document_body_scope{document_body}; + der_append_unsigned_integer(document_body, std::string_view{}); + document_body.append( + reinterpret_cast(RSA_ALGORITHM_IDENTIFIER.data()), + RSA_ALGORITHM_IDENTIFIER.size()); + der_append_element(document_body, 0x04, rsa_private_key); + std::string document; + const SecureScope document_scope{document}; + der_append_element(document, 0x30, document_body); + + auto encoded{base64_encode(document)}; + const SecureScope encoded_scope{encoded}; + std::string pem{"-----BEGIN PRIVATE KEY-----\n"}; + const SecureScope pem_scope{pem}; + for (std::size_t offset{0}; offset < encoded.size(); offset += 64) { + pem.append(encoded.substr(offset, 64)); + pem.push_back('\n'); + } + + pem.append("-----END PRIVATE KEY-----\n"); + return make_private_key(pem); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sign_windows.cc b/vendor/core/src/core/crypto/crypto_sign_windows.cc new file mode 100644 index 000000000..76d711c93 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sign_windows.cc @@ -0,0 +1,530 @@ +#include +#include + +#include "crypto_eddsa.h" +#include "crypto_helpers.h" +#include "crypto_pkcs8.h" + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG, LPCWSTR + +#include // BCrypt*, BCRYPT_* + +#include // std::array +#include // std::countl_zero +#include // assert +#include // std::size_t +#include // std::uint8_t +#include // std::memcpy +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move, std::unreachable + +namespace sourcemeta::core { + +// The parsed key keeps both the algorithm provider and the imported key handle +// alive for reuse. The Edwards curves have no CNG primitive, so they keep the +// raw seed and sign through the reference implementation +struct PrivateKey::Internal { + PrivateKey::Type kind; + BCRYPT_ALG_HANDLE algorithm; + BCRYPT_KEY_HANDLE key; + std::size_t field_bytes; + std::string edwards_seed; + EdwardsCurve edwards_curve; + bool rsa_pss_restricted{false}; +}; + +} // namespace sourcemeta::core + +namespace { + +auto to_cng_algorithm( + const sourcemeta::core::SignatureHashFunction hash) noexcept -> LPCWSTR { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return BCRYPT_SHA256_ALGORITHM; + case sourcemeta::core::SignatureHashFunction::SHA384: + return BCRYPT_SHA384_ALGORITHM; + case sourcemeta::core::SignatureHashFunction::SHA512: + return BCRYPT_SHA512_ALGORITHM; + } + + std::unreachable(); +} + +auto to_ecdsa_algorithm(const sourcemeta::core::EllipticCurve curve) noexcept + -> LPCWSTR { + switch (curve) { + case sourcemeta::core::EllipticCurve::P256: + return BCRYPT_ECDSA_P256_ALGORITHM; + case sourcemeta::core::EllipticCurve::P384: + return BCRYPT_ECDSA_P384_ALGORITHM; + case sourcemeta::core::EllipticCurve::P521: + return BCRYPT_ECDSA_P521_ALGORITHM; + } + + std::unreachable(); +} + +auto to_ecc_private_magic(const sourcemeta::core::EllipticCurve curve) noexcept + -> ULONG { + switch (curve) { + case sourcemeta::core::EllipticCurve::P256: + return BCRYPT_ECDSA_PRIVATE_P256_MAGIC; + case sourcemeta::core::EllipticCurve::P384: + return BCRYPT_ECDSA_PRIVATE_P384_MAGIC; + case sourcemeta::core::EllipticCurve::P521: + return BCRYPT_ECDSA_PRIVATE_P521_MAGIC; + } + + std::unreachable(); +} + +struct KeyPair { + BCRYPT_ALG_HANDLE algorithm; + BCRYPT_KEY_HANDLE key; +}; + +auto import_key_pair(const LPCWSTR algorithm_id, const LPCWSTR blob_type, + const std::string_view blob) -> KeyPair { + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptOpenAlgorithmProvider(&algorithm, algorithm_id, nullptr, 0))) { + return {.algorithm = nullptr, .key = nullptr}; + } + + BCRYPT_KEY_HANDLE key{nullptr}; + if (!BCRYPT_SUCCESS(BCryptImportKeyPair( + algorithm, nullptr, blob_type, &key, + reinterpret_cast(const_cast(blob.data())), + static_cast(blob.size()), 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + return {.algorithm = nullptr, .key = nullptr}; + } + + return {.algorithm = algorithm, .key = key}; +} + +// Build a BCRYPT_RSAFULLPRIVATE_BLOB from the PKCS#1 RSAPrivateKey components +auto native_rsa_private_key(const std::string_view rsa_private_key) -> KeyPair { + const auto sequence{sourcemeta::core::der_read(rsa_private_key)}; + if (!sequence.has_value() || sequence->tag != 0x30) { + return {.algorithm = nullptr, .key = nullptr}; + } + + std::array fields{}; + auto rest{sequence->content}; + // Read the version, then modulus, publicExponent, privateExponent, prime1, + // prime2, exponent1, exponent2, coefficient. Only the two-prime form (version + // zero, RFC 8017 Appendix A.1.2) maps onto the full private key blob, so a + // multi-prime key with trailing otherPrimeInfos is rejected + const auto version{sourcemeta::core::der_read(rest)}; + if (!version.has_value() || version->tag != 0x02 || + !sourcemeta::core::strip_left(version->content, '\x00').empty()) { + return {.algorithm = nullptr, .key = nullptr}; + } + + rest = version->rest; + for (auto &field : fields) { + const auto element{sourcemeta::core::der_read(rest)}; + if (!element.has_value() || element->tag != 0x02) { + return {.algorithm = nullptr, .key = nullptr}; + } + + field = sourcemeta::core::strip_left(element->content, '\x00'); + // Every field feeds a blob length or padding width, so oversize input is + // rejected before it drives a cast or an allocation + if (field.size() > sourcemeta::core::MAXIMUM_KEY_BYTES) { + return {.algorithm = nullptr, .key = nullptr}; + } + + rest = element->rest; + } + + const auto modulus{fields[0]}; + const auto public_exponent{fields[1]}; + const auto private_exponent{fields[2]}; + const auto prime1{fields[3]}; + const auto prime2{fields[4]}; + const auto exponent1{fields[5]}; + const auto exponent2{fields[6]}; + const auto coefficient{fields[7]}; + // Bound the key size before the sizes drive allocations and are cast to the + // fixed-width blob length fields, matching the public-key parsing limit + if (modulus.empty() || modulus.size() > sourcemeta::core::MAXIMUM_KEY_BYTES) { + return {.algorithm = nullptr, .key = nullptr}; + } + + const auto modulus_bytes{modulus.size()}; + const auto prime_bytes{prime1.size() > prime2.size() ? prime1.size() + : prime2.size()}; + const auto modulus_bit_length{ + (modulus_bytes * 8u) - static_cast(std::countl_zero( + static_cast(modulus.front())))}; + + BCRYPT_RSAKEY_BLOB header{}; + header.Magic = BCRYPT_RSAFULLPRIVATE_MAGIC; + header.BitLength = static_cast(modulus_bit_length); + header.cbPublicExp = static_cast(public_exponent.size()); + header.cbModulus = static_cast(modulus_bytes); + header.cbPrime1 = static_cast(prime_bytes); + header.cbPrime2 = static_cast(prime_bytes); + + std::string blob; + blob.resize(sizeof(header)); + std::memcpy(blob.data(), &header, sizeof(header)); + blob.append(public_exponent); + blob.append(modulus); + blob.append(sourcemeta::core::pad_left(prime1, prime_bytes, '\x00')); + blob.append(sourcemeta::core::pad_left(prime2, prime_bytes, '\x00')); + blob.append(sourcemeta::core::pad_left(exponent1, prime_bytes, '\x00')); + blob.append(sourcemeta::core::pad_left(exponent2, prime_bytes, '\x00')); + blob.append(sourcemeta::core::pad_left(coefficient, prime_bytes, '\x00')); + blob.append( + sourcemeta::core::pad_left(private_exponent, modulus_bytes, '\x00')); + return import_key_pair(BCRYPT_RSA_ALGORITHM, BCRYPT_RSAFULLPRIVATE_BLOB, + blob); +} + +// Build a BCRYPT_ECCPRIVATE_BLOB from the SEC1 ECPrivateKey scalar and point +auto native_ec_private_key(const sourcemeta::core::EllipticCurve curve, + const std::string_view sec1, + const std::size_t field_bytes) -> KeyPair { + const auto sequence{sourcemeta::core::der_read(sec1)}; + if (!sequence.has_value() || sequence->tag != 0x30) { + return {.algorithm = nullptr, .key = nullptr}; + } + + const auto version{sourcemeta::core::der_read(sequence->content)}; + if (!version.has_value() || version->tag != 0x02) { + return {.algorithm = nullptr, .key = nullptr}; + } + + const auto scalar{sourcemeta::core::der_read(version->rest)}; + if (!scalar.has_value() || scalar->tag != 0x04) { + return {.algorithm = nullptr, .key = nullptr}; + } + + std::string_view point; + auto rest{scalar->rest}; + while (!rest.empty()) { + const auto element{sourcemeta::core::der_read(rest)}; + if (!element.has_value()) { + return {.algorithm = nullptr, .key = nullptr}; + } + + if (element->tag == 0xa1) { + const auto bit_string{sourcemeta::core::der_read(element->content)}; + if (!bit_string.has_value() || bit_string->tag != 0x03 || + bit_string->content.size() != (2 * field_bytes) + 2) { + return {.algorithm = nullptr, .key = nullptr}; + } + + // Skip the unused-bits count and the uncompressed point lead byte + point = bit_string->content.substr(2); + break; + } + + 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) { + return {.algorithm = nullptr, .key = nullptr}; + } + + BCRYPT_ECCKEY_BLOB header{}; + header.dwMagic = to_ecc_private_magic(curve); + header.cbKey = static_cast(field_bytes); + + std::string blob; + blob.resize(sizeof(header)); + std::memcpy(blob.data(), &header, sizeof(header)); + blob.append(point); + blob.append(sourcemeta::core::pad_left(stripped_scalar, field_bytes, '\x00')); + return import_key_pair(to_ecdsa_algorithm(curve), BCRYPT_ECCPRIVATE_BLOB, + blob); +} + +// Build a BCRYPT_ECCPRIVATE_BLOB directly from the caller-provided components +auto native_ec_private_key_components( + const sourcemeta::core::EllipticCurve curve, const std::size_t field_bytes, + const std::string_view scalar, const std::string_view coordinate_x, + const std::string_view coordinate_y) -> KeyPair { + const auto stripped_x{sourcemeta::core::strip_left(coordinate_x, '\x00')}; + const auto stripped_y{sourcemeta::core::strip_left(coordinate_y, '\x00')}; + const auto stripped_scalar{sourcemeta::core::strip_left(scalar, '\x00')}; + if (stripped_scalar.empty() || stripped_x.size() > field_bytes || + stripped_y.size() > field_bytes || stripped_scalar.size() > field_bytes) { + return {.algorithm = nullptr, .key = nullptr}; + } + + BCRYPT_ECCKEY_BLOB header{}; + header.dwMagic = to_ecc_private_magic(curve); + header.cbKey = static_cast(field_bytes); + + std::string blob; + blob.resize(sizeof(header)); + std::memcpy(blob.data(), &header, sizeof(header)); + blob.append(sourcemeta::core::pad_left(stripped_x, field_bytes, '\x00')); + blob.append(sourcemeta::core::pad_left(stripped_y, field_bytes, '\x00')); + blob.append(sourcemeta::core::pad_left(stripped_scalar, field_bytes, '\x00')); + return import_key_pair(to_ecdsa_algorithm(curve), BCRYPT_ECCPRIVATE_BLOB, + blob); +} + +auto sign_hash(BCRYPT_KEY_HANDLE key, void *padding, + const std::string_view digest, const ULONG flags) + -> std::optional { + ULONG length{0}; + auto *digest_data{ + reinterpret_cast(const_cast(digest.data()))}; + if (!BCRYPT_SUCCESS(BCryptSignHash(key, padding, digest_data, + static_cast(digest.size()), nullptr, + 0, &length, flags))) { + return std::nullopt; + } + + std::string signature(length, '\x00'); + if (!BCRYPT_SUCCESS(BCryptSignHash( + key, padding, digest_data, static_cast(digest.size()), + reinterpret_cast(signature.data()), length, &length, + flags))) { + return std::nullopt; + } + + signature.resize(length); + return signature; +} + +} // namespace + +namespace sourcemeta::core { + +PrivateKey::PrivateKey(Internal *internal) noexcept : internal_{internal} {} + +PrivateKey::~PrivateKey() { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + BCryptDestroyKey(internal_->key); + } + + if (internal_->algorithm != nullptr) { + BCryptCloseAlgorithmProvider(internal_->algorithm, 0); + } + + secure_zero(internal_->edwards_seed); + delete internal_; + } +} + +PrivateKey::PrivateKey(PrivateKey &&other) noexcept + : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PrivateKey::operator=(PrivateKey &&other) noexcept -> PrivateKey & { + if (this != &other) { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + BCryptDestroyKey(internal_->key); + } + + if (internal_->algorithm != nullptr) { + BCryptCloseAlgorithmProvider(internal_->algorithm, 0); + } + + secure_zero(internal_->edwards_seed); + delete internal_; + } + + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PrivateKey::type() const noexcept -> Type { + // A moved-from key holds no state, so reading its kind is a use-after-move + assert(internal_ != nullptr); + return internal_->kind; +} + +auto make_private_key(const std::string_view pem) -> std::optional { + auto der{pem_to_der(pem)}; + if (!der.has_value()) { + return std::nullopt; + } + + // The decoded PKCS#8 holds the whole private key, so it is wiped on return + const SecureScope der_scope{der.value()}; + const auto parsed{parse_pkcs8(der.value())}; + if (!parsed.has_value()) { + return std::nullopt; + } + + switch (parsed->kind) { + case PKCS8KeyKind::RSA: { + const auto pair{native_rsa_private_key(parsed->key)}; + if (pair.key == nullptr) { + return std::nullopt; + } + + return PrivateKey{new PrivateKey::Internal{ + .kind = PrivateKey::Type::RSA, + .algorithm = pair.algorithm, + .key = pair.key, + .field_bytes = 0, + .edwards_seed = {}, + .edwards_curve = {}, + .rsa_pss_restricted = parsed->rsa_pss_restricted}}; + } + case PKCS8KeyKind::EllipticCurve: { + const auto field_bytes{curve_field_bytes(parsed->curve)}; + const auto pair{ + native_ec_private_key(parsed->curve, parsed->key, field_bytes)}; + if (pair.key == nullptr) { + return std::nullopt; + } + + return PrivateKey{ + new PrivateKey::Internal{.kind = PrivateKey::Type::EllipticCurve, + .algorithm = pair.algorithm, + .key = pair.key, + .field_bytes = field_bytes, + .edwards_seed = {}, + .edwards_curve = {}}}; + } + case PKCS8KeyKind::Edwards: { + const auto seed{der_read(parsed->key)}; + if (!seed.has_value() || seed->tag != 0x04 || + seed->content.size() != + eddsa_public_key_bytes(parsed->edwards_curve)) { + return std::nullopt; + } + + return PrivateKey{ + new PrivateKey::Internal{.kind = PrivateKey::Type::Edwards, + .algorithm = nullptr, + .key = nullptr, + .field_bytes = 0, + .edwards_seed = std::string{seed->content}, + .edwards_curve = parsed->edwards_curve}}; + } + } + + std::unreachable(); +} + +auto make_ec_private_key(const EllipticCurve curve, + const std::string_view scalar, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + if (!ec_private_scalar_in_range(scalar, curve)) { + return std::nullopt; + } + + const auto field_bytes{curve_field_bytes(curve)}; + const auto pair{native_ec_private_key_components(curve, field_bytes, scalar, + coordinate_x, coordinate_y)}; + if (pair.key == nullptr) { + return std::nullopt; + } + + return PrivateKey{ + new PrivateKey::Internal{.kind = PrivateKey::Type::EllipticCurve, + .algorithm = pair.algorithm, + .key = pair.key, + .field_bytes = field_bytes, + .edwards_seed = {}, + .edwards_curve = {}}}; +} + +auto make_edwards_private_key(const EdwardsCurve curve, + const std::string_view seed) + -> std::optional { + if (seed.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + return PrivateKey{new PrivateKey::Internal{.kind = PrivateKey::Type::Edwards, + .algorithm = nullptr, + .key = nullptr, + .field_bytes = 0, + .edwards_seed = std::string{seed}, + .edwards_curve = curve}}; +} + +auto rsassa_pkcs1_v15_sign(const PrivateKey &key, + const SignatureHashFunction hash, + const std::string_view message) + -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + return std::nullopt; + } + + // An id-RSASSA-PSS key is restricted to PSS and must not sign PKCS1v15 + if (internal->rsa_pss_restricted) { + return std::nullopt; + } + + const auto digest{digest_message(hash, message)}; + BCRYPT_PKCS1_PADDING_INFO padding{}; + padding.pszAlgId = to_cng_algorithm(hash); + return sign_hash(internal->key, &padding, digest, BCRYPT_PAD_PKCS1); +} + +auto rsassa_pss_sign(const PrivateKey &key, const SignatureHashFunction hash, + const std::string_view message) + -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + return std::nullopt; + } + + const auto digest{digest_message(hash, message)}; + BCRYPT_PSS_PADDING_INFO padding{}; + padding.pszAlgId = to_cng_algorithm(hash); + padding.cbSalt = static_cast(digest.size()); + return sign_hash(internal->key, &padding, digest, BCRYPT_PAD_PSS); +} + +auto ecdsa_sign(const PrivateKey &key, const SignatureHashFunction hash, + const std::string_view message) -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || + internal->kind != PrivateKey::Type::EllipticCurve) { + return std::nullopt; + } + + // CNG produces the raw fixed-width R || S concatenation that JWS mandates + const auto digest{digest_message(hash, message)}; + return sign_hash(internal->key, nullptr, digest, 0); +} + +auto eddsa_sign(const PrivateKey &key, const std::string_view message) + -> std::optional { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::Edwards) { + return std::nullopt; + } + + switch (internal->edwards_curve) { + case EdwardsCurve::Ed25519: + return edwards25519_sign(internal->edwards_seed, message); + case EdwardsCurve::Ed448: + return edwards448_sign(internal->edwards_seed, message); + } + + std::unreachable(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_uuid.cc b/vendor/core/src/core/crypto/crypto_uuid.cc index c0aac888a..adeda4c84 100644 --- a/vendor/core/src/core/crypto/crypto_uuid.cc +++ b/vendor/core/src/core/crypto/crypto_uuid.cc @@ -1,10 +1,11 @@ #include -#include // is_hex_digit +#include #include "crypto_random.h" #include // std::array #include // std::size_t +#include // std::uint8_t #include // std::string #include // std::string_view @@ -20,8 +21,8 @@ auto uuidv4() -> std::string { {false, false, false, false, true, false, true, false, true, false, true, false, false, false, false, false}}; - std::array random_bytes{}; - fill_random_bytes(random_bytes); + std::array bytes{}; + fill_random_bytes(bytes); std::string result; result.reserve(36); @@ -30,8 +31,8 @@ auto uuidv4() -> std::string { result += '-'; } - const auto high_nibble = (random_bytes[index] >> 4u) & 0x0fu; - const auto low_nibble = random_bytes[index] & 0x0fu; + const auto high_nibble = (bytes[index] >> 4u) & 0x0fu; + const auto low_nibble = bytes[index] & 0x0fu; // RFC 9562 Section 5.4: version bits (48-51) must be 0b0100 if (index == 6) { diff --git a/vendor/core/src/core/crypto/crypto_verify_apple.cc b/vendor/core/src/core/crypto/crypto_verify_apple.cc index 7f116b0f4..2de9240f0 100644 --- a/vendor/core/src/core/crypto/crypto_verify_apple.cc +++ b/vendor/core/src/core/crypto/crypto_verify_apple.cc @@ -1,6 +1,7 @@ #include #include +#include "crypto_der.h" #include "crypto_eddsa.h" #include "crypto_eddsa_apple.h" #include "crypto_helpers.h" @@ -9,6 +10,7 @@ #include // Sec*, kSec* #include // std::array +#include // assert #include // std::size_t #include // std::optional, std::nullopt #include // std::string @@ -239,7 +241,11 @@ auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { return *this; } -auto PublicKey::type() const noexcept -> Type { return internal_->kind; } +auto PublicKey::type() const noexcept -> Type { + // A moved-from key holds no state, so reading its kind is a use-after-move + assert(internal_ != nullptr); + return internal_->kind; +} auto make_rsa_public_key(const std::string_view modulus, const std::string_view exponent) @@ -248,7 +254,8 @@ auto make_rsa_public_key(const std::string_view modulus, const auto stripped_exponent{strip_left(exponent, '\x00')}; if (stripped_modulus.empty() || stripped_exponent.empty() || stripped_modulus.size() > MAXIMUM_KEY_BYTES || - stripped_exponent.size() > MAXIMUM_KEY_BYTES) { + stripped_exponent.size() > MAXIMUM_KEY_BYTES || + !rsa_public_exponent_acceptable(exponent, modulus)) { return std::nullopt; } diff --git a/vendor/core/src/core/crypto/crypto_verify_openssl.cc b/vendor/core/src/core/crypto/crypto_verify_openssl.cc index 428ba2f1d..51d5a7f44 100644 --- a/vendor/core/src/core/crypto/crypto_verify_openssl.cc +++ b/vendor/core/src/core/crypto/crypto_verify_openssl.cc @@ -10,6 +10,7 @@ #include // OSSL_PARAM_* #include // RSA_PKCS1_PSS_PADDING, RSA_PSS_SALTLEN_DIGEST +#include // assert #include // std::size_t #include // std::optional, std::nullopt #include // std::string @@ -249,7 +250,11 @@ auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { return *this; } -auto PublicKey::type() const noexcept -> Type { return internal_->kind; } +auto PublicKey::type() const noexcept -> Type { + // A moved-from key holds no state, so reading its kind is a use-after-move + assert(internal_ != nullptr); + return internal_->kind; +} auto make_rsa_public_key(const std::string_view modulus, const std::string_view exponent) @@ -258,7 +263,8 @@ auto make_rsa_public_key(const std::string_view modulus, const auto stripped_exponent{strip_left(exponent, '\x00')}; if (stripped_modulus.empty() || stripped_exponent.empty() || stripped_modulus.size() > MAXIMUM_KEY_BYTES || - stripped_exponent.size() > MAXIMUM_KEY_BYTES) { + stripped_exponent.size() > MAXIMUM_KEY_BYTES || + !rsa_public_exponent_acceptable(exponent, modulus)) { return std::nullopt; } diff --git a/vendor/core/src/core/crypto/crypto_verify_other.cc b/vendor/core/src/core/crypto/crypto_verify_other.cc index c441bab43..7f54aeba1 100644 --- a/vendor/core/src/core/crypto/crypto_verify_other.cc +++ b/vendor/core/src/core/crypto/crypto_verify_other.cc @@ -7,6 +7,7 @@ #include "crypto_helpers.h" #include // std::array +#include // assert #include // std::size_t #include // std::uint8_t, std::uint32_t #include // std::optional, std::nullopt @@ -15,6 +16,12 @@ #include // std::unreachable namespace sourcemeta::core { + +// The big integer capacity must hold the product of two maximum-size operands +// plus a normalisation limb and Knuth's implicit leading-zero digit, otherwise +// the modular reduction would index past the fixed-capacity word array +static_assert(Bignum::capacity >= 2 * ((MAXIMUM_KEY_BYTES + 7) / 8) + 2); + namespace { // The DigestInfo prefixes for the EMSA-PKCS1-v1_5 encoding, taken verbatim @@ -368,16 +375,20 @@ auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { return *this; } -auto PublicKey::type() const noexcept -> Type { return internal_->kind; } +auto PublicKey::type() const noexcept -> Type { + // A moved-from key holds no state, so reading its kind is a use-after-move + assert(internal_ != nullptr); + return internal_->kind; +} auto make_rsa_public_key(const std::string_view modulus, const std::string_view exponent) -> std::optional { const auto stripped_modulus{strip_left(modulus, '\x00')}; const auto stripped_exponent{strip_left(exponent, '\x00')}; - if (stripped_modulus.empty() || stripped_exponent.empty() || - stripped_modulus.size() > MAXIMUM_KEY_BYTES || - stripped_exponent.size() > MAXIMUM_KEY_BYTES) { + if (stripped_modulus.empty() || stripped_modulus.size() > MAXIMUM_KEY_BYTES || + stripped_exponent.size() > MAXIMUM_KEY_BYTES || + !rsa_public_exponent_acceptable(exponent, modulus)) { return std::nullopt; } diff --git a/vendor/core/src/core/crypto/crypto_verify_windows.cc b/vendor/core/src/core/crypto/crypto_verify_windows.cc index 0e41c9e1b..6530c92cf 100644 --- a/vendor/core/src/core/crypto/crypto_verify_windows.cc +++ b/vendor/core/src/core/crypto/crypto_verify_windows.cc @@ -11,6 +11,7 @@ #include // BCrypt*, BCRYPT_* #include // std::countl_zero +#include // assert #include // std::size_t #include // std::uint8_t #include // std::memcpy @@ -230,7 +231,11 @@ auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { return *this; } -auto PublicKey::type() const noexcept -> Type { return internal_->kind; } +auto PublicKey::type() const noexcept -> Type { + // A moved-from key holds no state, so reading its kind is a use-after-move + assert(internal_ != nullptr); + return internal_->kind; +} auto make_rsa_public_key(const std::string_view modulus, const std::string_view exponent) @@ -239,7 +244,8 @@ auto make_rsa_public_key(const std::string_view modulus, const auto stripped_exponent{strip_left(exponent, '\x00')}; if (stripped_modulus.empty() || stripped_exponent.empty() || stripped_modulus.size() > MAXIMUM_KEY_BYTES || - stripped_exponent.size() > MAXIMUM_KEY_BYTES) { + stripped_exponent.size() > MAXIMUM_KEY_BYTES || + !rsa_public_exponent_acceptable(exponent, modulus)) { return std::nullopt; } diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto.h index bab7ef2e4..9baf31a3a 100644 --- a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto.h +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto.h @@ -1,8 +1,14 @@ #ifndef SOURCEMETA_CORE_CRYPTO_H_ #define SOURCEMETA_CORE_CRYPTO_H_ +#ifndef SOURCEMETA_CORE_CRYPTO_EXPORT +#include +#endif + /// @defgroup crypto Crypto -/// @brief Cryptographic hash functions, UUID generation, and Base64 codecs. +/// @brief Cryptographic hashing and HMAC, digital signatures, authenticated +/// encryption, random bytes, constant-time comparison, and Base64 and UUID +/// helpers. /// /// This functionality is included as follows: /// @@ -10,14 +16,80 @@ /// #include /// ``` +#include #include #include #include +#include +#include +#include #include #include #include #include +#include #include #include +#include // std::size_t +#include // std::uint8_t +#include // std::span +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup crypto +/// Fill a buffer with random bytes drawn from the operating system's +/// cryptographically secure provider. The bytes are only cryptographically +/// secure when the library is built against a system provider (OpenSSL, the +/// Apple Security framework, or Windows CNG). The reference backend used when +/// no system provider is available falls back to a non-cryptographic generator. +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::array buffer{}; +/// sourcemeta::core::random_bytes(buffer); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT random_bytes(std::span buffer) + -> void; + +/// @ingroup crypto +/// Return the given number of random bytes as a string, drawn from the +/// operating system's cryptographically secure provider. The same backend +/// caveat as the buffer-filling overload applies. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto bytes{sourcemeta::core::random_bytes(32)}; +/// assert(bytes.size() == 32); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT random_bytes(const std::size_t length) + -> std::string; + +/// @ingroup crypto +/// Compare two byte sequences for equality in constant time when their lengths +/// match, returning false immediately when the lengths differ. The comparison +/// does not short-circuit on the first differing byte, so it does not leak the +/// position of a mismatch through timing. The length of the inputs is not +/// treated as secret. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::secure_equals("expected", "expected")); +/// assert(!sourcemeta::core::secure_equals("expected", "actual")); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT secure_equals( + const std::string_view left, const std::string_view right) noexcept -> bool; + +} // namespace sourcemeta::core + #endif diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_aes_gcm.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_aes_gcm.h new file mode 100644 index 000000000..d81865d79 --- /dev/null +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_aes_gcm.h @@ -0,0 +1,55 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_AES_GCM_H_ +#define SOURCEMETA_CORE_CRYPTO_AES_GCM_H_ + +#ifndef SOURCEMETA_CORE_CRYPTO_EXPORT +#include +#endif + +#include // std::optional +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup crypto +/// Seal a plaintext under a 256-bit key using AES-256 in Galois/Counter Mode +/// (NIST SP 800-38D) with a fresh random nonce and no associated data. The +/// result is the self-contained sealed message, the nonce followed by the +/// ciphertext and the authentication tag, and its confidentiality and +/// integrity are both protected when the library is built against a system +/// cryptography provider. Returns no value when the key is not 256 bits, the +/// plaintext is too large to process, or the random nonce could not be drawn. +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto sealed{sourcemeta::core::aes_256_gcm_seal(key, "hello")}; +/// assert(sealed.has_value()); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT +aes_256_gcm_seal(const std::string_view key, const std::string_view plaintext) + -> std::optional; + +/// @ingroup crypto +/// Open a message sealed with aes_256_gcm_seal under the same 256-bit key, +/// returning the original plaintext. Returns no value when the key is not 256 +/// bits, the input is too short to be a sealed message or too large to process, +/// or the authentication tag does not verify, so a tampered or truncated +/// message is rejected rather than decrypted. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto plaintext{sourcemeta::core::aes_256_gcm_unseal(key, sealed)}; +/// assert(plaintext.has_value()); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT +aes_256_gcm_unseal(const std::string_view key, const std::string_view sealed) + -> std::optional; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_hmac_sha256.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_hmac_sha256.h new file mode 100644 index 000000000..21d72fb28 --- /dev/null +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_hmac_sha256.h @@ -0,0 +1,64 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_HMAC_SHA256_H_ +#define SOURCEMETA_CORE_CRYPTO_HMAC_SHA256_H_ + +#ifndef SOURCEMETA_CORE_CRYPTO_EXPORT +#include +#endif + +#include // std::array +#include // std::uint8_t +#include // std::ostream +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup crypto +/// Authenticate a message under a key using HMAC-SHA256, writing the hex +/// digest. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::ostringstream result; +/// sourcemeta::core::hmac_sha256("key", "foo bar", result); +/// std::cout << result.str() << "\n"; +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT hmac_sha256(const std::string_view key, + const std::string_view message, + std::ostream &output) -> void; + +/// @ingroup crypto +/// Authenticate a message under a key using HMAC-SHA256, returning the hex +/// digest as a string. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// std::cout << sourcemeta::core::hmac_sha256("key", "foo bar") << "\n"; +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT hmac_sha256(const std::string_view key, + const std::string_view message) + -> std::string; + +/// @ingroup crypto +/// Authenticate a message under a key using HMAC-SHA256, returning the raw +/// digest bytes. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto digest{sourcemeta::core::hmac_sha256_digest("key", "foo bar")}; +/// assert(digest.size() == 32); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT +hmac_sha256_digest(const std::string_view key, const std::string_view message) + -> std::array; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_hmac_sha384.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_hmac_sha384.h new file mode 100644 index 000000000..14c7099de --- /dev/null +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_hmac_sha384.h @@ -0,0 +1,64 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_HMAC_SHA384_H_ +#define SOURCEMETA_CORE_CRYPTO_HMAC_SHA384_H_ + +#ifndef SOURCEMETA_CORE_CRYPTO_EXPORT +#include +#endif + +#include // std::array +#include // std::uint8_t +#include // std::ostream +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup crypto +/// Authenticate a message under a key using HMAC-SHA384, writing the hex +/// digest. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::ostringstream result; +/// sourcemeta::core::hmac_sha384("key", "foo bar", result); +/// std::cout << result.str() << "\n"; +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT hmac_sha384(const std::string_view key, + const std::string_view message, + std::ostream &output) -> void; + +/// @ingroup crypto +/// Authenticate a message under a key using HMAC-SHA384, returning the hex +/// digest as a string. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// std::cout << sourcemeta::core::hmac_sha384("key", "foo bar") << "\n"; +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT hmac_sha384(const std::string_view key, + const std::string_view message) + -> std::string; + +/// @ingroup crypto +/// Authenticate a message under a key using HMAC-SHA384, returning the raw +/// digest bytes. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto digest{sourcemeta::core::hmac_sha384_digest("key", "foo bar")}; +/// assert(digest.size() == 48); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT +hmac_sha384_digest(const std::string_view key, const std::string_view message) + -> std::array; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_hmac_sha512.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_hmac_sha512.h new file mode 100644 index 000000000..53e126b26 --- /dev/null +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_hmac_sha512.h @@ -0,0 +1,64 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_HMAC_SHA512_H_ +#define SOURCEMETA_CORE_CRYPTO_HMAC_SHA512_H_ + +#ifndef SOURCEMETA_CORE_CRYPTO_EXPORT +#include +#endif + +#include // std::array +#include // std::uint8_t +#include // std::ostream +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup crypto +/// Authenticate a message under a key using HMAC-SHA512, writing the hex +/// digest. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::ostringstream result; +/// sourcemeta::core::hmac_sha512("key", "foo bar", result); +/// std::cout << result.str() << "\n"; +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT hmac_sha512(const std::string_view key, + const std::string_view message, + std::ostream &output) -> void; + +/// @ingroup crypto +/// Authenticate a message under a key using HMAC-SHA512, returning the hex +/// digest as a string. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// std::cout << sourcemeta::core::hmac_sha512("key", "foo bar") << "\n"; +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT hmac_sha512(const std::string_view key, + const std::string_view message) + -> std::string; + +/// @ingroup crypto +/// Authenticate a message under a key using HMAC-SHA512, returning the raw +/// digest bytes. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto digest{sourcemeta::core::hmac_sha512_digest("key", "foo bar")}; +/// assert(digest.size() == 64); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT +hmac_sha512_digest(const std::string_view key, const std::string_view message) + -> std::array; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sign.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sign.h new file mode 100644 index 000000000..5447737ce --- /dev/null +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sign.h @@ -0,0 +1,188 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_SIGN_H_ +#define SOURCEMETA_CORE_CRYPTO_SIGN_H_ + +#ifndef SOURCEMETA_CORE_CRYPTO_EXPORT +#include +#endif + +#include + +#include // std::uint8_t +#include // std::optional +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup crypto +/// A parsed private key that holds the native key, so that the same key can +/// produce many signatures without paying the key construction cost on every +/// call. Build it once with the factory function and pass it to the matching +/// signing function. The signing counterpart to PublicKey. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::make_private_key(pem)}; +/// assert(key.has_value()); +/// const auto signature{sourcemeta::core::rsassa_pkcs1_v15_sign( +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, message)}; +/// assert(signature.has_value()); +/// ``` +class SOURCEMETA_CORE_CRYPTO_EXPORT PrivateKey { +public: + /// The kind of key, which fixes the signature schemes it can produce. + enum class Type : std::uint8_t { + /// The RSA key type. + RSA, + /// The elliptic-curve key type. + EllipticCurve, + /// The Edwards-curve key type. + Edwards + }; + + ~PrivateKey(); + /// Move constructor + PrivateKey(PrivateKey &&other) noexcept; + auto operator=(PrivateKey &&other) noexcept -> PrivateKey &; + PrivateKey(const PrivateKey &) = delete; + auto operator=(const PrivateKey &) -> PrivateKey & = delete; + + /// The kind of key this is. + [[nodiscard]] auto type() const noexcept -> Type; + + /// The backend specific parsed key state, defined by each backend + struct Internal; + /// Take ownership of a parsed key. Prefer the factory function below + explicit PrivateKey(Internal *internal) noexcept; + /// Access the parsed key, which the signing functions read. The type is + /// opaque, so there is nothing a caller can do with it + [[nodiscard]] auto internal() const noexcept -> const Internal * { + return this->internal_; + } + +private: + Internal *internal_; +}; + +/// @ingroup crypto +/// Parse a private key from an unencrypted PKCS#8 PEM document (RFC 5958), +/// returning no value when the input is not a supported RSA, elliptic curve, or +/// Edwards private key. Encrypted documents are not supported. An elliptic +/// curve key must carry its public point, which is optional in SEC1 (RFC 5915), +/// because some backends store the point alongside the private scalar. +auto SOURCEMETA_CORE_CRYPTO_EXPORT make_private_key(const std::string_view pem) + -> std::optional; + +/// @ingroup crypto +/// Parse an elliptic curve private key from its raw big-endian private scalar +/// and public point coordinates, returning no value when the material is +/// malformed. The public coordinates are required because the platform key +/// backends store the point alongside the scalar. +auto SOURCEMETA_CORE_CRYPTO_EXPORT make_ec_private_key( + const EllipticCurve curve, const std::string_view scalar, + const std::string_view coordinate_x, const std::string_view coordinate_y) + -> std::optional; + +/// @ingroup crypto +/// Parse an Edwards-curve private key from its raw seed, returning no value +/// when the seed is the wrong length for the curve. +auto SOURCEMETA_CORE_CRYPTO_EXPORT +make_edwards_private_key(const EdwardsCurve curve, const std::string_view seed) + -> std::optional; + +/// @ingroup crypto +/// Parse an RSA private key from the components of its two-prime form (RFC 8017 +/// Section 3.2), each a raw big-endian integer, in the order modulus, public +/// exponent, private exponent, first prime, second prime, first prime exponent, +/// second prime exponent, and coefficient. Every component is required, as the +/// platform backends import the key from its full private structure rather than +/// recomputing the primes. Returns no value when the material is malformed. +auto SOURCEMETA_CORE_CRYPTO_EXPORT make_rsa_private_key( + const std::string_view modulus, const std::string_view public_exponent, + const std::string_view private_exponent, const std::string_view prime1, + const std::string_view prime2, const std::string_view exponent1, + const std::string_view exponent2, const std::string_view coefficient) + -> std::optional; + +/// @ingroup crypto +/// Produce an RSASSA-PKCS1-v1_5 signature (RFC 8017 Section 8.2.1) over a +/// message, returning no value when the key is not an RSA key. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::make_private_key(pem)}; +/// assert(key.has_value()); +/// const auto signature{sourcemeta::core::rsassa_pkcs1_v15_sign( +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, message)}; +/// assert(signature.has_value()); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT rsassa_pkcs1_v15_sign( + const PrivateKey &key, const SignatureHashFunction hash, + const std::string_view message) -> std::optional; + +/// @ingroup crypto +/// Produce an RSASSA-PSS signature (RFC 8017 Section 8.1.1) over a message, +/// with the salt length fixed to the hash function output as RFC 7518 Section +/// 3.5 requires. Returns no value when the key is not an RSA key. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::make_private_key(pem)}; +/// assert(key.has_value()); +/// const auto signature{sourcemeta::core::rsassa_pss_sign( +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, message)}; +/// assert(signature.has_value()); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT +rsassa_pss_sign(const PrivateKey &key, const SignatureHashFunction hash, + const std::string_view message) -> std::optional; + +/// @ingroup crypto +/// Produce an ECDSA signature (FIPS 186-4 Section 6.4) over a message. The +/// signature is the raw concatenation of the two integers, each padded to the +/// curve field width, as JWS mandates (RFC 7518 Section 3.4), matching what +/// ecdsa_verify expects. Returns no value when the key is not an elliptic curve +/// key. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::make_private_key(pem)}; +/// assert(key.has_value()); +/// const auto signature{sourcemeta::core::ecdsa_sign( +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, message)}; +/// assert(signature.has_value()); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT ecdsa_sign(const PrivateKey &key, + const SignatureHashFunction hash, + const std::string_view message) + -> std::optional; + +/// @ingroup crypto +/// Produce an EdDSA signature (RFC 8032) over a message. There is no separate +/// hash function, as the curve fixes it. Returns no value when the key is not +/// an Edwards curve key. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::make_private_key(pem)}; +/// assert(key.has_value()); +/// const auto signature{sourcemeta::core::eddsa_sign(key.value(), message)}; +/// assert(signature.has_value()); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT eddsa_sign(const PrivateKey &key, + const std::string_view message) + -> std::optional; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_verify.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_verify.h index 38cc7ae35..3e1dedb62 100644 --- a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_verify.h +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_verify.h @@ -13,15 +13,34 @@ namespace sourcemeta::core { /// @ingroup crypto /// The hash functions supported by signature verification. -enum class SignatureHashFunction : std::uint8_t { SHA256, SHA384, SHA512 }; +enum class SignatureHashFunction : std::uint8_t { + /// The SHA-256 hash function. + SHA256, + /// The SHA-384 hash function. + SHA384, + /// The SHA-512 hash function. + SHA512 +}; /// @ingroup crypto /// The NIST elliptic curves supported by signature verification. -enum class EllipticCurve : std::uint8_t { P256, P384, P521 }; +enum class EllipticCurve : std::uint8_t { + /// The NIST P-256 elliptic curve. + P256, + /// The NIST P-384 elliptic curve. + P384, + /// The NIST P-521 elliptic curve. + P521 +}; /// @ingroup crypto /// The Edwards curves supported by signature verification. -enum class EdwardsCurve : std::uint8_t { Ed25519, Ed448 }; +enum class EdwardsCurve : std::uint8_t { + /// The Ed25519 Edwards curve. + Ed25519, + /// The Ed448 Edwards curve. + Ed448 +}; /// @ingroup crypto /// A parsed public key that holds the native key, so that the same key can @@ -42,9 +61,17 @@ enum class EdwardsCurve : std::uint8_t { Ed25519, Ed448 }; class SOURCEMETA_CORE_CRYPTO_EXPORT PublicKey { public: /// The kind of key, which fixes the signature schemes it can verify. - enum class Type : std::uint8_t { RSA, EllipticCurve, Edwards }; + enum class Type : std::uint8_t { + /// The RSA key type. + RSA, + /// The elliptic-curve key type. + EllipticCurve, + /// The Edwards-curve key type. + Edwards + }; ~PublicKey(); + /// Move constructor PublicKey(PublicKey &&other) noexcept; auto operator=(PublicKey &&other) noexcept -> PublicKey &; PublicKey(const PublicKey &) = delete; diff --git a/vendor/core/src/core/dns/idn_hostname.cc b/vendor/core/src/core/dns/idn_hostname.cc index b1d102d6d..97f43464f 100644 --- a/vendor/core/src/core/dns/idn_hostname.cc +++ b/vendor/core/src/core/dns/idn_hostname.cc @@ -5,6 +5,7 @@ #include #include // std::size_t +#include // std::optional #include // std::string, std::u32string #include // std::string_view, std::u32string_view #include // std::vector @@ -19,25 +20,19 @@ static constexpr auto is_idna_label_separator(const char32_t codepoint) codepoint == U'\uFF0E' || codepoint == U'\uFF61'; } -auto is_idn_hostname(const std::string_view value) -> bool { - // No conformance pass against `vendor/unicodetools/IdnaTestV2.txt` is - // wired here. That corpus tests UTS #46, which prepends a mapping pass - // we deliberately do not implement, so strict IDNA 2008 validation - // rejects most of its successful rows. Enabling it would require - // shipping a UTS #46 mapping module first. - - if (value.empty()) { - return false; - } +namespace { - const auto codepoints{utf8_to_utf32(value)}; - if (!codepoints.has_value() || codepoints->empty()) { +// Validate an already-decoded presentation-form domain name against the +// IDNA 2008 rules (RFC 5890-5893). The caller is responsible for any prior +// transformation, such as the UTS #46 mapping. +auto validate_idn_labels(const std::u32string_view codepoints) -> bool { + if (codepoints.empty()) { return false; } // UTS #46 §3.1: a leading or trailing separator means an empty label - if (is_idna_label_separator(codepoints->front()) || - is_idna_label_separator(codepoints->back())) { + if (is_idna_label_separator(codepoints.front()) || + is_idna_label_separator(codepoints.back())) { return false; } @@ -45,12 +40,12 @@ auto is_idn_hostname(const std::string_view value) -> bool { // RFC 1035 §3.1: presentation form ≤ 253 octets in A-label form std::size_t total_octets{0}; std::size_t label_start{0}; - for (std::size_t position = 0; position <= codepoints->size(); ++position) { - const bool at_end{position == codepoints->size()}; - if (!at_end && !is_idna_label_separator((*codepoints)[position])) { + for (std::size_t position = 0; position <= codepoints.size(); ++position) { + const bool at_end{position == codepoints.size()}; + if (!at_end && !is_idna_label_separator(codepoints[position])) { continue; } - const std::u32string_view label{codepoints->data() + label_start, + const std::u32string_view label{codepoints.data() + label_start, position - label_start}; std::u32string decoded; @@ -72,6 +67,13 @@ auto is_idn_hostname(const std::string_view value) -> bool { return false; } } 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, + // which is classified as ALabel rather than Ascii). This matches the + // U-label path, which already rejects such labels. + if (label.size() >= 4 && label[2] == U'-' && label[3] == U'-') { + return false; + } // RFC 1123 §2.1: ASCII labels still need the LDH grammar check std::string ascii; ascii.reserve(label.size()); @@ -131,4 +133,32 @@ auto is_idn_hostname(const std::string_view value) -> bool { return true; } +} // namespace + +auto is_idn_hostname(const std::string_view value) -> bool { + // This is strict IDNA 2008 validation: the input is validated as-is, with + // no mapping pass. Callers that need the UTS #46 lookup behaviour (case + // folding, deviation handling, ignorable removal) should use + // `is_idn_hostname_uts46`, which is what the `vendor/unicodetools` + // IdnaTestV2.txt corpus exercises. + const auto codepoints{utf8_to_utf32(value)}; + if (!codepoints.has_value()) { + return false; + } + + return validate_idn_labels(*codepoints); +} + +auto is_idn_hostname_uts46(const std::string_view value) -> bool { + const auto codepoints{utf8_to_utf32(value)}; + if (!codepoints.has_value()) { + return false; + } + + // UTS #46 §4 steps 1-2: map and normalise before validating. Disallowed + // codepoints are preserved by the mapping and rejected here by the per-label + // validity check. + return validate_idn_labels(idna_uts46_map(*codepoints)); +} + } // namespace sourcemeta::core diff --git a/vendor/core/src/core/dns/include/sourcemeta/core/dns.h b/vendor/core/src/core/dns/include/sourcemeta/core/dns.h index 810f59b27..ba3b1985f 100644 --- a/vendor/core/src/core/dns/include/sourcemeta/core/dns.h +++ b/vendor/core/src/core/dns/include/sourcemeta/core/dns.h @@ -62,6 +62,28 @@ auto is_hostname(const std::string_view value) -> bool; SOURCEMETA_CORE_DNS_EXPORT auto is_idn_hostname(const std::string_view value) -> bool; +/// @ingroup dns +/// Check whether the given string is a valid internationalized host name after +/// UTS #46 processing. The input is first mapped and normalised to NFC under +/// UTS #46 Nontransitional Processing, then the mapped result is validated with +/// the same IDNA 2008 per-label rules as a strict host name. This is the +/// lookup-side behaviour: human-typed forms such as uppercase ASCII and +/// fullwidth characters are folded before validation rather than rejected. See +/// https://www.unicode.org/reports/tr46/ for the algorithm. For example: +/// +/// ```cpp +/// #include +/// +/// #include +/// +/// assert(sourcemeta::core::is_idn_hostname_uts46("www.example.com")); +/// // Uppercase and fullwidth input is mapped before validation +/// assert(sourcemeta::core::is_idn_hostname_uts46("WWW.EXAMPLE.COM")); +/// assert(!sourcemeta::core::is_idn_hostname_uts46("-bad")); +/// ``` +SOURCEMETA_CORE_DNS_EXPORT +auto is_idn_hostname_uts46(const std::string_view value) -> bool; + } // namespace sourcemeta::core #endif diff --git a/vendor/core/src/core/email/email.cc b/vendor/core/src/core/email/email.cc index 51a578ff3..14d10b4de 100644 --- a/vendor/core/src/core/email/email.cc +++ b/vendor/core/src/core/email/email.cc @@ -8,13 +8,21 @@ namespace sourcemeta::core { // RFC 5321 §4.1.2 Mailbox grammar. When AllowUtf8 is true, RFC 6531 §3.3 -// extends atext, qtextSMTP, and sub-domain with UTF8-non-ascii alternatives -template +// extends atext, qtextSMTP, and sub-domain with UTF8-non-ascii alternatives. +// When UseUts46 is also true, the domain is validated under UTS #46 processing +// rather than strict IDNA 2008. +template static auto is_mailbox(const std::string_view value) -> bool { if (value.empty()) { return false; } + // RFC 5321 §4.5.3.1.3: a path is at most 256 octets including the enclosing + // angle brackets, so the mailbox it carries is at most 254 + if (value.size() > 254) { + return false; + } + std::string_view::size_type position{0}; if (value[0] == '"') { @@ -116,7 +124,11 @@ static auto is_mailbox(const std::string_view value) -> bool { if constexpr (AllowUtf8) { // RFC 6531 §3.3: sub-domain =/ U-label - return is_idn_hostname(domain); + if constexpr (UseUts46) { + return is_idn_hostname_uts46(domain); + } else { + return is_idn_hostname(domain); + } } else { // RFC 5321 §4.1.2 Domain matches is_hostname (RFC 1123 §2.1) by // grammar, by 63-octet label cap (RFC 1035 §2.3.4), and by @@ -133,4 +145,8 @@ auto is_idn_email(const std::string_view value) -> bool { return is_mailbox(value); } +auto is_idn_email_uts46(const std::string_view value) -> bool { + return is_mailbox(value); +} + } // namespace sourcemeta::core diff --git a/vendor/core/src/core/email/helpers.h b/vendor/core/src/core/email/helpers.h index 0a37ae526..98965c220 100644 --- a/vendor/core/src/core/email/helpers.h +++ b/vendor/core/src/core/email/helpers.h @@ -100,6 +100,11 @@ inline constexpr auto is_ipv4_address_literal(const std::string_view value) return false; } octets = static_cast(octets + 1); + // A valid literal has exactly four octets, so stop before the counter could + // wrap on a pathological run of segments + if (octets > 4) { + return false; + } if (dot == std::string_view::npos) { break; } diff --git a/vendor/core/src/core/email/include/sourcemeta/core/email.h b/vendor/core/src/core/email/include/sourcemeta/core/email.h index 947c1b939..907d98191 100644 --- a/vendor/core/src/core/email/include/sourcemeta/core/email.h +++ b/vendor/core/src/core/email/include/sourcemeta/core/email.h @@ -56,6 +56,32 @@ auto is_email(const std::string_view value) -> bool; SOURCEMETA_CORE_EMAIL_EXPORT auto is_idn_email(const std::string_view value) -> bool; +/// @ingroup email +/// Check whether the given string is a valid internationalized `Mailbox` per +/// RFC 6531 Section 3.3, validating the domain under UTS #46 processing rather +/// than strict IDNA 2008. The domain is mapped (case folding, compatibility +/// mappings such as fullwidth to ASCII, and removal of ignorable characters) +/// and NFC-normalised before validation, so forms that strict validation +/// rejects, such as fullwidth characters and non-normalised (non-NFC) labels, +/// are accepted. The local part carries no normalisation requirement +/// (RFC 6531) and is validated as-is. See https://www.unicode.org/reports/tr46/ +/// for the algorithm. For example: +/// +/// ```cpp +/// #include +/// +/// #include +/// +/// assert(sourcemeta::core::is_idn_email_uts46("joe.bloggs@example.com")); +/// // The fullwidth domain U+FF41 U+FF42 U+FF43 maps to "abc" and is accepted, +/// // whereas strict IDNA 2008 validation rejects it +/// assert(sourcemeta::core::is_idn_email_uts46( +/// "user@\xef\xbd\x81\xef\xbd\x82\xef\xbd\x83")); +/// assert(!sourcemeta::core::is_idn_email_uts46("2962")); +/// ``` +SOURCEMETA_CORE_EMAIL_EXPORT +auto is_idn_email_uts46(const std::string_view value) -> bool; + } // namespace sourcemeta::core #endif diff --git a/vendor/core/src/core/gzip/gzip.cc b/vendor/core/src/core/gzip/gzip.cc index a27104218..1399880b1 100644 --- a/vendor/core/src/core/gzip/gzip.cc +++ b/vendor/core/src/core/gzip/gzip.cc @@ -4,7 +4,8 @@ extern "C" { #include } -#include // std::unique_ptr +#include // std::min +#include // std::unique_ptr namespace sourcemeta::core { @@ -40,7 +41,8 @@ auto gzip(const std::uint8_t *input, const std::size_t size, const int level) } auto gunzip(const std::uint8_t *input, const std::size_t size, - const std::size_t output_hint) -> std::string { + const std::size_t output_hint, const std::size_t maximum_size) + -> std::string { std::unique_ptr decompressor{libdeflate_alloc_decompressor(), @@ -50,10 +52,26 @@ auto gunzip(const std::uint8_t *input, const std::size_t size, } std::string output; - auto capacity{output_hint > 0 ? output_hint : size * 4}; + // Bound the very first allocation too, so a hint or the size heuristic cannot + // exceed the cap before the loop has a chance to reject the input. The size + // heuristic is guarded so that the multiplication cannot overflow before the + // clamp, since the cap is caller-controlled + std::size_t capacity{0}; + if (output_hint > 0) { + capacity = std::min(output_hint, maximum_size); + } else if (size > maximum_size / 4) { + capacity = maximum_size; + } else { + capacity = size * 4; + } + // Decompress the first member. Every gzip stream has at least one member, so + // a failure here is a real error rather than trailing data + std::size_t total_in{0}; + std::size_t total_out{0}; for (;;) { - std::size_t actual_size{0}; + std::size_t member_in{0}; + std::size_t member_out{0}; auto result{LIBDEFLATE_BAD_DATA}; // libdeflate writes only the decompressed bytes, so leaving the buffer // uninitialised avoids zero-filling multi-megabyte allocations on every @@ -61,24 +79,76 @@ auto gunzip(const std::uint8_t *input, const std::size_t size, output.resize_and_overwrite( capacity, [&](char *const buffer, const std::size_t buffer_size) -> std::size_t { - result = - libdeflate_gzip_decompress(decompressor.get(), input, size, - buffer, buffer_size, &actual_size); + result = libdeflate_gzip_decompress_ex(decompressor.get(), input, + size, buffer, buffer_size, + &member_in, &member_out); return buffer_size; }); if (result == LIBDEFLATE_SUCCESS) { - output.resize(actual_size); - return output; + total_in = member_in; + total_out = member_out; + break; } if (result == LIBDEFLATE_INSUFFICIENT_SPACE) { - capacity *= 2; + if (capacity >= maximum_size) { + throw GZIPError{"Decompressed output exceeds the maximum allowed size"}; + } + + // Double without overflowing: capacity is below the cap here, so doubling + // only runs when the result still fits under it + capacity = (capacity > maximum_size / 2) ? maximum_size : capacity * 2; continue; } throw GZIPError{"Could not decompress input"}; } + + // RFC 1952 Section 2.2 permits concatenated members. Like gzip(1) and the + // streaming decoder, decompress every subsequent member and silently ignore + // trailing data that does not begin a new member, so `resize` here preserves + // the members already decoded + while (total_in < size) { + if (size - total_in < 2 || input[total_in] != 0x1F || + input[total_in + 1] != 0x8B) { + break; + } + + std::size_t member_in{0}; + std::size_t member_out{0}; + for (;;) { + const auto result{libdeflate_gzip_decompress_ex( + decompressor.get(), input + total_in, size - total_in, + output.data() + total_out, output.size() - total_out, &member_in, + &member_out)}; + + if (result == LIBDEFLATE_SUCCESS) { + break; + } + + if (result == LIBDEFLATE_INSUFFICIENT_SPACE) { + if (output.size() >= maximum_size) { + throw GZIPError{ + "Decompressed output exceeds the maximum allowed size"}; + } + + output.resize((output.size() > maximum_size / 2) ? maximum_size + : output.size() * 2); + continue; + } + + // A run that begins with the gzip magic but fails to decode is a corrupt + // member, not trailing data, so it is a real error + throw GZIPError{"Could not decompress input"}; + } + + total_in += member_in; + total_out += member_out; + } + + output.resize(total_out); + return output; } } // namespace sourcemeta::core diff --git a/vendor/core/src/core/gzip/include/sourcemeta/core/gzip.h b/vendor/core/src/core/gzip/include/sourcemeta/core/gzip.h index cd22de5c3..fd7f202b3 100644 --- a/vendor/core/src/core/gzip/include/sourcemeta/core/gzip.h +++ b/vendor/core/src/core/gzip/include/sourcemeta/core/gzip.h @@ -42,7 +42,11 @@ auto SOURCEMETA_CORE_GZIP_EXPORT gzip(const std::uint8_t *input, /// @ingroup gzip /// Decompress a GZIP compressed byte buffer (RFC 1952). An optional output -/// size hint can be provided to avoid repeated buffer resizing. For example: +/// size hint can be provided to avoid repeated buffer resizing. The output is +/// bounded by a maximum size (256 MiB by default) so that a highly compressed +/// input cannot exhaust memory, and decompressing beyond it throws. Every +/// concatenated member is decoded and any trailing data that does not begin a +/// new member is ignored, matching gzip(1). For example: /// /// ```cpp /// #include @@ -53,7 +57,8 @@ auto SOURCEMETA_CORE_GZIP_EXPORT gzip(const std::uint8_t *input, /// ``` auto SOURCEMETA_CORE_GZIP_EXPORT gunzip(const std::uint8_t *input, std::size_t size, - std::size_t output_hint = 0) + std::size_t output_hint = 0, + std::size_t maximum_size = 268435456) -> std::string; } // namespace sourcemeta::core diff --git a/vendor/core/src/core/gzip/include/sourcemeta/core/gzip_error.h b/vendor/core/src/core/gzip/include/sourcemeta/core/gzip_error.h index e7a989765..111a14c1f 100644 --- a/vendor/core/src/core/gzip/include/sourcemeta/core/gzip_error.h +++ b/vendor/core/src/core/gzip/include/sourcemeta/core/gzip_error.h @@ -22,6 +22,7 @@ namespace sourcemeta::core { /// An error that represents a GZIP compression or decompression failure class SOURCEMETA_CORE_GZIP_EXPORT GZIPError : public std::exception { public: + /// Construct an error with the given message GZIPError(const char *message) : message_{message} {} GZIPError(std::string message) = delete; GZIPError(std::string &&message) = delete; diff --git a/vendor/core/src/core/gzip/include/sourcemeta/core/gzip_streambuf.h b/vendor/core/src/core/gzip/include/sourcemeta/core/gzip_streambuf.h index fa711d26c..484872b6d 100644 --- a/vendor/core/src/core/gzip/include/sourcemeta/core/gzip_streambuf.h +++ b/vendor/core/src/core/gzip/include/sourcemeta/core/gzip_streambuf.h @@ -16,6 +16,7 @@ namespace sourcemeta::core { /// an input stream. class SOURCEMETA_CORE_GZIP_EXPORT GZIPStreamBuffer : public std::streambuf { public: + /// Construct a stream buffer over a compressed input stream. GZIPStreamBuffer(std::istream &compressed_stream); ~GZIPStreamBuffer() override; @@ -25,6 +26,7 @@ class SOURCEMETA_CORE_GZIP_EXPORT GZIPStreamBuffer : public std::streambuf { auto operator=(GZIPStreamBuffer &&) -> GZIPStreamBuffer & = delete; protected: + /// Refill the buffer with more decompressed data. auto underflow() -> int_type override; private: diff --git a/vendor/core/src/core/html/include/sourcemeta/core/html_buffer.h b/vendor/core/src/core/html/include/sourcemeta/core/html_buffer.h index ae8a4fa14..e89595fd7 100644 --- a/vendor/core/src/core/html/include/sourcemeta/core/html_buffer.h +++ b/vendor/core/src/core/html/include/sourcemeta/core/html_buffer.h @@ -24,12 +24,14 @@ class SOURCEMETA_CORE_HTML_EXPORT HTMLBuffer { HTMLBuffer(HTMLBuffer &&) = delete; auto operator=(HTMLBuffer &&) -> HTMLBuffer & = delete; + /// Reserve a number of bytes of capacity up front SOURCEMETA_FORCEINLINE inline auto reserve(const std::size_t bytes) -> void { this->buffer_.resize(bytes); this->cursor_ = this->buffer_.data(); this->end_ = this->cursor_ + bytes; } + /// Append a single character to the buffer SOURCEMETA_FORCEINLINE inline auto append(const char character) -> void { if (!this->cursor_ || this->cursor_ >= this->end_) [[unlikely]] { this->grow(1); @@ -39,6 +41,7 @@ class SOURCEMETA_CORE_HTML_EXPORT HTMLBuffer { ++this->cursor_; } + /// Append a sequence of characters to the buffer SOURCEMETA_FORCEINLINE inline auto append(const std::string_view data) -> void { const auto length{data.size()}; @@ -57,6 +60,7 @@ class SOURCEMETA_CORE_HTML_EXPORT HTMLBuffer { this->cursor_ += length; } + /// Get the accumulated contents of the buffer [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto str() -> const std::string & { if (this->cursor_) { @@ -69,6 +73,7 @@ class SOURCEMETA_CORE_HTML_EXPORT HTMLBuffer { return this->buffer_; } + /// Write the accumulated contents to an output stream auto write(std::ostream &stream) -> void; private: 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 4062c412d..b8e560c74 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 @@ -16,12 +16,9 @@ namespace sourcemeta::core { /// HTML character escaping implementation per HTML Living Standard. /// See: https://html.spec.whatwg.org/multipage/parsing.html#escapingString /// -/// This function escapes the five HTML special characters in-place: -/// - `&` becomes `&` -/// - `<` becomes `<` -/// - `>` becomes `>` -/// - `"` becomes `"` -/// - `'` becomes `'` +/// This function escapes the five HTML special characters in-place: the +/// ampersand, less-than sign, greater-than sign, double quote, and apostrophe +/// each become their corresponding HTML entity. /// /// For example: /// @@ -29,9 +26,9 @@ namespace sourcemeta::core { /// #include /// #include /// -/// std::string text{""}; +/// std::string text{"1 < 2 & 3 > 0 'x' \"y\""}; /// sourcemeta::core::html_escape(text); -/// assert(text == "<script>alert('xss')</script>"); +/// assert(text == "1 < 2 & 3 > 0 'x' "y""); /// ``` SOURCEMETA_CORE_HTML_EXPORT auto html_escape(std::string &text) -> void; diff --git a/vendor/core/src/core/http/CMakeLists.txt b/vendor/core/src/core/http/CMakeLists.txt index 588c6c9e9..ad184a816 100644 --- a/vendor/core/src/core/http/CMakeLists.txt +++ b/vendor/core/src/core/http/CMakeLists.txt @@ -10,10 +10,11 @@ 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 SOURCES helpers.h problem.cc match_accept.cc match_accept_language.cc negotiate_encoding.cc from_date.cc format_link.cc field_list.cc accept_includes_all.cc content_type_matches.cc parse_bearer.cc - cache_control_max_age.cc + cache_control_max_age.cc serialize_cookie.cc aws_sigv4.cc ${SOURCEMETA_CORE_HTTP_CLIENT_SOURCE}) if(SOURCEMETA_CORE_INSTALL) @@ -23,6 +24,9 @@ endif() target_link_libraries(sourcemeta_core_http PUBLIC sourcemeta::core::json) target_link_libraries(sourcemeta_core_http PUBLIC sourcemeta::core::text) target_link_libraries(sourcemeta_core_http PRIVATE sourcemeta::core::time) +target_link_libraries(sourcemeta_core_http PRIVATE sourcemeta::core::dns) +target_link_libraries(sourcemeta_core_http PRIVATE sourcemeta::core::crypto) +target_link_libraries(sourcemeta_core_http PRIVATE sourcemeta::core::uri) if(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) find_package(CURL REQUIRED) diff --git a/vendor/core/src/core/http/aws_sigv4.cc b/vendor/core/src/core/http/aws_sigv4.cc new file mode 100644 index 000000000..c0329c9f3 --- /dev/null +++ b/vendor/core/src/core/http/aws_sigv4.cc @@ -0,0 +1,336 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "helpers.h" + +#include // std::sort, std::stable_sort +#include // std::array +#include // std::chrono::system_clock +#include // std::size_t +#include // std::uint8_t +#include // std::optional +#include // std::span +#include // std::string +#include // std::string_view +#include // std::pair, std::move +#include // std::vector + +namespace { + +auto digest_view(const std::array &bytes) + -> std::string_view { + return {reinterpret_cast(bytes.data()), bytes.size()}; +} + +// The signing process stamps these headers, so any previously signed request +// must have them cleared to keep re-signing idempotent +auto is_signing_header(const std::string_view name) -> bool { + return sourcemeta::core::equals_ignore_case(name, "authorization") || + sourcemeta::core::equals_ignore_case(name, "x-amz-date") || + sourcemeta::core::equals_ignore_case(name, "x-amz-content-sha256") || + sourcemeta::core::equals_ignore_case(name, "x-amz-security-token"); +} + +auto sorted_headers( + const std::span> + headers) -> std::vector> { + std::vector> entries; + entries.reserve(headers.size()); + for (const auto &[name, value] : headers) { + entries.emplace_back(name, + sourcemeta::core::http_trim_trailing_ows( + sourcemeta::core::http_trim_leading_ows(value))); + } + + std::ranges::stable_sort( + entries, sourcemeta::core::less_ignore_case, + &std::pair::first); + return entries; +} + +auto append_lowercased(std::string &output, const std::string_view name) + -> void { + for (const auto character : name) { + output.push_back(sourcemeta::core::to_lowercase(character)); + } +} + +auto append_canonical_headers( + std::string &output, + const std::vector> &entries) + -> void { + std::size_t index{0}; + while (index < entries.size()) { + const auto name{entries[index].first}; + append_lowercased(output, name); + output.push_back(':'); + bool first{true}; + while (index < entries.size() && + sourcemeta::core::equals_ignore_case(entries[index].first, name)) { + if (!first) { + output.push_back(','); + } + + sourcemeta::core::squeeze(entries[index].second, ' ', output); + first = false; + ++index; + } + + output.push_back('\n'); + } +} + +auto append_signed_headers( + std::string &output, + const std::vector> &entries) + -> void { + std::string_view previous; + bool first{true}; + for (const auto &[name, value] : entries) { + if (!first && sourcemeta::core::equals_ignore_case(name, previous)) { + continue; + } + + if (!first) { + output.push_back(';'); + } + + append_lowercased(output, name); + previous = name; + first = false; + } +} + +auto append_canonical_uri(std::string &output, std::string_view path, + const bool normalize) -> void { + std::string normalized; + if (normalize) { + // Beyond RFC 3986 dot-segment removal, the default canonicalization also + // collapses redundant slashes, which Amazon S3 does not + normalized = sourcemeta::core::URI::normalize_path( + sourcemeta::core::squeeze(path, '/')); + path = normalized; + } + + if (path.empty()) { + output.push_back('/'); + return; + } + + // Each segment is normalised in place, so a segment that arrives already + // percent-encoded is not encoded a second time + std::size_t start{0}; + while (true) { + const auto slash{path.find('/', start)}; + if (slash == std::string_view::npos) { + sourcemeta::core::URI::escape(path.substr(start), output, true); + break; + } + + sourcemeta::core::URI::escape(path.substr(start, slash - start), output, + true); + output.push_back('/'); + start = slash + 1; + } +} + +auto append_canonical_query(std::string &output, const std::string_view query) + -> void { + if (query.empty()) { + return; + } + + // Each parameter is split off the encoded query, where delimiters are + // unambiguous, then normalised so that an encoded delimiter inside a value + // survives intact + std::vector> parameters; + for (const auto &[name, value] : sourcemeta::core::URI::Query{query}) { + std::string encoded_name; + std::string encoded_value; + sourcemeta::core::URI::escape(name, encoded_name, true); + sourcemeta::core::URI::escape(value, encoded_value, true); + parameters.emplace_back(std::move(encoded_name), std::move(encoded_value)); + } + + std::ranges::sort(parameters); + bool first{true}; + for (const auto &[key, value] : parameters) { + if (!first) { + output.push_back('&'); + } + + output.append(key); + output.push_back('='); + output.append(value); + first = false; + } +} + +} // namespace + +namespace sourcemeta::core { + +auto http_aws_sigv4_canonical_request( + const std::string_view method, const std::string_view path, + const std::string_view query, + const std::span> + headers, + const std::string_view payload_hash, const bool normalize) -> std::string { + const auto entries{sorted_headers(headers)}; + std::string result; + result.append(method); + result.push_back('\n'); + append_canonical_uri(result, path, normalize); + result.push_back('\n'); + append_canonical_query(result, query); + result.push_back('\n'); + append_canonical_headers(result, entries); + result.push_back('\n'); + append_signed_headers(result, entries); + result.push_back('\n'); + result.append(payload_hash); + return result; +} + +auto http_aws_sigv4_signed_headers( + const std::span> + headers) -> std::string { + std::string result; + append_signed_headers(result, sorted_headers(headers)); + return result; +} + +auto http_aws_sigv4_credential_scope(const std::string_view date, + const std::string_view region, + const std::string_view service) + -> std::string { + std::string result; + result.append(date); + result.push_back('/'); + result.append(region); + result.push_back('/'); + result.append(service); + result.append("/aws4_request"); + return result; +} + +auto http_aws_sigv4_string_to_sign(const std::string_view amz_date, + const std::string_view scope, + const std::string_view canonical_request) + -> std::string { + std::string result{"AWS4-HMAC-SHA256\n"}; + result.append(amz_date); + result.push_back('\n'); + result.append(scope); + result.push_back('\n'); + result.append(sha256(canonical_request)); + return result; +} + +auto http_aws_sigv4_signing_key(const std::string_view secret, + const std::string_view date, + const std::string_view region, + const std::string_view service) + -> std::array { + std::string initial{"AWS4"}; + initial.append(secret); + const auto key_date{hmac_sha256_digest(initial, date)}; + const auto key_region{hmac_sha256_digest(digest_view(key_date), region)}; + const auto key_service{hmac_sha256_digest(digest_view(key_region), service)}; + return hmac_sha256_digest(digest_view(key_service), "aws4_request"); +} + +auto http_aws_sigv4_signature(const std::array &signing_key, + const std::string_view string_to_sign) + -> std::string { + return hmac_sha256(digest_view(signing_key), string_to_sign); +} + +auto http_aws_sigv4_authorization(const std::string_view access_key_id, + const std::string_view scope, + const std::string_view signed_headers, + const std::string_view signature) + -> std::string { + std::string result{"AWS4-HMAC-SHA256 Credential="}; + result.append(access_key_id); + result.push_back('/'); + result.append(scope); + result.append(", SignedHeaders="); + result.append(signed_headers); + result.append(", Signature="); + result.append(signature); + return result; +} + +auto HTTPSystemRequest::sign_aws_sigv4( + const HTTPAWSCredentials &credentials, const std::string_view region, + const std::string_view service, + const std::chrono::system_clock::time_point moment) -> HTTPSystemRequest & { + URI url{this->url_}; + // The path and query are signed as they appear in the request target, so + // capture them before canonicalising the URL for the Host authority + const std::string path{url.path().value_or("/")}; + const auto raw_query{url.query()}; + const std::string query{raw_query.has_value() + ? std::string{raw_query.value().raw()} + : std::string{}}; + + // The signed Host must match the authority sent on the wire: canonicalisation + // drops a default port and wraps an IPv6 literal in brackets + url.canonicalize(); + const auto host{url.authority().value_or("")}; + + const auto amz_date{to_iso8601_basic(moment)}; + const std::string_view date{std::string_view{amz_date}.substr(0, 8)}; + const auto payload_hash{sha256( + this->body_.has_value() ? std::string_view{this->body_.value().data} + : std::string_view{})}; + + // Drop any signing headers left over from a previous signature so that + // re-signing replaces them rather than appending duplicates + std::erase_if(this->headers_, [](const auto &entry) -> bool { + return is_signing_header(entry.first); + }); + + this->header("x-amz-date", amz_date); + this->header("x-amz-content-sha256", payload_hash); + if (!credentials.session_token.empty()) { + this->header("x-amz-security-token", + std::string{credentials.session_token}); + } + + // The synthesised Host and Content-Type headers are signed alongside every + // header the caller set, which by now includes the x-amz-* headers above + std::vector> headers; + headers.reserve(this->headers_.size() + 2); + headers.emplace_back("host", host); + if (this->body_.has_value()) { + headers.emplace_back("content-type", this->body_.value().content_type); + } + + for (const auto &[name, value] : this->headers_) { + headers.emplace_back(name, value); + } + + const auto canonical{http_aws_sigv4_canonical_request( + http_method_string(this->method_), path, query, headers, payload_hash, + service != "s3")}; + const auto scope{http_aws_sigv4_credential_scope(date, region, service)}; + const auto string_to_sign{ + http_aws_sigv4_string_to_sign(amz_date, scope, canonical)}; + const auto key{http_aws_sigv4_signing_key(credentials.secret_access_key, date, + region, service)}; + const auto signature{http_aws_sigv4_signature(key, string_to_sign)}; + this->header("Authorization", + http_aws_sigv4_authorization( + credentials.access_key_id, scope, + http_aws_sigv4_signed_headers(headers), signature)); + return *this; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/cache_control_max_age.cc b/vendor/core/src/core/http/cache_control_max_age.cc index 7d3e5c077..d00aebf82 100644 --- a/vendor/core/src/core/http/cache_control_max_age.cc +++ b/vendor/core/src/core/http/cache_control_max_age.cc @@ -24,7 +24,7 @@ auto http_cache_control_max_age(const std::string_view cache_control) noexcept const auto separator{directive.find('=')}; if (separator == std::string_view::npos || - !http_iequals_ascii(http_subview(directive, 0, separator), + !equals_ignore_case(http_subview(directive, 0, separator), "max-age")) { return; } diff --git a/vendor/core/src/core/http/content_type_matches.cc b/vendor/core/src/core/http/content_type_matches.cc index aee92a99f..283e4b0fc 100644 --- a/vendor/core/src/core/http/content_type_matches.cc +++ b/vendor/core/src/core/http/content_type_matches.cc @@ -11,7 +11,7 @@ auto http_content_type_matches(const std::string_view content_type_header, -> bool { const auto bare{ http_trim_leading_ows(http_split_entry(content_type_header).first)}; - return http_iequals_ascii(bare, media_type); + return equals_ignore_case(bare, media_type); } } // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/helpers.h b/vendor/core/src/core/http/helpers.h index 826db7ecc..2a607910b 100644 --- a/vendor/core/src/core/http/helpers.h +++ b/vendor/core/src/core/http/helpers.h @@ -1,6 +1,8 @@ #ifndef SOURCEMETA_CORE_HTTP_HELPERS_H_ #define SOURCEMETA_CORE_HTTP_HELPERS_H_ +#include + #include // std::size_t #include // std::uint8_t, std::uint16_t #include // std::string_view @@ -14,25 +16,6 @@ inline auto http_is_ows(const char character) noexcept -> bool { return character == ' ' || character == '\t'; } -inline auto http_ascii_lower(const char character) noexcept -> char { - return (character >= 'A' && character <= 'Z') - ? static_cast(character + ('a' - 'A')) - : character; -} - -inline auto http_iequals_ascii(const std::string_view left, - const std::string_view right) noexcept -> bool { - if (left.size() != right.size()) { - return false; - } - for (std::size_t index{0}; index < left.size(); ++index) { - if (http_ascii_lower(left[index]) != http_ascii_lower(right[index])) { - return false; - } - } - return true; -} - inline auto http_subview(const std::string_view value, const std::size_t offset, const std::size_t length) noexcept -> std::string_view { @@ -42,7 +25,7 @@ inline auto http_subview(const std::string_view value, const std::size_t offset, inline auto http_media_specificity(const std::string_view range, const std::string_view candidate) noexcept -> std::uint8_t { - if (http_iequals_ascii(range, candidate)) { + if (equals_ignore_case(range, candidate)) { return 3; } if (range == "*/*") { @@ -60,10 +43,9 @@ inline auto http_media_specificity(const std::string_view range, if (range_slash != candidate_slash) { return 0; } - for (std::size_t index{0}; index < range_slash; ++index) { - if (http_ascii_lower(range[index]) != http_ascii_lower(candidate[index])) { - return 0; - } + if (!equals_ignore_case(http_subview(range, 0, range_slash), + http_subview(candidate, 0, range_slash))) { + return 0; } return 2; } 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 caa6ba739..94629f83d 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http.h @@ -6,6 +6,7 @@ #endif // NOLINTBEGIN(misc-include-cleaner) +#include #include #include #include @@ -38,7 +39,9 @@ namespace sourcemeta::core { /// @ingroup http /// A content coding supported by this implementation. enum class HTTPContentEncoding : std::uint8_t { + /// The identity coding that applies no transformation. Identity, + /// The gzip coding per RFC 9110 §8.4.1.3. GZIP, }; @@ -179,8 +182,11 @@ auto http_cache_control_max_age(const std::string_view cache_control) noexcept /// every field, must URI-escape `target`, and must ensure parameter values are /// valid `quoted-string` content. struct HTTPLink { + /// The link target reference std::string_view target; + /// The link relation type std::string_view rel; + /// The additional target attributes of the link std::span> parameters{}; }; @@ -253,6 +259,97 @@ auto http_format_links(std::span links, std::string &out) SOURCEMETA_CORE_HTTP_EXPORT auto http_format_links(std::span links) -> std::string; +/// @ingroup http +/// The `SameSite` attribute of a cookie per RFC 6265bis §5.2. +enum class HTTPCookieSameSite : std::uint8_t { + /// The cookie is withheld from every cross-site request. + Strict, + /// The cookie is sent on top-level cross-site navigations only. + Lax, + /// The cookie is sent on every cross-site request. + None +}; + +/// @ingroup http +/// A cookie to serialise into an RFC 6265 §4.1 `Set-Cookie` response header +/// value. The caller owns the backing storage for every field. A valid cookie +/// has a name that is an RFC 9110 §5.6.2 token and a value made of RFC 6265 +/// §4.1.1 cookie-octets. RFC 6265bis §5.7 requires a cookie with a same-site +/// mode of none to also be secure. +struct HTTPCookie { + /// The cookie name + std::string_view name{}; + /// The cookie value + std::string_view value{}; + /// The path the cookie is scoped to + std::optional path{}; + /// The host the cookie is scoped to + std::optional domain{}; + /// The cookie lifetime + std::optional max_age{}; + /// Whether the cookie is withheld from scripts + bool http_only{false}; + /// Whether the cookie is only sent over secure channels + bool secure{false}; + /// The cross-site request policy for the cookie + std::optional same_site{}; +}; + +/// @ingroup http +/// Test whether a cookie can be serialised into a valid RFC 6265 §4.1 +/// `Set-Cookie` header value: the name is a non-empty RFC 9110 §5.6.2 token, +/// the value is made of RFC 6265 §4.1.1 cookie-octets, any path is made of RFC +/// 6265bis av-octets, any domain is a valid RFC 1123 host name allowing an +/// ignorable leading dot, a present `max_age` is not negative, per RFC 6265bis +/// §5.7 a `HTTPCookieSameSite::None` cookie is also `secure`, and the RFC +/// 6265bis §4.1.3 `__Secure-` and `__Host-` name prefixes carry their required +/// attributes. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_cookie_valid({.name = "a", .value = "b"})); +/// assert(!sourcemeta::core::http_cookie_valid({.name = "a", .value = "b;c"})); +/// ``` +SOURCEMETA_CORE_HTTP_EXPORT +auto http_cookie_valid(const HTTPCookie &cookie) -> bool; + +/// @ingroup http +/// Append an RFC 6265 §4.1 `Set-Cookie` header value to `out`, returning `true` +/// on success. When the cookie is not `http_cookie_valid`, `out` is left +/// unchanged and this returns `false`. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::string buffer; +/// const auto ok{sourcemeta::core::http_serialize_cookie( +/// {.name = "session", .value = "abc", .http_only = true}, buffer)}; +/// assert(ok); +/// assert(buffer == "session=abc; HttpOnly"); +/// ``` +SOURCEMETA_CORE_HTTP_EXPORT +auto http_serialize_cookie(const HTTPCookie &cookie, std::string &out) -> bool; + +/// @ingroup http +/// Serialise an RFC 6265 §4.1 `Set-Cookie` header value, returning no value +/// when the cookie is not `http_cookie_valid`. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto value{sourcemeta::core::http_serialize_cookie( +/// {.name = "session", .value = "abc", .secure = true})}; +/// assert(value == "session=abc; Secure"); +/// ``` +SOURCEMETA_CORE_HTTP_EXPORT +auto http_serialize_cookie(const HTTPCookie &cookie) + -> std::optional; + /// @ingroup http /// Test whether a comma-separated header value per RFC 9110 §5.6.1 lists any /// of the given tokens. For example: diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_aws_sigv4.h b/vendor/core/src/core/http/include/sourcemeta/core/http_aws_sigv4.h new file mode 100644 index 000000000..f962e341c --- /dev/null +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_aws_sigv4.h @@ -0,0 +1,168 @@ +#ifndef SOURCEMETA_CORE_HTTP_AWS_SIGV4_H_ +#define SOURCEMETA_CORE_HTTP_AWS_SIGV4_H_ + +#ifndef SOURCEMETA_CORE_HTTP_EXPORT +#include +#endif + +#include // std::array +#include // std::uint8_t +#include // std::span +#include // std::string +#include // std::string_view +#include // std::pair + +namespace sourcemeta::core { + +/// @ingroup http +/// The credentials used to sign a request with AWS Signature Version 4. The +/// session token is left empty when using long-term credentials. +struct HTTPAWSCredentials { + /// The access key identifier + std::string_view access_key_id; + /// The secret access key + std::string_view secret_access_key; + /// The temporary session token used with short-term credentials + std::string_view session_token; +}; + +/// @ingroup http +/// Compute the AWS Signature Version 4 canonical request from a request method, +/// path, query, headers, and payload hash. Each path segment and query +/// parameter is decoded and re-encoded into canonical form, so the path and +/// query may be passed exactly as they appear in the request target. The path +/// is optionally normalised by collapsing sequential slashes and removing dot +/// segments, which every service except Amazon S3 requires. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// #include +/// +/// const std::vector> headers{ +/// {"Host", "example.amazonaws.com"}, {"X-Amz-Date", "20150830T123600Z"}}; +/// const auto canonical{sourcemeta::core::http_aws_sigv4_canonical_request( +/// "GET", "/", "", headers, +/// "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")}; +/// assert(canonical.starts_with("GET\n/\n")); +/// ``` +auto SOURCEMETA_CORE_HTTP_EXPORT http_aws_sigv4_canonical_request( + const std::string_view method, const std::string_view path, + const std::string_view query, + const std::span> + headers, + const std::string_view payload_hash, const bool normalize = true) + -> std::string; + +/// @ingroup http +/// Compute the AWS Signature Version 4 signed headers list, the lowercased +/// header names sorted and joined with a semicolon. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// #include +/// +/// const std::vector> headers{ +/// {"Host", "example.amazonaws.com"}, {"X-Amz-Date", "20150830T123600Z"}}; +/// assert(sourcemeta::core::http_aws_sigv4_signed_headers(headers) == +/// "host;x-amz-date"); +/// ``` +auto SOURCEMETA_CORE_HTTP_EXPORT http_aws_sigv4_signed_headers( + const std::span> + headers) -> std::string; + +/// @ingroup http +/// Compute the AWS Signature Version 4 credential scope from a date, region, +/// and service. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_aws_sigv4_credential_scope( +/// "20150830", "us-east-1", "service") == +/// "20150830/us-east-1/service/aws4_request"); +/// ``` +auto SOURCEMETA_CORE_HTTP_EXPORT http_aws_sigv4_credential_scope( + const std::string_view date, const std::string_view region, + const std::string_view service) -> std::string; + +/// @ingroup http +/// Compute the AWS Signature Version 4 string to sign from a timestamp, a +/// credential scope, and a canonical request. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto result{sourcemeta::core::http_aws_sigv4_string_to_sign( +/// "20150830T123600Z", "20150830/us-east-1/service/aws4_request", +/// "canonical request")}; +/// assert(result.starts_with("AWS4-HMAC-SHA256\n20150830T123600Z\n")); +/// ``` +auto SOURCEMETA_CORE_HTTP_EXPORT http_aws_sigv4_string_to_sign( + const std::string_view amz_date, const std::string_view scope, + const std::string_view canonical_request) -> std::string; + +/// @ingroup http +/// Derive the AWS Signature Version 4 signing key from a secret access key, +/// date, region, and service. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::http_aws_sigv4_signing_key( +/// "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", "20150830", "us-east-1", +/// "service")}; +/// assert(key.size() == 32); +/// ``` +auto SOURCEMETA_CORE_HTTP_EXPORT http_aws_sigv4_signing_key( + const std::string_view secret, const std::string_view date, + const std::string_view region, const std::string_view service) + -> std::array; + +/// @ingroup http +/// Compute the AWS Signature Version 4 hex signature from a signing key and a +/// string to sign. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::http_aws_sigv4_signing_key( +/// "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", "20150830", "us-east-1", +/// "service")}; +/// assert(sourcemeta::core::http_aws_sigv4_signature(key, "string to sign") +/// .size() == 64); +/// ``` +auto SOURCEMETA_CORE_HTTP_EXPORT +http_aws_sigv4_signature(const std::array &signing_key, + const std::string_view string_to_sign) -> std::string; + +/// @ingroup http +/// Assemble the AWS Signature Version 4 `Authorization` header value from an +/// access key, credential scope, signed headers, and signature. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto header{sourcemeta::core::http_aws_sigv4_authorization( +/// "AKIDEXAMPLE", "20150830/us-east-1/service/aws4_request", +/// "host;x-amz-date", "signature")}; +/// assert(header.starts_with("AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/")); +/// ``` +auto SOURCEMETA_CORE_HTTP_EXPORT http_aws_sigv4_authorization( + const std::string_view access_key_id, const std::string_view scope, + const std::string_view signed_headers, const std::string_view signature) + -> std::string; + +} // 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 45975ee01..86b5a3a97 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 @@ -38,6 +38,7 @@ namespace sourcemeta::core { /// ``` class SOURCEMETA_CORE_HTTP_EXPORT HTTPError : public std::runtime_error { public: + /// Construct an error from the request method, URL, and a message HTTPError(const HTTPMethod method, std::string url, const std::string &message) : std::runtime_error{message}, method_{method}, url_{std::move(url)} {} @@ -72,6 +73,8 @@ class SOURCEMETA_CORE_HTTP_EXPORT HTTPError : public std::runtime_error { /// ``` class SOURCEMETA_CORE_HTTP_EXPORT HTTPStatusError : public HTTPError { public: + /// Construct an error from the request method, URL, and the unsuccessful + /// response status HTTPStatusError(const HTTPMethod method, std::string url, const HTTPStatus &status) : HTTPError{method, std::move(url), "Unsuccessful HTTP response"}, 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 c6a62322b..c5e87526a 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 @@ -145,6 +145,90 @@ inline auto http_parse_headers(const std::string_view input, Callback callback) } } +/// @ingroup http +/// Parse the value of an RFC 6265 §4.2 `Cookie` request header, given without +/// the field name, into its cookie-pairs, invoking the callback once per pair +/// with the name and value. A request cookie header carries only names and +/// values, never attributes. Surrounding whitespace is trimmed, values are +/// otherwise reported verbatim, and pairs that lack a `=` or have an empty name +/// are skipped. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::string_view last_value; +/// sourcemeta::core::http_parse_cookies( +/// "session=abc; theme=dark", +/// [&last_value](const std::string_view, const std::string_view value) { +/// last_value = value; +/// }); +/// assert(last_value == "dark"); +/// ``` +template + requires std::invocable +inline auto http_parse_cookies(const std::string_view input, Callback callback) + -> void { + std::string_view rest{input}; + while (!rest.empty()) { + std::string_view pair; + const auto separator{rest.find(';')}; + if (separator == std::string_view::npos) { + pair = rest; + rest = {}; + } else { + pair = rest.substr(0, separator); + rest = rest.substr(separator + 1); + } + + const auto parts{split_once(trim(pair), '=')}; + if (!parts.has_value()) { + continue; + } + + const auto name{trim(parts->first)}; + if (name.empty()) { + continue; + } + + callback(name, trim(parts->second)); + } +} + +/// @ingroup http +/// Parse the value of an RFC 6265 §4.2 `Cookie` request header, given without +/// the field name, into any container of name and value pairs. The names and +/// values are forwarded as views that borrow from `input`, so a container of +/// `std::string_view` pairs collects them without copying, while a container of +/// `std::string` pairs owns them. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// #include +/// +/// std::vector> cookies; +/// sourcemeta::core::http_parse_cookies("session=abc; theme=dark", cookies); +/// assert(cookies.size() == 2); +/// assert(cookies.at(0).first == "session"); +/// assert(cookies.at(0).second == "abc"); +/// ``` +template + requires requires(Container container, std::string_view entry) { + container.emplace_back(entry, entry); + } +inline auto http_parse_cookies(const std::string_view input, Container &cookies) + -> void { + http_parse_cookies(input, + [&cookies](const std::string_view name, + const std::string_view value) -> void { + cookies.emplace_back(name, value); + }); +} + /// @ingroup http /// Parse the field lines of a raw message header block, skipping the start /// line, into any container of name and value pairs, normalising names to 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 767a1c11c..9e89ccfd4 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 @@ -10,14 +10,23 @@ namespace sourcemeta::core { /// @ingroup http /// A request method per RFC 9110 §9.3 and RFC 5789 §2. enum class HTTPMethod : std::uint8_t { + /// Transfer a current representation of the target resource. GET, + /// Identical to a retrieval but without transferring the response content. HEAD, + /// Process the enclosed representation according to the resource semantics. POST, + /// Replace the target resource with the enclosed representation. PUT, + /// Remove the association between the target resource and its function. DELETE, + /// Establish a tunnel to the server identified by the target resource. CONNECT, + /// Describe the communication options available for the target resource. OPTIONS, + /// Perform a message loop-back test along the path to the target resource. TRACE, + /// Apply a partial modification to the target resource per RFC 5789 §2. PATCH }; diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_problem.h b/vendor/core/src/core/http/include/sourcemeta/core/http_problem.h index a89b3ed74..e6813b841 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http_problem.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_problem.h @@ -13,10 +13,15 @@ namespace sourcemeta::core { /// @ingroup http /// Fields of an RFC 9457 §3.1 Problem Details object. struct HTTPProblemDetails { + /// The HTTP status code for this occurrence of the problem HTTPStatus status; + /// The identifier for the problem type JSON::StringView type{"about:blank"}; + /// The short human-readable summary of the problem type JSON::StringView title{}; + /// The human-readable explanation specific to this occurrence JSON::StringView detail{}; + /// The identifier for this specific occurrence of the problem JSON::StringView instance{}; }; diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_status.h b/vendor/core/src/core/http/include/sourcemeta/core/http_status.h index c95c6235e..53a1aae1f 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http_status.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_status.h @@ -18,8 +18,11 @@ namespace sourcemeta::core { /// assert(sourcemeta::core::HTTP_STATUS_OK.wire == "200 OK"); /// ``` struct HTTPStatus { + /// The numeric status code std::uint16_t code; + /// The reason phrase std::string_view phrase; + /// The status code and reason phrase in their wire form std::string_view wire; constexpr auto operator==(const HTTPStatus &) const -> bool = default; diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_system.h b/vendor/core/src/core/http/include/sourcemeta/core/http_system.h index 2d85dcbff..6e2c7431b 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http_system.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_system.h @@ -5,16 +5,19 @@ #include #endif +#include #include #include +#include -#include // std::chrono::milliseconds, std::chrono::seconds -#include // std::size_t -#include // std::optional -#include // std::runtime_error -#include // std::string -#include // std::move, std::pair -#include // std::vector +#include // std::chrono::milliseconds, std::chrono::seconds +#include // std::size_t +#include // std::optional +#include // std::runtime_error +#include // std::string +#include // std::string_view +#include // std::move, std::pair +#include // std::vector namespace sourcemeta::core { @@ -67,6 +70,8 @@ struct HTTPResponse { class SOURCEMETA_CORE_HTTP_EXPORT HTTPSystemBackendError : public std::runtime_error { public: + /// Construct an error from a message, the environment variable that overrides + /// the backend path, and the paths that were searched HTTPSystemBackendError(const std::string &message, std::string variable, std::vector paths) : std::runtime_error{message}, variable_{std::move(variable)}, @@ -107,6 +112,7 @@ class SOURCEMETA_CORE_HTTP_EXPORT HTTPSystemBackendError /// ``` class SOURCEMETA_CORE_HTTP_EXPORT HTTPSystemRequest { public: + /// Construct a request for the given URL and method explicit HTTPSystemRequest(std::string url, const HTTPMethod method = HTTPMethod::GET) : url_{std::move(url)}, method_{method} {} @@ -123,6 +129,33 @@ class SOURCEMETA_CORE_HTTP_EXPORT HTTPSystemRequest { return *this; } + /// Get the value of the first header with the given name, compared case + /// insensitively, or no value when no such header was added. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// sourcemeta::core::HTTPSystemRequest request{"https://example.com"}; + /// request.header("Accept", "application/json"); + /// assert(request.header("accept").value() == "application/json"); + /// ``` + [[nodiscard]] auto header(const std::string_view name) const + -> std::optional { + for (const auto &[key, value] : this->headers_) { + if (equals_ignore_case(key, name)) { + return value; + } + } + + return std::nullopt; + } + + /// Get the request headers configured so far, in the order they were added + [[nodiscard]] auto headers() const noexcept -> const auto & { + return this->headers_; + } + /// Set the request body, sent along with the given `Content-Type` header auto body(std::string data, std::string content_type) -> HTTPSystemRequest & { this->body_ = @@ -162,6 +195,29 @@ class SOURCEMETA_CORE_HTTP_EXPORT HTTPSystemRequest { return *this; } + /// Sign this request with AWS Signature Version 4, stamping the `x-amz-date`, + /// `x-amz-content-sha256`, and `Authorization` headers, plus + /// `x-amz-security-token` when the credentials carry a session token. The + /// path is normalised for every service except Amazon S3. The timestamp + /// defaults to the current time. For example: + /// + /// ```cpp + /// #include + /// + /// sourcemeta::core::HTTPSystemRequest request{ + /// "https://example.s3.amazonaws.com/key", + /// sourcemeta::core::HTTPMethod::GET}; + /// request.sign_aws_sigv4({.access_key_id = "AKIDEXAMPLE", + /// .secret_access_key = "secret"}, + /// "us-east-1", "s3"); + /// ``` + auto sign_aws_sigv4(const HTTPAWSCredentials &credentials, + const std::string_view region, + const std::string_view service, + const std::chrono::system_clock::time_point moment = + std::chrono::system_clock::now()) + -> HTTPSystemRequest &; + /// Perform the request. A failure to obtain a response is reported as an /// error, while unsuccessful status codes are returned on the result [[nodiscard]] auto send() const -> HTTPResponse; diff --git a/vendor/core/src/core/http/match_accept_language.cc b/vendor/core/src/core/http/match_accept_language.cc index caece96ce..422b97fb9 100644 --- a/vendor/core/src/core/http/match_accept_language.cc +++ b/vendor/core/src/core/http/match_accept_language.cc @@ -15,11 +15,11 @@ auto language_specificity(const std::string_view range, if (range == "*") { return 1; } - if (sourcemeta::core::http_iequals_ascii(range, candidate)) { + if (sourcemeta::core::equals_ignore_case(range, candidate)) { return candidate.size() + 1; } if (range.size() > candidate.size() && range[candidate.size()] == '-' && - sourcemeta::core::http_iequals_ascii( + sourcemeta::core::equals_ignore_case( sourcemeta::core::http_subview(range, 0, candidate.size()), candidate)) { return candidate.size(); @@ -62,9 +62,13 @@ auto http_match_accept_language( if (specificity == 0) { return; } - if (quality > candidate_quality || - (quality == candidate_quality && - specificity > candidate_specificity)) { + // RFC 9110 Section 12.4.2: a quality of zero means "not acceptable". + // Honour that by letting the most specific matching range govern, so + // an explicit refusal of a specific range is not overridden by a less + // specific range that offers a higher quality + if (specificity > candidate_specificity || + (specificity == candidate_specificity && + quality > candidate_quality)) { candidate_quality = quality; candidate_specificity = specificity; } diff --git a/vendor/core/src/core/http/negotiate_encoding.cc b/vendor/core/src/core/http/negotiate_encoding.cc index 09f89fcc0..a0df72528 100644 --- a/vendor/core/src/core/http/negotiate_encoding.cc +++ b/vendor/core/src/core/http/negotiate_encoding.cc @@ -26,13 +26,13 @@ auto http_negotiate_encoding( http_for_each_accept_entry( accept_encoding_header, [&](const std::string_view token, const float quality) -> void { - if (http_iequals_ascii(token, "gzip") || - http_iequals_ascii(token, "x-gzip")) { + if (equals_ignore_case(token, "gzip") || + equals_ignore_case(token, "x-gzip")) { gzip_listed = true; if (quality > gzip_quality) { gzip_quality = quality; } - } else if (http_iequals_ascii(token, "identity")) { + } else if (equals_ignore_case(token, "identity")) { identity_listed = true; if (quality > identity_quality) { identity_quality = quality; diff --git a/vendor/core/src/core/http/parse_bearer.cc b/vendor/core/src/core/http/parse_bearer.cc index aaa3ca48c..cb6c8bca7 100644 --- a/vendor/core/src/core/http/parse_bearer.cc +++ b/vendor/core/src/core/http/parse_bearer.cc @@ -50,7 +50,7 @@ auto http_parse_bearer(const std::string_view authorization) noexcept return {}; } - if (!http_iequals_ascii(http_subview(authorization, 0, scheme.size()), + if (!equals_ignore_case(http_subview(authorization, 0, scheme.size()), scheme)) { return {}; } diff --git a/vendor/core/src/core/http/serialize_cookie.cc b/vendor/core/src/core/http/serialize_cookie.cc new file mode 100644 index 000000000..008b653ac --- /dev/null +++ b/vendor/core/src/core/http/serialize_cookie.cc @@ -0,0 +1,278 @@ +#include +#include +#include + +#include // std::array +#include // std::to_chars +#include // std::chrono::seconds +#include // std::size_t +#include // std::numeric_limits +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::unreachable + +namespace { + +auto is_token_character(const char character) noexcept -> bool { + // RFC 9110 §5.6.2: tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / + // "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA + switch (character) { + case '!': + case '#': + case '$': + case '%': + case '&': + case '\'': + case '*': + case '+': + case '-': + case '.': + case '^': + case '_': + case '`': + case '|': + case '~': + return true; + default: + return sourcemeta::core::is_alphanum(character); + } +} + +auto is_cookie_octet(const char character) noexcept -> bool { + // RFC 6265 §4.1.1: cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / + // %x5D-7E, that is US-ASCII excluding CTLs, whitespace, DQUOTE, comma, + // semicolon, and backslash + const auto value{static_cast(character)}; + return value == 0x21 || (value >= 0x23 && value <= 0x2B) || + (value >= 0x2D && value <= 0x3A) || (value >= 0x3C && value <= 0x5B) || + (value >= 0x5D && value <= 0x7E); +} + +auto is_attribute_octet(const char character) noexcept -> bool { + // RFC 6265bis §4.1.1: path-value and domain-value are made of av-octet = + // %x20-3A / %x3C-7E, that is printable US-ASCII excluding the ";" delimiter, + // control characters, DEL, and any non-ASCII byte + const auto value{static_cast(character)}; + return value >= 0x20 && value <= 0x7E && character != ';'; +} + +auto is_token(const std::string_view value) noexcept -> bool { + if (value.empty()) { + return false; + } + + for (const auto character : value) { + if (!is_token_character(character)) { + return false; + } + } + + return true; +} + +auto is_cookie_value(const std::string_view value) noexcept -> bool { + // RFC 6265 §4.1.1: cookie-value is either bare cookie-octets or the same + // wrapped in a matched pair of double quotes + for (const auto character : sourcemeta::core::unquote(value, '"')) { + if (!is_cookie_octet(character)) { + return false; + } + } + + return true; +} + +auto is_attribute_value(const std::string_view value) noexcept -> bool { + for (const auto character : value) { + if (!is_attribute_octet(character)) { + return false; + } + } + + return true; +} + +auto is_cookie_domain(const std::string_view value) -> bool { + // RFC 6265 §4.1.2.3 lets a domain carry an ignorable leading dot, which is + // dropped before the remainder is checked as an RFC 1123 host name + const auto host{value.starts_with('.') ? value.substr(1) : value}; + return sourcemeta::core::is_hostname(host); +} + +constexpr std::string_view COOKIE_PAIR_SEPARATOR{"="}; +constexpr std::string_view COOKIE_ATTRIBUTE_SEPARATOR{"; "}; +constexpr std::string_view COOKIE_DOMAIN{"Domain="}; +constexpr std::string_view COOKIE_PATH{"Path="}; +constexpr std::string_view COOKIE_MAX_AGE{"Max-Age="}; +constexpr std::string_view COOKIE_SECURE{"Secure"}; +constexpr std::string_view COOKIE_HTTP_ONLY{"HttpOnly"}; +constexpr std::string_view COOKIE_SAME_SITE{"SameSite="}; +constexpr std::string_view COOKIE_SAME_SITE_STRICT{"Strict"}; +constexpr std::string_view COOKIE_SAME_SITE_LAX{"Lax"}; +constexpr std::string_view COOKIE_SAME_SITE_NONE{"None"}; + +auto same_site_token(const sourcemeta::core::HTTPCookieSameSite value) noexcept + -> std::string_view { + switch (value) { + case sourcemeta::core::HTTPCookieSameSite::Strict: + return COOKIE_SAME_SITE_STRICT; + case sourcemeta::core::HTTPCookieSameSite::Lax: + return COOKIE_SAME_SITE_LAX; + case sourcemeta::core::HTTPCookieSameSite::None: + return COOKIE_SAME_SITE_NONE; + } + + std::unreachable(); +} + +auto required_size(const sourcemeta::core::HTTPCookie &cookie, + const std::string_view max_age) noexcept -> std::size_t { + std::size_t size{cookie.name.size() + COOKIE_PAIR_SEPARATOR.size() + + cookie.value.size()}; + if (cookie.domain.has_value()) { + size += COOKIE_ATTRIBUTE_SEPARATOR.size() + COOKIE_DOMAIN.size() + + cookie.domain->size(); + } + if (cookie.path.has_value()) { + size += COOKIE_ATTRIBUTE_SEPARATOR.size() + COOKIE_PATH.size() + + cookie.path->size(); + } + if (cookie.max_age.has_value()) { + size += COOKIE_ATTRIBUTE_SEPARATOR.size() + COOKIE_MAX_AGE.size() + + max_age.size(); + } + if (cookie.secure) { + size += COOKIE_ATTRIBUTE_SEPARATOR.size() + COOKIE_SECURE.size(); + } + if (cookie.http_only) { + size += COOKIE_ATTRIBUTE_SEPARATOR.size() + COOKIE_HTTP_ONLY.size(); + } + if (cookie.same_site.has_value()) { + size += COOKIE_ATTRIBUTE_SEPARATOR.size() + COOKIE_SAME_SITE.size() + + same_site_token(*cookie.same_site).size(); + } + return size; +} + +} // namespace + +namespace sourcemeta::core { + +auto http_cookie_valid(const HTTPCookie &cookie) -> bool { + if (!is_token(cookie.name) || !is_cookie_value(cookie.value)) { + return false; + } + + if (cookie.path.has_value() && !is_attribute_value(*cookie.path)) { + return false; + } + + if (cookie.domain.has_value() && !is_cookie_domain(*cookie.domain)) { + return false; + } + + // RFC 6265bis §4.1.1 defines the max age attribute value as one or more + // digits, so a negative expiry cannot be serialised. Zero or a positive value + // is valid, and zero is the canonical way to expire a cookie + if (cookie.max_age.has_value() && cookie.max_age->count() < 0) { + return false; + } + + // RFC 6265bis §5.7 ignores a cookie whose same-site mode is none unless it is + // also marked secure + if (cookie.same_site == HTTPCookieSameSite::None && !cookie.secure) { + return false; + } + + // RFC 6265bis §4.1.3.1 requires a cookie whose name carries the secure prefix + // to be marked secure. The prefix is matched case-insensitively because RFC + // 6265bis §5.4 has user agents do so, and a mismatched cookie is dropped + if (starts_with_ignore_case(cookie.name, "__Secure-") && !cookie.secure) { + return false; + } + + // RFC 6265bis §4.1.3.2 requires a cookie whose name carries the host prefix + // to be marked secure, scoped to the root path, and free of a domain, again + // matched case-insensitively to agree with the user agent + if (starts_with_ignore_case(cookie.name, "__Host-") && + (!cookie.secure || cookie.domain.has_value() || + !cookie.path.has_value() || *cookie.path != "/")) { + return false; + } + + return true; +} + +auto http_serialize_cookie(const HTTPCookie &cookie, std::string &out) -> bool { + if (!http_cookie_valid(cookie)) { + return false; + } + + // Format the max age into a stack buffer, avoiding an intermediate heap + // allocation. A non-negative representation never exceeds these digits + std::array::digits10 + 2> + max_age_buffer; + std::string_view max_age; + if (cookie.max_age.has_value()) { + const auto result{std::to_chars( + max_age_buffer.data(), max_age_buffer.data() + max_age_buffer.size(), + cookie.max_age->count())}; + max_age = std::string_view{ + max_age_buffer.data(), + static_cast(result.ptr - max_age_buffer.data())}; + } + + out.reserve(out.size() + required_size(cookie, max_age)); + out.append(cookie.name); + out.append(COOKIE_PAIR_SEPARATOR); + out.append(cookie.value); + + if (cookie.domain.has_value()) { + out.append(COOKIE_ATTRIBUTE_SEPARATOR); + out.append(COOKIE_DOMAIN); + out.append(*cookie.domain); + } + + if (cookie.path.has_value()) { + out.append(COOKIE_ATTRIBUTE_SEPARATOR); + out.append(COOKIE_PATH); + out.append(*cookie.path); + } + + if (cookie.max_age.has_value()) { + out.append(COOKIE_ATTRIBUTE_SEPARATOR); + out.append(COOKIE_MAX_AGE); + out.append(max_age); + } + + if (cookie.secure) { + out.append(COOKIE_ATTRIBUTE_SEPARATOR); + out.append(COOKIE_SECURE); + } + + if (cookie.http_only) { + out.append(COOKIE_ATTRIBUTE_SEPARATOR); + out.append(COOKIE_HTTP_ONLY); + } + + if (cookie.same_site.has_value()) { + out.append(COOKIE_ATTRIBUTE_SEPARATOR); + out.append(COOKIE_SAME_SITE); + out.append(same_site_token(*cookie.same_site)); + } + + return true; +} + +auto http_serialize_cookie(const HTTPCookie &cookie) + -> std::optional { + std::string out; + if (!http_serialize_cookie(cookie, out)) { + return std::nullopt; + } + + return out; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/idna/CMakeLists.txt b/vendor/core/src/core/idna/CMakeLists.txt index 21006e31e..14c7cfd03 100644 --- a/vendor/core/src/core/idna/CMakeLists.txt +++ b/vendor/core/src/core/idna/CMakeLists.txt @@ -18,10 +18,12 @@ add_custom_command( COMMAND "${SOURCEMETA_CORE_IDNA_CODEGEN_TARGET}" "${SOURCEMETA_CORE_IDNA_DATA_HEADER}" "${SOURCEMETA_CORE_IDNA_UCD_DIR}/Idna2008.txt" + "${SOURCEMETA_CORE_IDNA_UCD_DIR}/IdnaMappingTable.txt" DEPENDS "${SOURCEMETA_CORE_IDNA_CODEGEN_TARGET}" "${SOURCEMETA_CORE_IDNA_UCD_DIR}/Idna2008.txt" - COMMENT "Generating IDNA property tables" + "${SOURCEMETA_CORE_IDNA_UCD_DIR}/IdnaMappingTable.txt" + COMMENT "Generating IDNA property and mapping tables" VERBATIM) sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME idna diff --git a/vendor/core/src/core/idna/codegen.cc b/vendor/core/src/core/idna/codegen.cc index 88bfba34b..4b35aa8f8 100644 --- a/vendor/core/src/core/idna/codegen.cc +++ b/vendor/core/src/core/idna/codegen.cc @@ -5,7 +5,8 @@ #include #include -#include // std::size_t, std::ptrdiff_t +#include // std::sort +#include // std::size_t, std::ptrdiff_t #include // std::uint8_t, std::uint16_t, std::uint32_t, std::uint64_t #include // EXIT_FAILURE, EXIT_SUCCESS #include // std::exception @@ -108,17 +109,7 @@ auto parse_idna_file(const std::filesystem::path &input_path) return result; } -auto build_pages(const std::vector &entries) -> TwoStageTable { - std::vector values( - TOTAL_CODEPOINTS, - static_cast(sourcemeta::core::IDNAProperty::PValid)); - for (const auto &entry : entries) { - for (std::uint32_t codepoint{entry.first}; codepoint <= entry.last; - codepoint += 1) { - values[codepoint] = static_cast(entry.value); - } - } - +auto compress_pages(const std::vector &values) -> TwoStageTable { std::unordered_map page_to_id; TwoStageTable table; table.stage1.reserve(NUM_PAGES); @@ -145,6 +136,22 @@ auto build_pages(const std::vector &entries) -> TwoStageTable { return table; } +auto build_pages(const std::vector &entries) -> TwoStageTable { + // RFC 5892 Section 2.10: a code point not covered by the derived-property + // data defaults to Unassigned (which validation treats as disallowed), so an + // uncovered code point fails closed rather than being accepted + std::vector values( + TOTAL_CODEPOINTS, + static_cast(sourcemeta::core::IDNAProperty::Unassigned)); + for (const auto &entry : entries) { + for (std::uint32_t codepoint{entry.first}; codepoint <= entry.last; + codepoint += 1) { + values[codepoint] = static_cast(entry.value); + } + } + return compress_pages(values); +} + template auto emit_row(std::ostream &stream, const std::span items) -> void { constexpr std::size_t row_width{16}; @@ -173,6 +180,161 @@ auto emit_property(std::ostream &stream, const std::string_view prefix, stream << "};\n\n"; } +struct MappingEntry { + std::uint32_t first; + std::uint32_t last; + sourcemeta::core::IDNAMappingStatus status; + std::u32string mapping; +}; + +struct MappingIndexEntry { + std::uint32_t codepoint; + std::uint32_t offset; + std::uint32_t length; +}; + +struct MappingTable { + TwoStageTable status; + std::u32string pool; + std::vector index; +}; + +auto mapping_status_from_token(const std::string_view token) + -> sourcemeta::core::IDNAMappingStatus { +#define SOURCEMETA_CORE_IDNA_MAPPING_CASE(name, alias) \ + if (token == alias) { \ + return sourcemeta::core::IDNAMappingStatus::name; \ + } + SOURCEMETA_CORE_IDNA_MAPPING_STATUS_LIST(SOURCEMETA_CORE_IDNA_MAPPING_CASE) +#undef SOURCEMETA_CORE_IDNA_MAPPING_CASE + throw std::runtime_error{ + std::string{"Unknown IDNA mapping status: "}.append(token)}; +} + +// Parse a space-separated list of hexadecimal codepoints (the mapping column) +auto parse_mapping_sequence(const std::string_view field) -> std::u32string { + std::u32string result; + sourcemeta::core::split(field, ' ', [&](const std::string_view token) { + const auto trimmed{sourcemeta::core::trim(token)}; + if (!trimmed.empty()) { + result.push_back(static_cast(parse_hex_codepoint(trimmed))); + } + }); + return result; +} + +auto parse_mapping_line(const std::string_view payload) -> MappingEntry { + const auto trimmed{ + sourcemeta::core::trim(sourcemeta::core::take_until(payload, '#'))}; + const auto first_split{sourcemeta::core::split_once(trimmed, ';')}; + if (!first_split.has_value()) { + throw std::runtime_error{ + std::string{"Missing ';' in line: "}.append(payload)}; + } + const auto range_part{sourcemeta::core::trim(first_split->first)}; + const auto range_split{ + sourcemeta::core::split_once(range_part, std::string_view{".."})}; + const auto first{parse_hex_codepoint( + range_split.has_value() ? range_split->first : range_part)}; + const auto last{range_split.has_value() + ? parse_hex_codepoint(range_split->second) + : first}; + + const auto second_split{ + sourcemeta::core::split_once(first_split->second, ';')}; + const auto status_part{sourcemeta::core::trim( + second_split.has_value() ? second_split->first : first_split->second)}; + const auto status{mapping_status_from_token(status_part)}; + + std::u32string mapping; + if (status == sourcemeta::core::IDNAMappingStatus::Mapped && + second_split.has_value()) { + const auto third_split{ + sourcemeta::core::split_once(second_split->second, ';')}; + const auto mapping_part{sourcemeta::core::trim( + third_split.has_value() ? third_split->first : second_split->second)}; + mapping = parse_mapping_sequence(mapping_part); + } + + return {first, last, status, std::move(mapping)}; +} + +auto parse_mapping_file(const std::filesystem::path &input_path) + -> std::vector { + auto stream{sourcemeta::core::read_file(input_path)}; + std::vector entries; + sourcemeta::core::for_each_line(stream, [&](const std::string_view raw_line) { + const auto line{sourcemeta::core::trim(raw_line)}; + if (line.empty() || line.front() == '#') { + return; + } + entries.push_back(parse_mapping_line(line)); + }); + return entries; +} + +auto build_mapping(const std::vector &entries) -> MappingTable { + // Codepoints absent from the table default to disallowed per UTS #46 + std::vector status_values( + TOTAL_CODEPOINTS, static_cast( + sourcemeta::core::IDNAMappingStatus::Disallowed)); + MappingTable table; + // Deduplicate identical replacement sequences into a shared pool + std::unordered_map sequence_to_offset; + for (const auto &entry : entries) { + std::uint32_t offset{0}; + if (entry.status == sourcemeta::core::IDNAMappingStatus::Mapped) { + const auto existing{sequence_to_offset.find(entry.mapping)}; + if (existing != sequence_to_offset.end()) { + offset = existing->second; + } else { + offset = static_cast(table.pool.size()); + sequence_to_offset.emplace(entry.mapping, offset); + table.pool.append(entry.mapping); + } + } + for (std::uint32_t codepoint{entry.first}; codepoint <= entry.last; + codepoint += 1) { + status_values[codepoint] = static_cast(entry.status); + if (entry.status == sourcemeta::core::IDNAMappingStatus::Mapped) { + table.index.push_back( + {codepoint, offset, + static_cast(entry.mapping.size())}); + } + } + } + + std::sort(table.index.begin(), table.index.end(), + [](const MappingIndexEntry &left, const MappingIndexEntry &right) { + return left.codepoint < right.codepoint; + }); + table.status = compress_pages(status_values); + return table; +} + +auto emit_mapping(std::ostream &stream, const MappingTable &table) -> void { + emit_property(stream, "IDNA_MAPPING_STATUS", table.status); + + stream << "constexpr char32_t IDNA_MAPPING_POOL[" << table.pool.size() + << "] = {\n"; + emit_row( + stream, std::span{table.pool.data(), table.pool.size()}); + stream << "};\n\n"; + + stream << "struct IDNAMappingEntry {\n"; + stream << " std::uint32_t codepoint;\n"; + stream << " std::uint32_t offset;\n"; + stream << " std::uint32_t length;\n"; + stream << "};\n\n"; + stream << "constexpr IDNAMappingEntry IDNA_MAPPING_INDEX[" + << table.index.size() << "] = {\n"; + for (const auto &entry : table.index) { + stream << " {" << entry.codepoint << ", " << entry.offset << ", " + << entry.length << "},\n"; + } + stream << "};\n\n"; +} + } // namespace auto main(const int argc, const char *const argv[]) -> int { @@ -180,21 +342,23 @@ auto main(const int argc, const char *const argv[]) -> int { sourcemeta::core::Options app; app.parse(argc, argv); const auto &positional{app.positional()}; - if (positional.size() != 2) { + if (positional.size() != 3) { std::cerr << "Usage: " << (argc > 0 ? argv[0] : "codegen") - << " \n"; + << " \n"; return EXIT_FAILURE; } const std::filesystem::path output_path{positional.at(0)}; - const std::filesystem::path input_path{positional.at(1)}; + const std::filesystem::path property_path{positional.at(1)}; + const std::filesystem::path mapping_path{positional.at(2)}; - const auto entries{parse_idna_file(input_path)}; - const auto table{build_pages(entries)}; + const auto table{build_pages(parse_idna_file(property_path))}; + const auto mapping{build_mapping(parse_mapping_file(mapping_path))}; sourcemeta::core::write_file(output_path, [&](std::ostream &stream) { stream << "#include \n\n"; stream << "namespace {\n\n"; emit_property(stream, "IDNA_PROPERTY", table); + emit_mapping(stream, mapping); stream << "} // namespace\n"; }); } catch (const std::exception &error) { diff --git a/vendor/core/src/core/idna/idna.cc b/vendor/core/src/core/idna/idna.cc index 87018b7ed..41926b54a 100644 --- a/vendor/core/src/core/idna/idna.cc +++ b/vendor/core/src/core/idna/idna.cc @@ -3,6 +3,7 @@ #include #include +#include // std::ranges::lower_bound #include // std::size_t #include // std::optional, std::nullopt #include // std::string, std::u32string @@ -428,4 +429,52 @@ auto idna_is_valid_a_label(const std::string_view label) -> bool { return validate_a_label_body(encoded, decoded); } +namespace { + +auto mapping_status(const char32_t codepoint) noexcept -> IDNAMappingStatus { + if (codepoint > 0x10FFFF) { + return IDNAMappingStatus::Disallowed; + } + const std::size_t page{IDNA_MAPPING_STATUS_STAGE1[codepoint >> 10U]}; + return static_cast( + IDNA_MAPPING_STATUS_STAGE2[(page << 10U) | (codepoint & 0x3FFU)]); +} + +// The replacement sequence for a Mapped codepoint. The caller must only +// invoke this when `mapping_status(codepoint)` is Mapped, in which case the +// codepoint is guaranteed to be present in the index. +auto mapping_replacement(const char32_t codepoint) noexcept + -> std::u32string_view { + const auto *const found{std::ranges::lower_bound( + IDNA_MAPPING_INDEX, codepoint, {}, &IDNAMappingEntry::codepoint)}; + return std::u32string_view{IDNA_MAPPING_POOL + found->offset, found->length}; +} + +} // namespace + +auto idna_uts46_map(const std::u32string_view input) -> std::u32string { + std::u32string mapped; + mapped.reserve(input.size()); + for (const auto codepoint : input) { + switch (mapping_status(codepoint)) { + // UTS #46 §4 step 1. Valid and (under Nontransitional Processing) + // deviation code points are kept unchanged. Disallowed code points are + // also left in place, so the later validity check rejects them rather + // than the mapping silently dropping the whole input. + case IDNAMappingStatus::Valid: + case IDNAMappingStatus::Deviation: + case IDNAMappingStatus::Disallowed: + mapped.push_back(codepoint); + break; + case IDNAMappingStatus::Mapped: + mapped.append(mapping_replacement(codepoint)); + break; + case IDNAMappingStatus::Ignored: + break; + } + } + + return nfc(mapped); +} + } // namespace sourcemeta::core diff --git a/vendor/core/src/core/idna/include/sourcemeta/core/idna.h b/vendor/core/src/core/idna/include/sourcemeta/core/idna.h index 9c7874682..6f86204f8 100644 --- a/vendor/core/src/core/idna/include/sourcemeta/core/idna.h +++ b/vendor/core/src/core/idna/include/sourcemeta/core/idna.h @@ -183,6 +183,33 @@ auto idna_is_valid_u_label(const std::u32string_view label) -> bool; SOURCEMETA_CORE_IDNA_EXPORT auto idna_is_valid_a_label(const std::string_view label) -> bool; +/// @ingroup idna +/// Apply the UTS #46 mapping to `input` and return the result normalised to +/// NFC. This is the mapping and normalisation half of UTS #46 processing +/// (steps 1 and 2), performed with Nontransitional Processing and +/// UseSTD3ASCIIRules disabled, so the four deviation characters (U+00DF, +/// U+03C2, U+200C, U+200D) map to themselves. Following the UTS #46 Map step, +/// disallowed codepoints are left unchanged rather than removed, so that the +/// later per-label validity check is what rejects them (the same is true of +/// ASCII characters outside the LDH set). Label separation and the per-label +/// validity checks are not performed here. See +/// https://www.unicode.org/reports/tr46/ for the algorithm. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// // Uppercase is case-folded, fullwidth digits map to ASCII +/// assert(sourcemeta::core::idna_uts46_map(U"EXAMPLE") == U"example"); +/// assert(sourcemeta::core::idna_uts46_map(U"\uFF11\uFF12\uFF13") == U"123"); +/// // Soft hyphen is ignored +/// assert(sourcemeta::core::idna_uts46_map(U"a\u00ADb") == U"ab"); +/// // A disallowed codepoint is preserved for validation to reject +/// assert(sourcemeta::core::idna_uts46_map(U"\u0080") == U"\u0080"); +/// ``` +SOURCEMETA_CORE_IDNA_EXPORT +auto idna_uts46_map(const std::u32string_view input) -> std::u32string; + } // namespace sourcemeta::core #endif diff --git a/vendor/core/src/core/idna/include/sourcemeta/core/idna_ucd.h b/vendor/core/src/core/idna/include/sourcemeta/core/idna_ucd.h index 737537cb0..b5cd1a3ca 100644 --- a/vendor/core/src/core/idna/include/sourcemeta/core/idna_ucd.h +++ b/vendor/core/src/core/idna/include/sourcemeta/core/idna_ucd.h @@ -18,9 +18,33 @@ namespace sourcemeta::core { /// The RFC 5892 derived property of a Unicode codepoint. See /// https://www.rfc-editor.org/rfc/rfc5892 for the property's definition. enum class IDNAProperty : std::uint8_t { +#if !defined(DOXYGEN) #define SOURCEMETA_CORE_IDNA_ENUM_ENTRY(name, alias) name, SOURCEMETA_CORE_IDNA_PROPERTY_LIST(SOURCEMETA_CORE_IDNA_ENUM_ENTRY) #undef SOURCEMETA_CORE_IDNA_ENUM_ENTRY +#endif +}; + +/// @ingroup idna +/// Each entry maps an `IDNAMappingStatus` enum name to its UTS #46 +/// IdnaMappingTable.txt token. +#define SOURCEMETA_CORE_IDNA_MAPPING_STATUS_LIST(X) \ + X(Valid, "valid") \ + X(Ignored, "ignored") \ + X(Mapped, "mapped") \ + X(Deviation, "deviation") \ + X(Disallowed, "disallowed") + +/// @ingroup idna +/// The UTS #46 status of a Unicode codepoint in the mapping table. See +/// https://www.unicode.org/reports/tr46/ for the status definitions. +enum class IDNAMappingStatus : std::uint8_t { +#if !defined(DOXYGEN) +#define SOURCEMETA_CORE_IDNA_MAPPING_ENUM_ENTRY(name, alias) name, + SOURCEMETA_CORE_IDNA_MAPPING_STATUS_LIST( + SOURCEMETA_CORE_IDNA_MAPPING_ENUM_ENTRY) +#undef SOURCEMETA_CORE_IDNA_MAPPING_ENUM_ENTRY +#endif }; } // namespace sourcemeta::core diff --git a/vendor/core/src/core/ip/CMakeLists.txt b/vendor/core/src/core/ip/CMakeLists.txt index 3c2fdd7e6..99c9faf2b 100644 --- a/vendor/core/src/core/ip/CMakeLists.txt +++ b/vendor/core/src/core/ip/CMakeLists.txt @@ -4,3 +4,5 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME ip if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME ip) endif() + +target_link_libraries(sourcemeta_core_ip PRIVATE sourcemeta::core::text) diff --git a/vendor/core/src/core/ip/ipv6.cc b/vendor/core/src/core/ip/ipv6.cc index 099be9bdb..4d7883b38 100644 --- a/vendor/core/src/core/ip/ipv6.cc +++ b/vendor/core/src/core/ip/ipv6.cc @@ -1,26 +1,8 @@ #include - -#include // std::array -#include // std::uint8_t +#include namespace sourcemeta::core { -static constexpr auto make_hex_table() -> std::array { - std::array table{}; - for (auto index{0u}; index < 256; index++) { - table[index] = (index >= '0' && index <= '9') || - (index >= 'a' && index <= 'f') || - (index >= 'A' && index <= 'F'); - } - return table; -} - -static constexpr auto HEX_TABLE{make_hex_table()}; - -static constexpr auto is_hex_digit(const char character) -> bool { - return HEX_TABLE[static_cast(character)]; -} - auto is_ipv6(const std::string_view address) -> bool { if (address.empty()) { return false; diff --git a/vendor/core/src/core/jose/CMakeLists.txt b/vendor/core/src/core/jose/CMakeLists.txt index 986ec4483..2d11654cb 100644 --- a/vendor/core/src/core/jose/CMakeLists.txt +++ b/vendor/core/src/core/jose/CMakeLists.txt @@ -1,8 +1,11 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME jose - PRIVATE_HEADERS algorithm.h error.h jwk.h jwks.h jwks_provider.h jwt.h verify.h - SOURCES jose_algorithm.cc jose_jwk.cc jose_jwks.cc jose_jwks_provider.cc + PRIVATE_HEADERS algorithm.h error.h jwk.h jwk_private.h jwks.h jwks_provider.h + jwt.h sign.h verify.h + SOURCES jose_algorithm.cc jose_jwk.cc jose_jwk_private.cc jose_jwks.cc + jose_jwks_provider.cc jose_jwt.cc jose_jwt_check_claims.cc jose_jws_verify_signature.cc - jose_jwt_verify_signature.cc jose_jwt_verify.cc) + jose_jwt_verify_signature.cc jose_jwt_verify.cc + jose_jws_sign.cc jose_jwt_sign.cc jose_key.h) target_link_libraries(sourcemeta_core_jose PUBLIC sourcemeta::core::json) diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose.h index 169065fa6..de47d07d6 100644 --- a/vendor/core/src/core/jose/include/sourcemeta/core/jose.h +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose.h @@ -9,9 +9,11 @@ #include #include #include +#include #include #include #include +#include #include // NOLINTEND(misc-include-cleaner) diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_algorithm.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_algorithm.h index 3f938c103..43d41b27d 100644 --- a/vendor/core/src/core/jose/include/sourcemeta/core/jose_algorithm.h +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_algorithm.h @@ -12,21 +12,38 @@ namespace sourcemeta::core { /// @ingroup jose -/// The asymmetric JSON Web Signature algorithms from RFC 7518 Section 3.1 and -/// the Edwards-curve algorithm from RFC 8037 Section 3.1. The symmetric HMAC -/// family and the null algorithm are intentionally absent, which makes -/// algorithm confusion attacks unrepresentable in the type system. +/// The JSON Web Signature algorithms from RFC 7518 Section 3.1 and the +/// Edwards-curve algorithm from RFC 8037 Section 3.1. The null algorithm is +/// intentionally absent. Each algorithm demands a key of exactly one family, +/// the symmetric algorithms an octet sequence and the asymmetric ones their own +/// key type, which is what keeps algorithm confusion attacks unexploitable. enum class JWSAlgorithm : std::uint8_t { + /// RSASSA-PKCS1-v1_5 using SHA-256. RS256, + /// RSASSA-PKCS1-v1_5 using SHA-384. RS384, + /// RSASSA-PKCS1-v1_5 using SHA-512. RS512, + /// RSASSA-PSS using SHA-256 and MGF1 with SHA-256. PS256, + /// RSASSA-PSS using SHA-384 and MGF1 with SHA-384. PS384, + /// RSASSA-PSS using SHA-512 and MGF1 with SHA-512. PS512, + /// ECDSA using the NIST P-256 curve and SHA-256. ES256, + /// ECDSA using the NIST P-384 curve and SHA-384. ES384, + /// ECDSA using the NIST P-521 curve and SHA-512. ES512, - EdDSA + /// Edwards-curve Digital Signature Algorithm. + EdDSA, + /// HMAC using SHA-256. + HS256, + /// HMAC using SHA-384. + HS384, + /// HMAC using SHA-512. + HS512 }; /// @ingroup jose diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk.h index c6b41134f..634b015c1 100644 --- a/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk.h +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk.h @@ -22,8 +22,9 @@ namespace sourcemeta::core { /// @ingroup jose /// A parsed public JSON Web Key (RFC 7517), restricted to RSA, elliptic curve, -/// and octet key pair (RFC 8037) keys. The key owns its decoded material, so -/// the source JSON document does not need to outlive it. For example: +/// octet key pair (RFC 8037), and symmetric octet (RFC 7518 Section 6.4) keys. +/// The key owns its decoded material, so the source JSON document does not need +/// to outlive it. For example: /// /// ```cpp /// #include @@ -38,7 +39,17 @@ namespace sourcemeta::core { /// ``` class SOURCEMETA_CORE_JOSE_EXPORT JWK { public: - enum class Type : std::uint8_t { RSA, EllipticCurve, OctetKeyPair }; + /// The family of key material a key holds. + enum class Type : std::uint8_t { + /// The RSA key type. + RSA, + /// The elliptic-curve key type. + EllipticCurve, + /// The Edwards-curve octet key pair type. + OctetKeyPair, + /// The symmetric octet sequence key type. + Octet + }; /// Parse a JSON Web Key from a JSON value, throwing on invalid input. explicit JWK(const JSON &value); @@ -60,8 +71,10 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWK { /// input. [[nodiscard]] static auto from(JSON &&value) -> std::optional; + /// The family of key material this key holds. [[nodiscard]] auto type() const noexcept -> Type { return this->type_; } + /// The key identifier used to select this key, if present. [[nodiscard]] auto key_id() const noexcept -> std::optional { if (this->key_id_.has_value()) { @@ -71,6 +84,7 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWK { return std::nullopt; } + /// The algorithm this key is intended for, if present. [[nodiscard]] auto algorithm() const noexcept -> std::optional { return this->algorithm_; } @@ -78,6 +92,7 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWK { // Elliptic curve keys (RFC 7518 Section 6.2) and octet key pairs (RFC 8037 // Section 2) carry a curve name, which the elliptic curve algorithms pin to // exactly one curve + /// The curve this key is pinned to, empty when it carries none. [[nodiscard]] auto curve() const noexcept -> std::string_view { return this->curve_; } @@ -85,10 +100,20 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWK { // The parsed platform key, built once from the decoded material so that // verification reuses it rather than reconstructing it per signature. It is // null when the material could not be turned into a key + /// The parsed platform key, null when the material could not be turned into + /// one. [[nodiscard]] auto public_key() const noexcept -> const PublicKey * { return this->public_key_.has_value() ? &*this->public_key_ : nullptr; } + // Symmetric keys carry their raw secret rather than a parsed platform key + // (RFC 7518 Section 6.4), and knowing the secret is required for both + // producing and checking an HMAC, so the public key class carries it + /// The raw symmetric secret, empty for asymmetric keys. + [[nodiscard]] auto secret() const noexcept -> std::string_view { + return this->secret_; + } + private: JWK() = default; static auto parse(const JSON &value, JWK &result) -> bool; @@ -101,6 +126,7 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWK { std::optional algorithm_; std::string curve_; std::optional public_key_; + std::string secret_; #if defined(_MSC_VER) #pragma warning(default : 4251) #endif diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk_private.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk_private.h new file mode 100644 index 000000000..7d60b98df --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk_private.h @@ -0,0 +1,134 @@ +#ifndef SOURCEMETA_CORE_JOSE_JWK_PRIVATE_H_ +#define SOURCEMETA_CORE_JOSE_JWK_PRIVATE_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +#include + +#include +#include + +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup jose +/// A parsed private JSON Web Key (RFC 7517), the private counterpart to a +/// public one, restricted to RSA, elliptic curve, octet key pair (RFC 8037), +/// and symmetric octet (RFC 7518 Section 6.4) keys. The key owns its decoded +/// material, so neither the source JSON document nor the source PEM needs to +/// outlive it. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::JWKPrivate::from_pem(pem)}; +/// assert(key.has_value()); +/// assert(key.value().private_key() != nullptr); +/// ``` +class SOURCEMETA_CORE_JOSE_EXPORT JWKPrivate { +public: + /// The family of key material a key holds. + enum class Type : std::uint8_t { + /// The RSA key type. + RSA, + /// The elliptic-curve key type. + EllipticCurve, + /// The Edwards-curve octet key pair type. + OctetKeyPair, + /// The symmetric octet sequence key type. + Octet + }; + + /// A key exclusively owns its parsed private key, so it is move-only. + JWKPrivate(JWKPrivate &&other) noexcept = default; + auto operator=(JWKPrivate &&other) noexcept -> JWKPrivate & = default; + JWKPrivate(const JWKPrivate &) = delete; + auto operator=(const JWKPrivate &) -> JWKPrivate & = delete; + ~JWKPrivate() = default; + + /// Parse a private JSON Web Key from a JSON value, returning no value on + /// invalid input. The private parameters (RFC 7518 Sections 6.2.2 and 6.3.2, + /// RFC 8037 Section 2) are required, so a public-only key is rejected. + [[nodiscard]] static auto from(const JSON &value) + -> std::optional; + + /// Parse a private JSON Web Key from a JSON value, returning no value on + /// invalid input. + [[nodiscard]] static auto from(JSON &&value) -> std::optional; + + /// Parse a private key from an unencrypted PKCS#8 PEM document (RFC 5958), + /// returning no value on invalid input. The key identifier, algorithm, and + /// curve are left unset, as a PEM document carries no such metadata. + [[nodiscard]] static auto from_pem(const std::string_view pem) + -> std::optional; + + /// The family of key material this key holds. + [[nodiscard]] auto type() const noexcept -> Type { return this->type_; } + + /// The key identifier used to select this key, if present. + [[nodiscard]] auto key_id() const noexcept + -> std::optional { + if (this->key_id_.has_value()) { + return std::string_view{*this->key_id_}; + } + + return std::nullopt; + } + + /// The algorithm this key is intended for, if present. + [[nodiscard]] auto algorithm() const noexcept -> std::optional { + return this->algorithm_; + } + + // Elliptic curve keys (RFC 7518 Section 6.2) and octet key pairs (RFC 8037 + // Section 2) carry a curve name, which the elliptic curve algorithms pin to + // exactly one curve. It is empty for keys parsed from a PEM document + /// The curve this key is pinned to, empty when it carries none. + [[nodiscard]] auto curve() const noexcept -> std::string_view { + return this->curve_; + } + + // The parsed platform key, built once from the decoded material so that + // signing reuses it rather than reconstructing it per signature. It is null + // when the material could not be turned into a key + /// The parsed private key, or null when the material could not be decoded. + [[nodiscard]] auto private_key() const noexcept -> const PrivateKey * { + return this->private_key_.has_value() ? &*this->private_key_ : nullptr; + } + + // Symmetric keys carry their raw secret rather than a parsed platform key + // (RFC 7518 Section 6.4), and knowing the secret is required for both + // producing and checking an HMAC, so the private key class carries it + /// The raw symmetric secret, empty for asymmetric keys. + [[nodiscard]] auto secret() const noexcept -> std::string_view { + return this->secret_; + } + +private: + JWKPrivate() = default; + static auto parse(const JSON &value, JWKPrivate &result) -> bool; + +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + Type type_{Type::RSA}; + std::optional key_id_; + std::optional algorithm_; + std::string curve_; + std::optional private_key_; + std::string secret_; +#if defined(_MSC_VER) +#pragma warning(default : 4251) +#endif +}; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks.h index 9880b6833..f7eebbdd7 100644 --- a/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks.h +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks.h @@ -42,6 +42,8 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWKS { /// Parse a JSON Web Key Set from a JSON value, throwing a `JWKSParseError` /// on invalid input. explicit JWKS(const JSON &value); + /// Parse a JSON Web Key Set from a JSON value, throwing a `JWKSParseError` + /// on invalid input. explicit JWKS(JSON &&value); /// A key set exclusively owns its keys, so it is move-only. @@ -53,6 +55,8 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWKS { /// Parse a JSON Web Key Set from a JSON value, returning no value on invalid /// input. [[nodiscard]] static auto from(const JSON &value) -> std::optional; + /// Parse a JSON Web Key Set from a JSON value, returning no value on invalid + /// input. [[nodiscard]] static auto from(JSON &&value) -> std::optional; /// Look up a key by its identifier (RFC 7515 Section 4.1.4), returning no @@ -60,10 +64,12 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWKS { [[nodiscard]] auto find(const std::string_view key_id) const noexcept -> const JWK *; + /// The number of keys in the set. [[nodiscard]] auto size() const noexcept -> std::size_t { return this->keys_.size(); } + /// Whether the set holds no keys. [[nodiscard]] auto empty() const noexcept -> bool { return this->keys_.empty(); } diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks_provider.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks_provider.h index 53d12eeda..40b18e303 100644 --- a/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks_provider.h +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks_provider.h @@ -36,7 +36,9 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWKSProvider { /// the caching response header. An absent hint means no lifetime was /// advertised, which is distinct from an advertised lifetime of zero. struct FetchResult { + /// The raw key set bytes. std::string body; + /// The advertised freshness lifetime, absent when none was given. std::optional max_age; }; @@ -55,17 +57,27 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWKSProvider { /// Tunables for the caching and verification policy. struct Options { + /// The lifetime to assume when the transport advertises none. std::chrono::seconds fallback_ttl{std::chrono::hours{1}}; + /// The shortest lifetime honored, clamping smaller advertised values up. std::chrono::seconds minimum_ttl{std::chrono::minutes{5}}; + /// The longest lifetime honored, clamping larger advertised values down. std::chrono::seconds maximum_ttl{std::chrono::hours{24}}; + /// The wait before another refetch is allowed after an unknown key + /// identifier. std::chrono::seconds unknown_kid_cooldown{std::chrono::minutes{5}}; + /// The tolerance applied to time-based claims. std::chrono::seconds clock_skew{0}; }; /// Construct a provider for a concrete key set URL with an injected - /// transport, optionally overriding the policy and the clock. + /// transport, using the default policy and the system clock. JWKSProvider(std::string jwks_uri, Fetcher fetcher); + /// Construct a provider for a concrete key set URL with an injected + /// transport, overriding the caching and verification policy. JWKSProvider(std::string jwks_uri, Fetcher fetcher, Options options); + /// Construct a provider for a concrete key set URL with an injected + /// transport, overriding the policy and the clock. JWKSProvider(std::string jwks_uri, Fetcher fetcher, Options options, Clock clock); diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwt.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwt.h index a4b51abc3..8938aebba 100644 --- a/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwt.h +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwt.h @@ -48,50 +48,64 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWT { // Header (RFC 7515 Section 4) + /// The signing algorithm declared in the token header, if present. [[nodiscard]] auto algorithm() const noexcept -> std::optional { return this->algorithm_; } + /// The key identifier from the token header, if present. [[nodiscard]] auto key_id() const noexcept -> std::optional; + /// The token type declared in the header, if present. [[nodiscard]] auto type() const noexcept -> std::optional; + /// The decoded token header. [[nodiscard]] auto header() const noexcept -> const JSON & { return this->header_; } // Registered claims (RFC 7519 Section 4.1) + /// The issuer that created the token, if present. [[nodiscard]] auto issuer() const noexcept -> std::optional; + /// The subject the token is about, if present. [[nodiscard]] auto subject() const noexcept -> std::optional; + /// Whether the token is intended for the given audience. [[nodiscard]] auto has_audience(const std::string_view audience) const noexcept -> bool; + /// The time after which the token is no longer valid, if present. [[nodiscard]] auto expires_at() const -> std::optional; + /// The time before which the token is not yet valid, if present. [[nodiscard]] auto not_before() const -> std::optional; + /// The time at which the token was issued, if present. [[nodiscard]] auto issued_at() const -> std::optional; + /// The unique identifier of the token, if present. [[nodiscard]] auto token_id() const noexcept -> std::optional; + /// The decoded token payload. [[nodiscard]] auto payload() const noexcept -> const JSON & { return this->payload_; } // The exact wire bytes the signature is computed over, never re-serialized // (RFC 7515 Section 5.1) + /// The exact wire bytes the signature is computed over. [[nodiscard]] auto signing_input() const noexcept -> std::string_view { return this->signing_input_; } + /// The raw token signature. [[nodiscard]] auto signature() const noexcept -> std::string_view { return this->signature_; } diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_sign.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_sign.h new file mode 100644 index 000000000..adde65e97 --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_sign.h @@ -0,0 +1,69 @@ +#ifndef SOURCEMETA_CORE_JOSE_SIGN_H_ +#define SOURCEMETA_CORE_JOSE_SIGN_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +#include +#include + +#include + +#include // std::optional +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup jose +/// Produce the raw signature bytes for a signing input under an algorithm, the +/// signing counterpart to signature verification. Returns no value for a key +/// whose type or curve cannot serve the algorithm, a key declaring a +/// contradicting algorithm, or a key whose material never formed a usable +/// private key. The signing input is the exact bytes to sign, which carry no +/// constraint on their content. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::JWKPrivate::from_pem(pem)}; +/// assert(key.has_value()); +/// const auto signature{sourcemeta::core::jws_sign( +/// sourcemeta::core::JWSAlgorithm::RS256, "header.payload", key.value())}; +/// assert(signature.has_value()); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto jws_sign(const JWSAlgorithm algorithm, + const std::string_view signing_input, const JWKPrivate &key) + -> std::optional; + +/// @ingroup jose +/// Build and sign a JSON Web Token in compact serialization (RFC 7519, RFC +/// 7515) from a header and a payload, returning the base64url header, payload, +/// and signature joined by dots. The algorithm is taken from the header +/// algorithm parameter (RFC 7515 Section 4.1.1). Returns no value when the +/// header or payload is not an object, the header names no supported algorithm, +/// or the key cannot produce the +/// signature. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::JWKPrivate::from_pem(pem)}; +/// assert(key.has_value()); +/// const auto token{sourcemeta::core::jwt_sign( +/// sourcemeta::core::parse_json(R"({ "alg": "RS256" })"), +/// sourcemeta::core::parse_json(R"({ "iss": "acme" })"), key.value())}; +/// assert(token.has_value()); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto jwt_sign(const JSON &header, const JSON &payload, const JWKPrivate &key) + -> std::optional; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_verify.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_verify.h index 30b9d37df..2e95d5541 100644 --- a/vendor/core/src/core/jose/include/sourcemeta/core/jose_verify.h +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_verify.h @@ -24,11 +24,17 @@ namespace sourcemeta::core { /// The claim validation errors that claim checking can return, one per check /// performed rather than an exhaustive list of registered claims. enum class JWTClaimError : std::uint8_t { + /// The issuer claim is missing or does not match the expected value. Issuer, + /// The subject claim is missing or does not match the expected value. Subject, + /// The audience claim is missing or does not contain the expected value. Audience, + /// The expiration time claim is missing or the token has expired. Expiration, + /// The not-before time claim is malformed or lies in the future. NotBefore, + /// The issued-at time claim is malformed or lies in the future. IssuedAt }; @@ -120,15 +126,25 @@ auto jwt_verify_signature(const JWT &token, const JWK &key) -> bool; /// The steps of full token verification that can fail, in the order they are /// evaluated. enum class JWTVerificationError : std::uint8_t { + /// The token's algorithm is missing or absent from the allow-list. AlgorithmNotAllowed, + /// No key in the set could be selected or verified the signature. UnknownKey, + /// The named key was found but its signature did not verify. Signature, + /// The token type does not match the expected media type. Type, + /// The issuer claim is missing or does not match the expected value. Issuer, + /// The subject claim is missing or does not match the expected value. Subject, + /// The audience claim is missing or does not contain the expected value. Audience, + /// The expiration time claim is missing or the token has expired. Expiration, + /// The not-before time claim is malformed or lies in the future. NotBefore, + /// The issued-at time claim is malformed or lies in the future. IssuedAt }; diff --git a/vendor/core/src/core/jose/jose_algorithm.cc b/vendor/core/src/core/jose/jose_algorithm.cc index 185877eb3..f74f8bb79 100644 --- a/vendor/core/src/core/jose/jose_algorithm.cc +++ b/vendor/core/src/core/jose/jose_algorithm.cc @@ -27,6 +27,12 @@ auto to_jws_algorithm(const std::string_view value) noexcept return JWSAlgorithm::ES512; } else if (value == "EdDSA") { return JWSAlgorithm::EdDSA; + } else if (value == "HS256") { + return JWSAlgorithm::HS256; + } else if (value == "HS384") { + return JWSAlgorithm::HS384; + } else if (value == "HS512") { + return JWSAlgorithm::HS512; } else { return std::nullopt; } diff --git a/vendor/core/src/core/jose/jose_jwk.cc b/vendor/core/src/core/jose/jose_jwk.cc index b34477fda..79d5158ee 100644 --- a/vendor/core/src/core/jose/jose_jwk.cc +++ b/vendor/core/src/core/jose/jose_jwk.cc @@ -2,7 +2,8 @@ #include -#include // std::size_t +#include "jose_key.h" + #include // std::optional, std::nullopt #include // std::string_view #include // std::move, std::unreachable @@ -25,84 +26,24 @@ const auto HASH_DP{sourcemeta::core::JSON::Object::hash("dp"sv)}; const auto HASH_DQ{sourcemeta::core::JSON::Object::hash("dq"sv)}; const auto HASH_QI{sourcemeta::core::JSON::Object::hash("qi"sv)}; const auto HASH_OTH{sourcemeta::core::JSON::Object::hash("oth"sv)}; - -// The RSA algorithms only require an RSA key, each ECDSA algorithm is tied to a -// specific curve (RFC 7518 Section 3.1), and the Edwards-curve algorithm -// requires an octet key pair of either curve (RFC 8037 Section 3.1) -auto algorithm_matches_key(const sourcemeta::core::JWSAlgorithm algorithm, - const sourcemeta::core::JWK::Type type, - const std::string_view curve) -> bool { - switch (algorithm) { - case sourcemeta::core::JWSAlgorithm::RS256: - case sourcemeta::core::JWSAlgorithm::RS384: - case sourcemeta::core::JWSAlgorithm::RS512: - case sourcemeta::core::JWSAlgorithm::PS256: - case sourcemeta::core::JWSAlgorithm::PS384: - case sourcemeta::core::JWSAlgorithm::PS512: - return type == sourcemeta::core::JWK::Type::RSA; - case sourcemeta::core::JWSAlgorithm::ES256: - return type == sourcemeta::core::JWK::Type::EllipticCurve && - curve == "P-256"; - case sourcemeta::core::JWSAlgorithm::ES384: - return type == sourcemeta::core::JWK::Type::EllipticCurve && - curve == "P-384"; - case sourcemeta::core::JWSAlgorithm::ES512: - return type == sourcemeta::core::JWK::Type::EllipticCurve && - curve == "P-521"; - case sourcemeta::core::JWSAlgorithm::EdDSA: - return type == sourcemeta::core::JWK::Type::OctetKeyPair; +const auto HASH_K{sourcemeta::core::JSON::Object::hash("k"sv)}; + +auto to_jwk_kind(const sourcemeta::core::JWK::Type type) noexcept + -> sourcemeta::core::JWKKind { + switch (type) { + case sourcemeta::core::JWK::Type::RSA: + return sourcemeta::core::JWKKind::RSA; + case sourcemeta::core::JWK::Type::EllipticCurve: + return sourcemeta::core::JWKKind::EllipticCurve; + case sourcemeta::core::JWK::Type::OctetKeyPair: + return sourcemeta::core::JWKKind::OctetKeyPair; + case sourcemeta::core::JWK::Type::Octet: + return sourcemeta::core::JWKKind::Octet; } std::unreachable(); } -// The coordinate octet length is fixed per curve (RFC 7518 Section 6.2.1.2) -auto ec_coordinate_bytes(const std::string_view curve) - -> std::optional { - if (curve == "P-256") { - return 32; - } else if (curve == "P-384") { - return 48; - } else if (curve == "P-521") { - return 66; - } else { - return std::nullopt; - } -} - -// The public key octet length is fixed per Edwards curve (RFC 8032 Sections -// 5.1.5 and 5.2.5) -auto okp_key_bytes(const std::string_view curve) -> std::optional { - if (curve == "Ed25519") { - return 32; - } else if (curve == "Ed448") { - return 57; - } else { - return std::nullopt; - } -} - -// Both mappings are only reached after the curve has been validated above -auto to_elliptic_curve(const std::string_view curve) noexcept - -> sourcemeta::core::EllipticCurve { - if (curve == "P-256") { - return sourcemeta::core::EllipticCurve::P256; - } else if (curve == "P-384") { - return sourcemeta::core::EllipticCurve::P384; - } else { - return sourcemeta::core::EllipticCurve::P521; - } -} - -auto to_edwards_curve(const std::string_view curve) noexcept - -> sourcemeta::core::EdwardsCurve { - if (curve == "Ed25519") { - return sourcemeta::core::EdwardsCurve::Ed25519; - } else { - return sourcemeta::core::EdwardsCurve::Ed448; - } -} - } // namespace namespace sourcemeta::core { @@ -146,6 +87,10 @@ auto JWK::parse(const JSON &value, JWK &result) -> bool { return false; } + if (!jwk_rsa_modulus_is_allowed(decoded_modulus.value())) { + return false; + } + result.type_ = Type::RSA; parsed_key = make_rsa_public_key(decoded_modulus.value(), decoded_exponent.value()); @@ -165,7 +110,7 @@ auto JWK::parse(const JSON &value, JWK &result) -> bool { return false; } - const auto coordinate_bytes{ec_coordinate_bytes(curve->to_string())}; + const auto coordinate_bytes{jwk_ec_coordinate_bytes(curve->to_string())}; if (!coordinate_bytes.has_value()) { return false; } @@ -181,7 +126,7 @@ auto JWK::parse(const JSON &value, JWK &result) -> bool { result.type_ = Type::EllipticCurve; result.curve_ = curve->to_string(); - parsed_key = make_ec_public_key(to_elliptic_curve(result.curve_), + parsed_key = make_ec_public_key(jwk_to_elliptic_curve(result.curve_), decoded_x.value(), decoded_y.value()); } else if (key_type_value == "OKP") { // A public key must not carry the private parameter (RFC 8037 Section 2) @@ -196,7 +141,7 @@ auto JWK::parse(const JSON &value, JWK &result) -> bool { return false; } - const auto key_bytes{okp_key_bytes(curve->to_string())}; + const auto key_bytes{jwk_okp_key_bytes(curve->to_string())}; if (!key_bytes.has_value()) { return false; } @@ -209,8 +154,21 @@ auto JWK::parse(const JSON &value, JWK &result) -> bool { result.type_ = Type::OctetKeyPair; result.curve_ = curve->to_string(); - parsed_key = make_eddsa_public_key(to_edwards_curve(result.curve_), + parsed_key = make_eddsa_public_key(jwk_to_edwards_curve(result.curve_), decoded_public_key.value()); + } else if (key_type_value == "oct") { + const auto *key_value{value.try_at("k", HASH_K)}; + if (key_value == nullptr || !key_value->is_string()) { + return false; + } + + auto decoded_key{base64url_decode(key_value->to_string())}; + if (!decoded_key.has_value() || decoded_key.value().empty()) { + return false; + } + + result.type_ = Type::Octet; + result.secret_ = std::move(decoded_key).value(); } else { return false; } @@ -235,7 +193,8 @@ auto JWK::parse(const JSON &value, JWK &result) -> bool { // and otherwise leave it unset rather than rejecting an otherwise valid key const auto parsed{to_jws_algorithm(algorithm->to_string())}; if (parsed.has_value() && - algorithm_matches_key(parsed.value(), result.type_, result.curve_)) { + jwk_algorithm_matches_key(parsed.value(), to_jwk_kind(result.type_), + result.curve_)) { result.algorithm_ = parsed; } } diff --git a/vendor/core/src/core/jose/jose_jwk_private.cc b/vendor/core/src/core/jose/jose_jwk_private.cc new file mode 100644 index 000000000..378a15eb5 --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwk_private.cc @@ -0,0 +1,258 @@ +#include + +#include + +#include "jose_key.h" + +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move + +namespace { +using namespace std::string_view_literals; + +const auto HASH_KTY{sourcemeta::core::JSON::Object::hash("kty"sv)}; +const auto HASH_N{sourcemeta::core::JSON::Object::hash("n"sv)}; +const auto HASH_E{sourcemeta::core::JSON::Object::hash("e"sv)}; +const auto HASH_D{sourcemeta::core::JSON::Object::hash("d"sv)}; +const auto HASH_P{sourcemeta::core::JSON::Object::hash("p"sv)}; +const auto HASH_Q{sourcemeta::core::JSON::Object::hash("q"sv)}; +const auto HASH_DP{sourcemeta::core::JSON::Object::hash("dp"sv)}; +const auto HASH_DQ{sourcemeta::core::JSON::Object::hash("dq"sv)}; +const auto HASH_QI{sourcemeta::core::JSON::Object::hash("qi"sv)}; +const auto HASH_OTH{sourcemeta::core::JSON::Object::hash("oth"sv)}; +const auto HASH_K{sourcemeta::core::JSON::Object::hash("k"sv)}; +const auto HASH_CRV{sourcemeta::core::JSON::Object::hash("crv"sv)}; +const auto HASH_X{sourcemeta::core::JSON::Object::hash("x"sv)}; +const auto HASH_Y{sourcemeta::core::JSON::Object::hash("y"sv)}; +const auto HASH_KID{sourcemeta::core::JSON::Object::hash("kid"sv)}; +const auto HASH_ALG{sourcemeta::core::JSON::Object::hash("alg"sv)}; + +// A required string member decoded from base64url, empty when the member is +// missing, of the wrong type, malformed, or decodes to nothing +template +auto required_base64url(const sourcemeta::core::JSON &value, + const std::string_view name, const Hash &hash) + -> std::optional { + const auto *member{value.try_at(name, hash)}; + if (member == nullptr || !member->is_string()) { + return std::nullopt; + } + + auto decoded{sourcemeta::core::base64url_decode(member->to_string())}; + if (!decoded.has_value() || decoded.value().empty()) { + return std::nullopt; + } + + return decoded; +} + +auto to_jwk_kind(const sourcemeta::core::JWKPrivate::Type type) noexcept + -> sourcemeta::core::JWKKind { + switch (type) { + case sourcemeta::core::JWKPrivate::Type::RSA: + return sourcemeta::core::JWKKind::RSA; + case sourcemeta::core::JWKPrivate::Type::EllipticCurve: + return sourcemeta::core::JWKKind::EllipticCurve; + case sourcemeta::core::JWKPrivate::Type::OctetKeyPair: + return sourcemeta::core::JWKKind::OctetKeyPair; + case sourcemeta::core::JWKPrivate::Type::Octet: + return sourcemeta::core::JWKKind::Octet; + } + + std::unreachable(); +} + +} // namespace + +namespace sourcemeta::core { + +auto JWKPrivate::parse(const JSON &value, JWKPrivate &result) -> bool { + if (!value.is_object()) { + return false; + } + + const auto *key_type{value.try_at("kty", HASH_KTY)}; + if (key_type == nullptr || !key_type->is_string()) { + return false; + } + + const auto &key_type_value{key_type->to_string()}; + std::optional parsed_key; + if (key_type_value == "RSA") { + // Only the two-prime form is supported, so a multi-prime key is rejected + // (RFC 7518 Section 6.3.2.7) + if (value.try_at("oth", HASH_OTH) != nullptr) { + return false; + } + + // Every private component is required (RFC 7518 Section 6.3.2), since the + // key is built from its full private structure + const auto modulus{required_base64url(value, "n", HASH_N)}; + const auto public_exponent{required_base64url(value, "e", HASH_E)}; + const auto private_exponent{required_base64url(value, "d", HASH_D)}; + const auto prime1{required_base64url(value, "p", HASH_P)}; + const auto prime2{required_base64url(value, "q", HASH_Q)}; + const auto exponent1{required_base64url(value, "dp", HASH_DP)}; + const auto exponent2{required_base64url(value, "dq", HASH_DQ)}; + const auto coefficient{required_base64url(value, "qi", HASH_QI)}; + if (!modulus.has_value() || !public_exponent.has_value() || + !private_exponent.has_value() || !prime1.has_value() || + !prime2.has_value() || !exponent1.has_value() || + !exponent2.has_value() || !coefficient.has_value()) { + return false; + } + + if (!jwk_rsa_modulus_is_allowed(modulus.value())) { + return false; + } + + result.type_ = Type::RSA; + parsed_key = make_rsa_private_key(modulus.value(), public_exponent.value(), + private_exponent.value(), prime1.value(), + prime2.value(), exponent1.value(), + exponent2.value(), coefficient.value()); + } else if (key_type_value == "EC") { + const auto *curve{value.try_at("crv", HASH_CRV)}; + if (curve == nullptr || !curve->is_string()) { + return false; + } + + const auto coordinate_bytes{jwk_ec_coordinate_bytes(curve->to_string())}; + if (!coordinate_bytes.has_value()) { + return false; + } + + // The public coordinates and the private scalar are all required and share + // the curve field width (RFC 7518 Sections 6.2.1 and 6.2.2) + const auto coordinate_x{required_base64url(value, "x", HASH_X)}; + const auto coordinate_y{required_base64url(value, "y", HASH_Y)}; + const auto scalar{required_base64url(value, "d", HASH_D)}; + if (!coordinate_x.has_value() || + coordinate_x.value().size() != coordinate_bytes.value() || + !coordinate_y.has_value() || + coordinate_y.value().size() != coordinate_bytes.value() || + !scalar.has_value() || + scalar.value().size() != coordinate_bytes.value()) { + return false; + } + + result.type_ = Type::EllipticCurve; + result.curve_ = curve->to_string(); + parsed_key = make_ec_private_key(jwk_to_elliptic_curve(result.curve_), + scalar.value(), coordinate_x.value(), + coordinate_y.value()); + } else if (key_type_value == "OKP") { + const auto *curve{value.try_at("crv", HASH_CRV)}; + if (curve == nullptr || !curve->is_string()) { + return false; + } + + const auto key_bytes{jwk_okp_key_bytes(curve->to_string())}; + if (!key_bytes.has_value()) { + return false; + } + + // The public key and the private seed are both required and share the same + // length (RFC 8037 Section 2) + const auto public_key{required_base64url(value, "x", HASH_X)}; + const auto seed{required_base64url(value, "d", HASH_D)}; + if (!public_key.has_value() || + public_key.value().size() != key_bytes.value() || !seed.has_value() || + seed.value().size() != key_bytes.value()) { + return false; + } + + result.type_ = Type::OctetKeyPair; + result.curve_ = curve->to_string(); + parsed_key = make_edwards_private_key(jwk_to_edwards_curve(result.curve_), + seed.value()); + } else if (key_type_value == "oct") { + const auto *key_value{value.try_at("k", HASH_K)}; + if (key_value == nullptr || !key_value->is_string()) { + return false; + } + + auto decoded_key{base64url_decode(key_value->to_string())}; + if (!decoded_key.has_value() || decoded_key.value().empty()) { + return false; + } + + result.type_ = Type::Octet; + result.secret_ = std::move(decoded_key).value(); + } else { + return false; + } + + const auto *key_id{value.try_at("kid", HASH_KID)}; + if (key_id != nullptr) { + if (!key_id->is_string()) { + return false; + } + + result.key_id_ = key_id->to_string(); + } + + const auto *algorithm{value.try_at("alg", HASH_ALG)}; + if (algorithm != nullptr) { + if (!algorithm->is_string()) { + return false; + } + + // The algorithm is an advisory hint (RFC 7517 Section 4.4), so honor it + // only when it names a supported algorithm consistent with the key type, + // and otherwise leave it unset rather than rejecting an otherwise valid key + const auto parsed{to_jws_algorithm(algorithm->to_string())}; + if (parsed.has_value() && + jwk_algorithm_matches_key(parsed.value(), to_jwk_kind(result.type_), + result.curve_)) { + result.algorithm_ = parsed; + } + } + + // The platform key is built once when the material is decoded, so signing + // reuses it. A key that cannot be turned into one stays null and simply fails + // to sign + result.private_key_ = std::move(parsed_key); + return true; +} + +auto JWKPrivate::from(const JSON &value) -> std::optional { + JWKPrivate result; + if (parse(value, result)) { + return result; + } + + return std::nullopt; +} + +auto JWKPrivate::from(JSON &&value) -> std::optional { + return from(value); +} + +auto JWKPrivate::from_pem(const std::string_view pem) + -> std::optional { + auto parsed_key{make_private_key(pem)}; + if (!parsed_key.has_value()) { + return std::nullopt; + } + + JWKPrivate result; + switch (parsed_key.value().type()) { + case PrivateKey::Type::RSA: + result.type_ = Type::RSA; + break; + case PrivateKey::Type::EllipticCurve: + result.type_ = Type::EllipticCurve; + break; + case PrivateKey::Type::Edwards: + result.type_ = Type::OctetKeyPair; + break; + } + + result.private_key_ = std::move(parsed_key); + return result; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jws_sign.cc b/vendor/core/src/core/jose/jose_jws_sign.cc new file mode 100644 index 000000000..a2572daf2 --- /dev/null +++ b/vendor/core/src/core/jose/jose_jws_sign.cc @@ -0,0 +1,116 @@ +#include + +#include + +#include "jose_key.h" + +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::unreachable + +namespace sourcemeta::core { + +auto jws_sign(const JWSAlgorithm algorithm, + const std::string_view signing_input, const JWKPrivate &key) + -> std::optional { + // A key that names an algorithm must not contradict the one in use (RFC 7517 + // Section 4.4) + if (key.algorithm().has_value() && key.algorithm().value() != algorithm) { + return std::nullopt; + } + + const auto *private_key{key.private_key()}; + + switch (algorithm) { + // The asymmetric key material is parsed into a reusable platform key when + // the key is constructed, so an absent one is material that never formed a + // valid key + case JWSAlgorithm::RS256: + case JWSAlgorithm::RS384: + case JWSAlgorithm::RS512: + if (private_key == nullptr || key.type() != JWKPrivate::Type::RSA) { + return std::nullopt; + } + + return rsassa_pkcs1_v15_sign(*private_key, jws_hash_for(algorithm), + signing_input); + case JWSAlgorithm::PS256: + case JWSAlgorithm::PS384: + case JWSAlgorithm::PS512: + if (private_key == nullptr || key.type() != JWKPrivate::Type::RSA) { + return std::nullopt; + } + + return rsassa_pss_sign(*private_key, jws_hash_for(algorithm), + signing_input); + // Each ECDSA algorithm is pinned to exactly one curve (RFC 7518 Section + // 3.4). The raw signature width is twice the curve field width, so it pins + // the key's curve to the algorithm, including for a key parsed from PEM + // which carries no curve name + case JWSAlgorithm::ES256: + case JWSAlgorithm::ES384: + case JWSAlgorithm::ES512: { + if (private_key == nullptr || + key.type() != JWKPrivate::Type::EllipticCurve) { + return std::nullopt; + } + + auto signature{ + ecdsa_sign(*private_key, jws_hash_for(algorithm), signing_input)}; + if (!signature.has_value() || + signature.value().size() != jws_ecdsa_signature_bytes(algorithm)) { + return std::nullopt; + } + + return signature; + } + // The Edwards-curve algorithm names one of two curves through the key + // rather than the algorithm (RFC 8037 Section 3.1), and the key fixes the + // curve when it is parsed + case JWSAlgorithm::EdDSA: + if (private_key == nullptr || + key.type() != JWKPrivate::Type::OctetKeyPair) { + return std::nullopt; + } + + return eddsa_sign(*private_key, signing_input); + // The symmetric algorithms authenticate with the raw secret rather than a + // parsed platform key, and the secret must be at least as large as the hash + // output (RFC 7518 Section 3.2) + case JWSAlgorithm::HS256: { + if (key.type() != JWKPrivate::Type::Octet || + key.secret().size() < jws_hmac_minimum_secret_bytes(algorithm)) { + return std::nullopt; + } + + const auto digest{hmac_sha256_digest(key.secret(), signing_input)}; + return std::string{reinterpret_cast(digest.data()), + digest.size()}; + } + case JWSAlgorithm::HS384: { + if (key.type() != JWKPrivate::Type::Octet || + key.secret().size() < jws_hmac_minimum_secret_bytes(algorithm)) { + return std::nullopt; + } + + const auto digest{hmac_sha384_digest(key.secret(), signing_input)}; + return std::string{reinterpret_cast(digest.data()), + digest.size()}; + } + case JWSAlgorithm::HS512: { + if (key.type() != JWKPrivate::Type::Octet || + key.secret().size() < jws_hmac_minimum_secret_bytes(algorithm)) { + return std::nullopt; + } + + const auto digest{hmac_sha512_digest(key.secret(), signing_input)}; + return std::string{reinterpret_cast(digest.data()), + digest.size()}; + } + } + + std::unreachable(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jws_verify_signature.cc b/vendor/core/src/core/jose/jose_jws_verify_signature.cc index 41555a61e..ebf38bf7f 100644 --- a/vendor/core/src/core/jose/jose_jws_verify_signature.cc +++ b/vendor/core/src/core/jose/jose_jws_verify_signature.cc @@ -2,39 +2,12 @@ #include +#include "jose_key.h" + #include // std::optional #include // std::string_view #include // std::unreachable -namespace { - -auto hash_for(const sourcemeta::core::JWSAlgorithm algorithm) - -> sourcemeta::core::SignatureHashFunction { - using sourcemeta::core::JWSAlgorithm; - using sourcemeta::core::SignatureHashFunction; - switch (algorithm) { - case JWSAlgorithm::RS256: - case JWSAlgorithm::PS256: - case JWSAlgorithm::ES256: - return SignatureHashFunction::SHA256; - case JWSAlgorithm::RS384: - case JWSAlgorithm::PS384: - case JWSAlgorithm::ES384: - return SignatureHashFunction::SHA384; - case JWSAlgorithm::RS512: - case JWSAlgorithm::PS512: - case JWSAlgorithm::ES512: - return SignatureHashFunction::SHA512; - // The Edwards-curve algorithm fixes its own hash, so it never reaches here - case JWSAlgorithm::EdDSA: - break; - } - - std::unreachable(); -} - -} // namespace - namespace sourcemeta::core { auto jws_verify_signature(const std::optional algorithm, @@ -52,47 +25,88 @@ auto jws_verify_signature(const std::optional algorithm, return false; } - // The key material is parsed into a reusable platform key when the key is - // constructed, so an absent one is material that never formed a valid key const auto *public_key{key.public_key()}; - if (public_key == nullptr) { - return false; - } switch (algorithm.value()) { + // The asymmetric key material is parsed into a reusable platform key when + // the key is constructed, so an absent one is material that never formed a + // valid key case JWSAlgorithm::RS256: case JWSAlgorithm::RS384: case JWSAlgorithm::RS512: - return key.type() == JWK::Type::RSA && - rsassa_pkcs1_v15_verify(*public_key, hash_for(algorithm.value()), + return public_key != nullptr && key.type() == JWK::Type::RSA && + rsassa_pkcs1_v15_verify(*public_key, + jws_hash_for(algorithm.value()), signing_input, signature); case JWSAlgorithm::PS256: case JWSAlgorithm::PS384: case JWSAlgorithm::PS512: - return key.type() == JWK::Type::RSA && - rsassa_pss_verify(*public_key, hash_for(algorithm.value()), + return public_key != nullptr && key.type() == JWK::Type::RSA && + rsassa_pss_verify(*public_key, jws_hash_for(algorithm.value()), signing_input, signature); // Each ECDSA algorithm is pinned to exactly one curve (RFC 7518 Section // 3.4), so the key's curve is checked independently of any algorithm it // declares case JWSAlgorithm::ES256: - return key.type() == JWK::Type::EllipticCurve && key.curve() == "P-256" && + return public_key != nullptr && key.type() == JWK::Type::EllipticCurve && + key.curve() == "P-256" && ecdsa_verify(*public_key, SignatureHashFunction::SHA256, signing_input, signature); case JWSAlgorithm::ES384: - return key.type() == JWK::Type::EllipticCurve && key.curve() == "P-384" && + return public_key != nullptr && key.type() == JWK::Type::EllipticCurve && + key.curve() == "P-384" && ecdsa_verify(*public_key, SignatureHashFunction::SHA384, signing_input, signature); case JWSAlgorithm::ES512: - return key.type() == JWK::Type::EllipticCurve && key.curve() == "P-521" && + return public_key != nullptr && key.type() == JWK::Type::EllipticCurve && + key.curve() == "P-521" && ecdsa_verify(*public_key, SignatureHashFunction::SHA512, signing_input, signature); // The Edwards-curve algorithm names one of two curves through the key // rather than the algorithm (RFC 8037 Section 3.1), and the key fixes the // curve when it is parsed case JWSAlgorithm::EdDSA: - return key.type() == JWK::Type::OctetKeyPair && + return public_key != nullptr && key.type() == JWK::Type::OctetKeyPair && eddsa_verify(*public_key, signing_input, signature); + // The symmetric algorithms recompute the tag and compare it in constant + // time (RFC 7518 Section 3.2), and the secret must be at least as large as + // the hash output + case JWSAlgorithm::HS256: { + if (key.type() != JWK::Type::Octet || + key.secret().size() < + jws_hmac_minimum_secret_bytes(algorithm.value())) { + return false; + } + + const auto digest{hmac_sha256_digest(key.secret(), signing_input)}; + return secure_equals( + {reinterpret_cast(digest.data()), digest.size()}, + signature); + } + case JWSAlgorithm::HS384: { + if (key.type() != JWK::Type::Octet || + key.secret().size() < + jws_hmac_minimum_secret_bytes(algorithm.value())) { + return false; + } + + const auto digest{hmac_sha384_digest(key.secret(), signing_input)}; + return secure_equals( + {reinterpret_cast(digest.data()), digest.size()}, + signature); + } + case JWSAlgorithm::HS512: { + if (key.type() != JWK::Type::Octet || + key.secret().size() < + jws_hmac_minimum_secret_bytes(algorithm.value())) { + return false; + } + + const auto digest{hmac_sha512_digest(key.secret(), signing_input)}; + return secure_equals( + {reinterpret_cast(digest.data()), digest.size()}, + signature); + } } std::unreachable(); diff --git a/vendor/core/src/core/jose/jose_jwt.cc b/vendor/core/src/core/jose/jose_jwt.cc index 9f53f2a11..3a297cbb2 100644 --- a/vendor/core/src/core/jose/jose_jwt.cc +++ b/vendor/core/src/core/jose/jose_jwt.cc @@ -4,11 +4,12 @@ #include #include -#include // std::chrono::duration, std::chrono::system_clock -#include // std::optional, std::nullopt -#include // std::out_of_range -#include // std::string_view -#include // std::move +#include // std::chrono::duration, std::chrono::system_clock +#include // std::optional, std::nullopt +#include // std::out_of_range +#include // std::string_view +#include // std::unordered_set +#include // std::move namespace { using namespace std::string_view_literals; @@ -100,6 +101,16 @@ auto JWT::parse(const std::string_view input, JWT &result) -> bool { return false; } + // RFC 7515 Section 4: the header parameter names must be unique, so a header + // with a duplicate is rejected, since the JSON layer preserves repeated + // members rather than collapsing them + std::unordered_set header_parameters; + for (const auto ¶meter : header_json.value().as_object()) { + if (!header_parameters.emplace(parameter.first).second) { + return false; + } + } + // The algorithm header parameter is required and must be a string (RFC 7515 // Section 4.1.1) const auto *algorithm{header_json.value().try_at("alg", HASH_ALG)}; diff --git a/vendor/core/src/core/jose/jose_jwt_check_claims.cc b/vendor/core/src/core/jose/jose_jwt_check_claims.cc index 20ba883e5..38ca56c16 100644 --- a/vendor/core/src/core/jose/jose_jwt_check_claims.cc +++ b/vendor/core/src/core/jose/jose_jwt_check_claims.cc @@ -1,5 +1,6 @@ #include +#include // std::clamp #include // std::chrono::seconds, std::chrono::system_clock #include // std::optional, std::nullopt #include // std::string_view @@ -35,11 +36,28 @@ auto jwt_check_claims(const JWT &token, const std::string_view expected_issuer, return JWTClaimError::Audience; } + // The skew is applied to the server clock rather than the attacker-controlled + // claim, so a NumericDate near the representable bound cannot overflow the + // comparison (the two forms are otherwise equivalent). The skew is also + // clamped to a non-negative, bounded grace period and the shift saturates, so + // an extreme caller-supplied clock or skew stays well-defined too + using Clock = std::chrono::system_clock; + const auto skew{std::clamp(clock_skew, std::chrono::seconds::zero(), + std::chrono::seconds{31556952})}; + const auto skew_ticks{std::chrono::duration_cast(skew)}; + const auto since_epoch{now.time_since_epoch()}; + const auto now_minus_skew{(since_epoch < Clock::duration::min() + skew_ticks) + ? Clock::time_point{Clock::duration::min()} + : now - skew_ticks}; + const auto now_plus_skew{(since_epoch > Clock::duration::max() - skew_ticks) + ? Clock::time_point{Clock::duration::max()} + : now + skew_ticks}; + // A bearer credential without an expiry is not acceptable for authentication, // so the claim is required here even though RFC 7519 makes it optional in // general (RFC 9068 Section 2.2) const auto expires_at{token.expires_at()}; - if (!expires_at.has_value() || now >= expires_at.value() + clock_skew) { + if (!expires_at.has_value() || now_minus_skew >= expires_at.value()) { return JWTClaimError::Expiration; } @@ -49,7 +67,7 @@ auto jwt_check_claims(const JWT &token, const std::string_view expected_issuer, const auto &payload{token.payload()}; if (payload.defines("nbf")) { const auto not_before{token.not_before()}; - if (!not_before.has_value() || now < not_before.value() - clock_skew) { + if (!not_before.has_value() || now_plus_skew < not_before.value()) { return JWTClaimError::NotBefore; } } @@ -58,7 +76,7 @@ auto jwt_check_claims(const JWT &token, const std::string_view expected_issuer, // in the future (RFC 7519 Section 4.1.6) if (payload.defines("iat")) { const auto issued_at{token.issued_at()}; - if (!issued_at.has_value() || now < issued_at.value() - clock_skew) { + if (!issued_at.has_value() || now_plus_skew < issued_at.value()) { return JWTClaimError::IssuedAt; } } diff --git a/vendor/core/src/core/jose/jose_jwt_sign.cc b/vendor/core/src/core/jose/jose_jwt_sign.cc new file mode 100644 index 000000000..a30dfa9a1 --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwt_sign.cc @@ -0,0 +1,67 @@ +#include + +#include +#include + +#include // std::optional, std::nullopt +#include // std::ostringstream +#include // std::string +#include // std::string_view + +namespace { +using namespace std::string_view_literals; + +const auto HASH_ALG{sourcemeta::core::JSON::Object::hash("alg"sv)}; +const auto HASH_CRIT{sourcemeta::core::JSON::Object::hash("crit"sv)}; + +auto compact(const sourcemeta::core::JSON &document) -> std::string { + std::ostringstream stream; + sourcemeta::core::stringify(document, stream); + return stream.str(); +} + +} // namespace + +namespace sourcemeta::core { + +auto jwt_sign(const JSON &header, const JSON &payload, const JWKPrivate &key) + -> std::optional { + if (!header.is_object() || !payload.is_object()) { + return std::nullopt; + } + + // Critical header extensions are not understood, so a token carrying them is + // not emitted rather than producing one the parser would reject (RFC 7515 + // Section 4.1.11) + if (header.try_at("crit", HASH_CRIT) != nullptr) { + return std::nullopt; + } + + // The algorithm is taken from the header (RFC 7515 Section 4.1.1) + const auto *algorithm{header.try_at("alg", HASH_ALG)}; + if (algorithm == nullptr || !algorithm->is_string()) { + return std::nullopt; + } + + const auto parsed{to_jws_algorithm(algorithm->to_string())}; + if (!parsed.has_value()) { + return std::nullopt; + } + + // The signing input is the base64url header and payload joined by a dot (RFC + // 7515 Section 5.1) + std::string signing_input{base64url_encode(compact(header))}; + signing_input.push_back('.'); + signing_input.append(base64url_encode(compact(payload))); + + const auto signature{jws_sign(parsed.value(), signing_input, key)}; + if (!signature.has_value()) { + return std::nullopt; + } + + signing_input.push_back('.'); + signing_input.append(base64url_encode(signature.value())); + return signing_input; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_key.h b/vendor/core/src/core/jose/jose_key.h new file mode 100644 index 000000000..6023213ff --- /dev/null +++ b/vendor/core/src/core/jose/jose_key.h @@ -0,0 +1,210 @@ +#ifndef SOURCEMETA_CORE_JOSE_KEY_H_ +#define SOURCEMETA_CORE_JOSE_KEY_H_ + +// Curve and algorithm mappings shared by the public JWK and the private +// JWKPrivate parsing paths + +#include +#include + +#include // std::countl_zero +#include // std::size_t +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::string_view +#include // std::unreachable + +namespace sourcemeta::core { + +// The kind of key, shared by the public and private parsing paths so that the +// algorithm matching does not need to be written twice +enum class JWKKind : std::uint8_t { RSA, EllipticCurve, OctetKeyPair, Octet }; + +// RFC 7518 Section 3.3: "A key of size 2048 bits or larger MUST be used with" +// the RSASSA algorithms, so a smaller modulus is refused before a key is built +inline constexpr std::size_t MINIMUM_RSA_MODULUS_BITS{2048}; + +inline auto jwk_rsa_modulus_is_allowed(const std::string_view modulus) noexcept + -> bool { + std::size_t offset{0}; + while (offset < modulus.size() && + static_cast(modulus[offset]) == 0) { + offset += 1; + } + + if (offset >= modulus.size()) { + return false; + } + + const auto leading{static_cast(modulus[offset])}; + const auto bits{(modulus.size() - offset - 1) * 8 + + (8 - static_cast(std::countl_zero(leading)))}; + return bits >= MINIMUM_RSA_MODULUS_BITS; +} + +// The coordinate octet length is fixed per curve (RFC 7518 Section 6.2.1.2) +inline auto jwk_ec_coordinate_bytes(const std::string_view curve) + -> std::optional { + if (curve == "P-256") { + return 32; + } else if (curve == "P-384") { + return 48; + } else if (curve == "P-521") { + return 66; + } else { + return std::nullopt; + } +} + +// The key octet length is fixed per Edwards curve (RFC 8032 Sections 5.1.5 and +// 5.2.5), and the private seed shares it (RFC 8037 Section 2) +inline auto jwk_okp_key_bytes(const std::string_view curve) + -> std::optional { + if (curve == "Ed25519") { + return 32; + } else if (curve == "Ed448") { + return 57; + } else { + return std::nullopt; + } +} + +// Both mappings are only reached after the curve has been validated above +inline auto jwk_to_elliptic_curve(const std::string_view curve) noexcept + -> EllipticCurve { + if (curve == "P-256") { + return EllipticCurve::P256; + } else if (curve == "P-384") { + return EllipticCurve::P384; + } else { + return EllipticCurve::P521; + } +} + +inline auto jwk_to_edwards_curve(const std::string_view curve) noexcept + -> EdwardsCurve { + if (curve == "Ed25519") { + return EdwardsCurve::Ed25519; + } else { + return EdwardsCurve::Ed448; + } +} + +// The RSA algorithms only require an RSA key, each ECDSA algorithm is tied to a +// specific curve (RFC 7518 Section 3.1), and the Edwards-curve algorithm +// requires an octet key pair of either curve (RFC 8037 Section 3.1) +inline auto jwk_algorithm_matches_key(const JWSAlgorithm algorithm, + const JWKKind kind, + const std::string_view curve) -> bool { + switch (algorithm) { + case JWSAlgorithm::RS256: + case JWSAlgorithm::RS384: + case JWSAlgorithm::RS512: + case JWSAlgorithm::PS256: + case JWSAlgorithm::PS384: + case JWSAlgorithm::PS512: + return kind == JWKKind::RSA; + case JWSAlgorithm::ES256: + return kind == JWKKind::EllipticCurve && curve == "P-256"; + case JWSAlgorithm::ES384: + return kind == JWKKind::EllipticCurve && curve == "P-384"; + case JWSAlgorithm::ES512: + return kind == JWKKind::EllipticCurve && curve == "P-521"; + case JWSAlgorithm::EdDSA: + return kind == JWKKind::OctetKeyPair; + case JWSAlgorithm::HS256: + case JWSAlgorithm::HS384: + case JWSAlgorithm::HS512: + return kind == JWKKind::Octet; + } + + std::unreachable(); +} + +// The hash function each RSA and ECDSA algorithm computes over the signing +// input (RFC 7518 Section 3.1). The Edwards-curve algorithm fixes its own hash, +// so it never reaches here +inline auto jws_hash_for(const JWSAlgorithm algorithm) + -> SignatureHashFunction { + switch (algorithm) { + case JWSAlgorithm::RS256: + case JWSAlgorithm::PS256: + case JWSAlgorithm::ES256: + return SignatureHashFunction::SHA256; + case JWSAlgorithm::RS384: + case JWSAlgorithm::PS384: + case JWSAlgorithm::ES384: + return SignatureHashFunction::SHA384; + case JWSAlgorithm::RS512: + case JWSAlgorithm::PS512: + case JWSAlgorithm::ES512: + return SignatureHashFunction::SHA512; + case JWSAlgorithm::EdDSA: + case JWSAlgorithm::HS256: + case JWSAlgorithm::HS384: + case JWSAlgorithm::HS512: + break; + } + + std::unreachable(); +} + +// RFC 7518 Section 3.2: "A key of the same size as the hash output (for +// instance, 256 bits for HS256) or larger MUST be used with this algorithm" +inline auto jws_hmac_minimum_secret_bytes(const JWSAlgorithm algorithm) + -> std::size_t { + switch (algorithm) { + case JWSAlgorithm::HS256: + return 32; + case JWSAlgorithm::HS384: + return 48; + case JWSAlgorithm::HS512: + return 64; + case JWSAlgorithm::RS256: + case JWSAlgorithm::RS384: + case JWSAlgorithm::RS512: + case JWSAlgorithm::PS256: + case JWSAlgorithm::PS384: + case JWSAlgorithm::PS512: + case JWSAlgorithm::ES256: + case JWSAlgorithm::ES384: + case JWSAlgorithm::ES512: + case JWSAlgorithm::EdDSA: + break; + } + + std::unreachable(); +} + +// The raw ECDSA signature length for each algorithm, twice the curve field +// width (RFC 7518 Section 3.4). Since each algorithm is pinned to one curve, +// this length identifies the curve the key must use, which the other +// algorithms do not need +inline auto jws_ecdsa_signature_bytes(const JWSAlgorithm algorithm) + -> std::size_t { + switch (algorithm) { + case JWSAlgorithm::ES256: + return 64; + case JWSAlgorithm::ES384: + return 96; + case JWSAlgorithm::ES512: + return 132; + case JWSAlgorithm::RS256: + case JWSAlgorithm::RS384: + case JWSAlgorithm::RS512: + case JWSAlgorithm::PS256: + case JWSAlgorithm::PS384: + case JWSAlgorithm::PS512: + case JWSAlgorithm::EdDSA: + case JWSAlgorithm::HS256: + case JWSAlgorithm::HS384: + case JWSAlgorithm::HS512: + break; + } + + std::unreachable(); +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/json/CMakeLists.txt b/vendor/core/src/core/json/CMakeLists.txt index a9c059067..898e9728a 100644 --- a/vendor/core/src/core/json/CMakeLists.txt +++ b/vendor/core/src/core/json/CMakeLists.txt @@ -7,6 +7,7 @@ if(SOURCEMETA_CORE_INSTALL) endif() target_link_libraries(sourcemeta_core_json PRIVATE sourcemeta::core::io) +target_link_libraries(sourcemeta_core_json PRIVATE sourcemeta::core::text) target_link_libraries(sourcemeta_core_json PRIVATE sourcemeta::core::unicode) target_link_libraries(sourcemeta_core_json PUBLIC sourcemeta::core::numeric) target_link_libraries(sourcemeta_core_json PUBLIC sourcemeta::core::preprocessor) diff --git a/vendor/core/src/core/json/construct.h b/vendor/core/src/core/json/construct.h index 8c385096a..f837136f1 100644 --- a/vendor/core/src/core/json/construct.h +++ b/vendor/core/src/core/json/construct.h @@ -5,6 +5,7 @@ #include #include +#include #include #include "parser.h" @@ -82,18 +83,9 @@ inline auto unescape_string(const char *data, const std::uint32_t length, auto parse_hex4 = [](const char *&position) -> unsigned long { unsigned long value{0}; for (std::size_t index = 0; index < 4; index++) { - const char hex_char{*position++}; - unsigned long digit; - if (hex_char >= '0' && hex_char <= '9') { - digit = static_cast(hex_char - '0'); - } else if (hex_char >= 'a' && hex_char <= 'f') { - digit = static_cast(hex_char - 'a') + 10; - } else if (hex_char >= 'A' && hex_char <= 'F') { - digit = static_cast(hex_char - 'A') + 10; - } else { - digit = 0; - } - value = (value << 4) | digit; + const auto digit{hex_digit_value(*position++)}; + value = (value << 4) | + static_cast(digit < 0 ? 0 : digit); } return value; }; diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json.h b/vendor/core/src/core/json/include/sourcemeta/core/json.h index f294776f4..5044823f4 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json.h @@ -328,6 +328,7 @@ make_set(std::initializer_list types) -> JSON::TypeSet { } // namespace sourcemeta::core +/// @cond template <> struct std::formatter { constexpr auto parse(std::format_parse_context &context) -> decltype(context.begin()) { @@ -355,5 +356,6 @@ template <> struct std::formatter { return std::format_to(context.out(), "{}", stream.str()); } }; +/// @endcond #endif diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_array.h b/vendor/core/src/core/json/include/sourcemeta/core/json_array.h index f71944922..97a2fb575 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_array.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_array.h @@ -7,12 +7,15 @@ namespace sourcemeta::core { /// @ingroup json +/// An ordered sequence of JSON values template class JSONArray { public: // Constructors + /// The underlying container type that holds the array elements using Container = std::vector>; JSONArray() : data{} {} + /// Construct an array from a list of values JSONArray(std::initializer_list values) : data{values} {} // Operators diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_auto.h b/vendor/core/src/core/json/include/sourcemeta/core/json_auto.h index 94fffbec4..03c1522dc 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_auto.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_auto.h @@ -13,7 +13,7 @@ #include // std::optional, std::nullopt, std::bad_optional_access #include // std::tuple, std::apply, std::tuple_element_t, std::tuple_size, std::tuple_size_v #include // std::false_type, std::true_type, std::is_enum_v, std::underlying_type_t, std::is_same_v, std::is_base_of_v, std::remove_cvref_t -#include // std::pair, std:::make_index_sequence, std::index_sequence +#include // std::pair, std::make_index_sequence, std::index_sequence, std::in_range #include // std::variant, std::variant_size_v, std::variant_alternative_t, std::visit namespace sourcemeta::core { @@ -173,7 +173,9 @@ auto from_json(const JSON &value) -> std::optional { template requires(std::is_integral_v && !std::is_same_v) auto from_json(const JSON &value) -> std::optional { - if (value.is_integer()) { + // A value outside the target type's range yields no result rather than a + // silently narrowed one + if (value.is_integer() && std::in_range(value.to_integer())) { return static_cast(value.to_integer()); } else { return std::nullopt; @@ -240,6 +242,7 @@ auto to_json(const T &value) -> JSON { } /// @ingroup json +/// Convert a file time point into JSON template requires std::is_same_v auto to_json(const T value) -> JSON { @@ -356,6 +359,7 @@ auto from_json(const JSON &value) -> std::optional { } /// @ingroup json +/// Convert an optional value into JSON, using null when empty template auto to_json(const std::optional &value) -> JSON { return value.has_value() ? to_json(value.value()) : JSON{nullptr}; } @@ -379,6 +383,7 @@ auto from_json(const JSON &value) -> std::optional { } /// @ingroup json +/// Convert a range of list-like elements into a JSON array template auto to_json(typename T::const_iterator begin, typename T::const_iterator end) -> JSON { @@ -397,6 +402,8 @@ auto to_json(typename T::const_iterator begin, typename T::const_iterator end) } /// @ingroup json +/// Convert a range of list-like elements into a JSON array using a custom +/// callback template F> auto to_json(typename T::const_iterator begin, typename T::const_iterator end, @@ -421,6 +428,7 @@ template auto to_json(const T &value) -> JSON { } /// @ingroup json +/// Convert a list-like value into a JSON array using a custom callback template F> auto to_json(const T &value, const F &callback) -> JSON { @@ -457,6 +465,7 @@ auto from_json(const JSON &value) -> std::optional { } /// @ingroup json +/// Convert a JSON array into a list-like value using a custom callback template auto from_json( const JSON &value, @@ -539,6 +548,7 @@ auto from_json(const JSON &value) -> std::optional { } /// @ingroup json +/// Convert a JSON object into a map-like value using a custom callback template auto from_json( const JSON &value, @@ -569,6 +579,7 @@ auto to_json(const T &value, const F &callback) -> JSON { } /// @ingroup json +/// Convert a pair into a JSON array of two elements template auto to_json(const std::pair &value) -> JSON { auto tuple{JSON::make_array()}; diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_error.h b/vendor/core/src/core/json/include/sourcemeta/core/json_error.h index 0cab22a20..bf2d93ce1 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_error.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_error.h @@ -50,7 +50,7 @@ class SOURCEMETA_CORE_JSON_EXPORT JSONParseError : public std::exception { return this->line_; } - // Get the column number of the error + /// Get the column number of the error [[nodiscard]] auto column() const noexcept -> std::uint64_t { return this->column_; } diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_hash.h b/vendor/core/src/core/json/include/sourcemeta/core/json_hash.h index 728fdc9f7..e18efca98 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_hash.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_hash.h @@ -10,6 +10,7 @@ namespace sourcemeta::core { /// @ingroup json +/// A hash function object for JSON values template struct HashJSON { using hash_type = std::uint64_t; @@ -21,6 +22,7 @@ template struct HashJSON { } } + /// Check whether the given hash is a perfect hash [[nodiscard]] inline auto is_perfect(const hash_type) const noexcept -> bool { return false; @@ -28,6 +30,7 @@ template struct HashJSON { }; /// @ingroup json +/// A hash function object for JSON object property keys template struct PropertyHashJSON { struct hash_type { using type = sourcemeta::core::uint128_t; @@ -37,6 +40,7 @@ template struct PropertyHashJSON { auto operator==(const hash_type &) const noexcept -> bool = default; }; + /// Compute a perfect hash from raw data [[nodiscard]] inline auto perfect(const char *data, const std::size_t size) const noexcept -> hash_type { @@ -211,6 +215,7 @@ template struct PropertyHashJSON { } } + /// Check whether the given hash is a perfect hash [[nodiscard]] inline auto is_perfect(const hash_type &hash) const noexcept -> bool { // If there is anything written past the first byte, diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_object.h b/vendor/core/src/core/json/include/sourcemeta/core/json_object.h index b680d8bbe..44db7b902 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_object.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_object.h @@ -15,6 +15,7 @@ namespace sourcemeta::core { /// @ingroup json +/// A JSON object mapping property keys to values template class JSONObject { public: JSONObject() = default; @@ -23,9 +24,11 @@ template class JSONObject { using mapped_type = Value; using hash_type = typename Hash::hash_type; using pair_value_type = std::pair; + /// The string view type used to look up object keys using KeyView = std::basic_string_view; + /// Construct an object from a list of key and value pairs JSONObject(std::initializer_list entries) : data{} { this->data.reserve(entries.size()); for (auto &&entry : entries) { @@ -33,9 +36,13 @@ template class JSONObject { } } + /// A single object property entry struct Entry { + /// The property key key_type first; + /// The property value mapped_type second; + /// The precomputed hash of the property key hash_type hash; /// Check whether this entry's key equals the given key, comparing the @@ -56,8 +63,12 @@ template class JSONObject { const hash_type key_hash) const -> bool { assert(JSONObject::hash(key) == key_hash); + // A perfect hash captures the key bytes but not its length, so two keys + // that differ only by trailing NUL bytes hash equal. Comparing sizes + // disambiguates them without the cost of a full string comparison return this->hash == key_hash && - (hasher.is_perfect(key_hash) || this->first == key); + (hasher.is_perfect(key_hash) ? this->first.size() == key.size() + : this->first == key); } }; @@ -192,7 +203,8 @@ template class JSONObject { // Move the perfect hash condition out of the loop for extra performance if (this->hasher.is_perfect(key_hash)) { for (size_type index = 0; index < this->size(); index++) { - if (this->data[index].hash == key_hash) { + if (this->data[index].hash == key_hash && + this->data[index].first.size() == key.size()) { auto iterator{this->cbegin()}; std::advance(iterator, index); return iterator; @@ -221,7 +233,8 @@ template class JSONObject { // Move the perfect hash condition out of the loop for extra performance if (this->hasher.is_perfect(key_hash)) { for (size_type index = 0; index < this->size(); index++) { - if (this->data[index].hash == key_hash) { + if (this->data[index].hash == key_hash && + this->data[index].first.size() == key.size()) { auto iterator{this->cbegin()}; std::advance(iterator, index); return iterator; @@ -249,7 +262,7 @@ template class JSONObject { // Move the perfect hash condition out of the loop for extra performance if (this->hasher.is_perfect(hash)) { for (const auto &entry : *this) { - if (entry.hash == hash) { + if (entry.hash == hash && entry.first.size() == key.size()) { return true; } } @@ -273,7 +286,7 @@ template class JSONObject { // Move the perfect hash condition out of the loop for extra performance if (this->hasher.is_perfect(hash)) { for (const auto &entry : *this) { - if (entry.hash == hash) { + if (entry.hash == hash && entry.first.size() == key.size()) { return true; } } @@ -314,7 +327,7 @@ template class JSONObject { // Move the perfect hash condition out of the loop for extra performance if (this->hasher.is_perfect(key_hash)) { for (const auto &entry : *this) { - if (entry.hash == key_hash) { + if (entry.hash == key_hash && entry.first.size() == key.size()) { return entry.second; } } @@ -339,7 +352,7 @@ template class JSONObject { // Move the perfect hash condition out of the loop for extra performance if (this->hasher.is_perfect(key_hash)) { for (const auto &entry : *this) { - if (entry.hash == key_hash) { + if (entry.hash == key_hash && entry.first.size() == key.size()) { return entry.second; } } @@ -361,7 +374,7 @@ template class JSONObject { // Move the perfect hash condition out of the loop for extra performance if (this->hasher.is_perfect(key_hash)) { for (auto &entry : this->data) { - if (entry.hash == key_hash) { + if (entry.hash == key_hash && entry.first.size() == key.size()) { return entry.second; } } @@ -385,7 +398,7 @@ template class JSONObject { // Move the perfect hash condition out of the loop for extra performance if (this->hasher.is_perfect(key_hash)) { for (auto &entry : this->data) { - if (entry.hash == key_hash) { + if (entry.hash == key_hash && entry.first.size() == key.size()) { return entry.second; } } @@ -409,7 +422,8 @@ template class JSONObject { // Move the perfect hash condition out of the loop for extra performance if (this->hasher.is_perfect(key_hash)) { for (size_type index = 0; index < this->size(); index++) { - if (this->data[index].hash == key_hash) { + if (this->data[index].hash == key_hash && + this->data[index].first.size() == key.size()) { return &this->data[index].second; } } @@ -435,7 +449,8 @@ template class JSONObject { // Move the perfect hash condition out of the loop for extra performance if (this->hasher.is_perfect(key_hash)) { for (size_type index = 0; index < this->size(); index++) { - if (this->data[index].hash == key_hash) { + if (this->data[index].hash == key_hash && + this->data[index].first.size() == key.size()) { return &this->data[index].second; } } @@ -462,7 +477,8 @@ template class JSONObject { if (this->hasher.is_perfect(key_hash)) { for (size_type count = 0; count < object_size; count++) { const auto index{(start + count) % object_size}; - if (this->data[index].hash == key_hash) { + if (this->data[index].hash == key_hash && + this->data[index].first.size() == key.size()) { start = index + 1; return &this->data[index].second; } @@ -494,7 +510,8 @@ template class JSONObject { if (this->hasher.is_perfect(key_hash)) { for (size_type count = 0; count < object_size; count++) { const auto index{(start + count) % object_size}; - if (this->data[index].hash == key_hash) { + if (this->data[index].hash == key_hash && + this->data[index].first.size() == key.size()) { start = index + 1; return &this->data[index].second; } @@ -522,7 +539,8 @@ template class JSONObject { if (this->hasher.is_perfect(key_hash)) { for (auto iterator = this->data.begin(); iterator != this->data.end(); ++iterator) { - if (iterator->hash == key_hash) { + if (iterator->hash == key_hash && + iterator->first.size() == key.size()) { iterator->second = value; return key_hash; } else if (iterator->hash == suffix_hash && iterator->first == suffix) { @@ -553,7 +571,7 @@ template class JSONObject { if (this->hasher.is_perfect(key_hash)) { for (auto &entry : this->data) { - if (entry.hash == key_hash) { + if (entry.hash == key_hash && entry.first.size() == key.size()) { entry.second = std::move(value); return key_hash; } @@ -577,7 +595,7 @@ template class JSONObject { if (this->hasher.is_perfect(key_hash)) { for (auto &entry : this->data) { - if (entry.hash == key_hash) { + if (entry.hash == key_hash && entry.first.size() == key.size()) { entry.second = std::move(value); return key_hash; } @@ -601,7 +619,7 @@ template class JSONObject { if (this->hasher.is_perfect(key_hash)) { for (auto &entry : this->data) { - if (entry.hash == key_hash) { + if (entry.hash == key_hash && entry.first.size() == key.size()) { entry.second = value; return key_hash; } @@ -664,7 +682,7 @@ template class JSONObject { if (this->hasher.is_perfect(key_hash)) { for (auto &entry : this->data) { - if (entry.hash == key_hash) { + if (entry.hash == key_hash && entry.first.size() == key.size()) { entry.first = std::move(to); entry.hash = to_hash; break; @@ -688,7 +706,8 @@ template class JSONObject { if (this->hasher.is_perfect(key_hash)) { for (auto iterator = this->data.begin(); iterator != this->data.end(); ++iterator) { - if (iterator->hash == key_hash) { + if (iterator->hash == key_hash && + iterator->first.size() == key.size()) { this->data.erase(iterator); return current_size - 1; } @@ -715,7 +734,8 @@ template class JSONObject { if (this->hasher.is_perfect(key_hash)) { for (auto iterator = this->data.begin(); iterator != this->data.end(); ++iterator) { - if (iterator->hash == key_hash) { + if (iterator->hash == key_hash && + iterator->first.size() == key.size()) { this->data.erase(iterator); return current_size - 1; } 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 ce5fe9d51..55cd02c52 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 @@ -34,6 +34,7 @@ namespace sourcemeta::core { /// @ingroup json +/// This class represents a JSON document. class SOURCEMETA_CORE_JSON_EXPORT JSON { public: /// The character type used by the JSON document. @@ -55,18 +56,31 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { /// The object type used by the JSON document. using Object = JSONObject>; /// The parsing phase of a JSON document. - enum class ParsePhase : std::uint8_t { Pre, Post }; + enum class ParsePhase : std::uint8_t { + /// The phase before a value is parsed. + Pre, + /// The phase after a value is parsed. + Post + }; // The enumeration indexes must stay in sync with the internal variant /// The different types of a JSON instance. enum class Type : std::uint8_t { + /// The JSON null type. Null = 0, + /// The JSON boolean type. Boolean = 1, + /// The JSON integer type. Integer = 2, + /// The JSON real number type. Real = 3, + /// The JSON string type. String = 4, + /// The JSON array type. Array = 5, + /// The JSON object type. Object = 6, + /// The JSON decimal type. Decimal = 7 }; @@ -74,7 +88,14 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { using TypeSet = std::bitset<8>; /// The context type for parse callbacks - enum class ParseContext : std::uint8_t { Root, Property, Index }; + enum class ParseContext : std::uint8_t { + /// The root value of the document + Root, + /// An object property value + Property, + /// An array element value + Index + }; /// An optional callback that can be passed to parsing functions to obtain /// metadata during the parsing process @@ -122,6 +143,7 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { explicit JSON(const int value); // On some systems, `std::int64_t` might be equal to `long` + /// This constructor creates a JSON document from an integer type. template explicit JSON(const long value) requires(!std::is_same_v) @@ -250,6 +272,7 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { /// Misc constructors JSON(const JSON &); + /// A move constructor. JSON(JSON &&) noexcept; auto operator=(const JSON &) -> JSON &; auto operator=(JSON &&) noexcept -> JSON &; @@ -305,12 +328,25 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { * Operators */ - auto operator<(const JSON &other) const noexcept -> bool; - auto operator<=(const JSON &other) const noexcept -> bool; - auto operator>(const JSON &other) const noexcept -> bool; - auto operator>=(const JSON &other) const noexcept -> bool; - auto operator==(const JSON &other) const noexcept -> bool; - auto operator!=(const JSON &) const noexcept -> bool = default; + auto operator<(const JSON &other) const -> bool; + auto operator<=(const JSON &other) const -> bool; + auto operator>(const JSON &other) const -> bool; + auto operator>=(const JSON &other) const -> bool; + + /// Compare two JSON instances for equality. Numbers compare by exact + /// mathematical value across the integer, real, and decimal representations, + /// so the same value in different forms is equal. A real compares by the + /// exact number its floating point form stores, which can differ from the + /// same digits written as a decimal. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// assert(sourcemeta::core::JSON{2} == sourcemeta::core::JSON{2.0}); + /// ``` + auto operator==(const JSON &other) const -> bool; + auto operator!=(const JSON &) const -> bool = default; /// Add two numeric JSON instances and get a new instance with the result. For /// example: @@ -1330,6 +1366,7 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { /// const auto result = document.try_at("foo"); /// EXPECT_TRUE(result); /// EXPECT_EQ(result->to_integer(), 1); + /// ``` [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto try_at(const String &key) const -> const JSON * { assert(this->is_object()); @@ -1362,6 +1399,7 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { /// document.as_object().hash("foo")); /// EXPECT_TRUE(result); /// EXPECT_EQ(result->to_integer(), 1); + /// ``` [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto try_at(const String &key, const typename Object::hash_type hash) const -> const JSON * { diff --git a/vendor/core/src/core/json/json.cc b/vendor/core/src/core/json/json.cc index 27193d0c7..7aadf6d14 100644 --- a/vendor/core/src/core/json/json.cc +++ b/vendor/core/src/core/json/json.cc @@ -16,7 +16,7 @@ #include // std::optional, std::nullopt #include // std::basic_ostream #include // std::conditional_t -#include // std::cmp_greater +#include // std::cmp_greater, std::move #include // std::vector namespace sourcemeta::core { @@ -84,6 +84,42 @@ static auto internal_parse_json(const char *&cursor, const char *end, return output; } +// RFC 8259 §2: JSON-text = ws value ws. A string_view input has no continuation +// consumer, so anything other than whitespace after the top-level value is a +// parse error. This advances past the trailing whitespace and returns the first +// non-whitespace character, or the end if the remainder is all whitespace +static auto skip_trailing_whitespace(const char *cursor, const char *end) + -> const char * { + using namespace internal; + while (cursor != end && + (*cursor == token_whitespace_tabulation || + *cursor == token_whitespace_line_feed || + *cursor == token_whitespace_carriage_return || + *cursor == token_whitespace_space)) { + ++cursor; + } + + return cursor; +} + +// Compute the line and column of a position within the input, matching the +// convention the scanner uses, so that a trailing-content error points at the +// offending character rather than where the value happened to end +static auto position_of(const char *begin, const char *target, + std::uint64_t &line, std::uint64_t &column) -> void { + using namespace internal; + line = 1; + column = 1; + for (const char *iterator{begin}; iterator != target; ++iterator) { + if (*iterator == token_whitespace_line_feed) { + line += 1; + column = 1; + } else { + column += 1; + } + } +} + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) auto parse_json(std::basic_istream &stream, std::uint64_t &line, std::uint64_t &column) -> JSON { @@ -104,6 +140,9 @@ auto parse_json(std::basic_istream &stream, auto parse_json( const std::basic_string_view input, std::uint64_t &line, std::uint64_t &column) -> JSON { + // The overloads that expose the line and column are continuation-style: they + // parse a single value and leave the cursor for the caller to resume from, + // so they do not reject trailing content (see the position-less overloads) const char *cursor{input.empty() ? "" : input.data()}; return internal_parse_json(cursor, cursor + input.size(), line, column, true); } @@ -130,9 +169,17 @@ auto parse_json( const std::basic_string_view input) -> JSON { std::uint64_t line{1}; std::uint64_t column{0}; - const char *cursor{input.empty() ? "" : input.data()}; - return internal_parse_json(cursor, cursor + input.size(), line, column, - false); + const char *begin{input.empty() ? "" : input.data()}; + const char *cursor{begin}; + const char *end{cursor + input.size()}; + auto result{internal_parse_json(cursor, end, line, column, false)}; + const char *trailing{skip_trailing_whitespace(cursor, end)}; + if (trailing != end) { + position_of(begin, trailing, line, column); + throw JSONParseError(line, column); + } + + return result; } auto try_parse_json( @@ -141,9 +188,11 @@ auto try_parse_json( std::uint64_t line{1}; std::uint64_t column{0}; const char *cursor{input.empty() ? "" : input.data()}; + const char *end{cursor + input.size()}; JSON output{nullptr}; - if (internal_parse_json(cursor, cursor + input.size(), line, column, - nullptr, false, output)) { + if (internal_parse_json(cursor, end, line, column, nullptr, false, + output) && + skip_trailing_whitespace(cursor, end) == end) { return output; } @@ -179,6 +228,7 @@ auto parse_json( const std::basic_string_view input, std::uint64_t &line, std::uint64_t &column, JSON &output, const JSON::ParseCallback &callback) -> void { + // Continuation-style overload: see the note on the non-callback variant const char *cursor{input.empty() ? "" : input.data()}; internal_parse_json(cursor, cursor + input.size(), line, column, callback, true, output); @@ -206,9 +256,20 @@ auto parse_json( JSON &output, const JSON::ParseCallback &callback) -> void { std::uint64_t line{1}; std::uint64_t column{0}; - const char *cursor{input.empty() ? "" : input.data()}; - internal_parse_json(cursor, cursor + input.size(), line, column, - callback, false, output); + const char *begin{input.empty() ? "" : input.data()}; + const char *cursor{begin}; + const char *end{cursor + input.size()}; + // Parse into a temporary so that a trailing-content failure leaves the output + // untouched, matching how a malformed value fails before ever populating it + JSON result{nullptr}; + internal_parse_json(cursor, end, line, column, callback, false, result); + const char *trailing{skip_trailing_whitespace(cursor, end)}; + if (trailing != end) { + position_of(begin, trailing, line, column); + throw JSONParseError(line, column); + } + + output = std::move(result); } auto read_json(const std::filesystem::path &path, JSON &output, diff --git a/vendor/core/src/core/json/json_value.cc b/vendor/core/src/core/json/json_value.cc index 3fd229376..569828ac6 100644 --- a/vendor/core/src/core/json/json_value.cc +++ b/vendor/core/src/core/json/json_value.cc @@ -3,12 +3,14 @@ #include // std::ranges::contains, std::ranges::fold_left #include // assert -#include // std::isinf, std::isnan, std::modf +#include // std::isinf, std::isnan, std::modf, std::floor +#include // std::strong_ordering, std::is_eq, std::is_lt #include // std::size_t #include // std::int64_t #include // std::terminate #include // std::reference_wrapper #include // std::initializer_list +#include // std::numeric_limits #include // std::construct_at #include // std::basic_istringstream #include // std::invalid_argument @@ -21,6 +23,95 @@ namespace sourcemeta::core { static constexpr auto TRIM_WHITESPACE = " \t\n\r\v\f"; +namespace { + +// Reverse the direction of a comparison result +auto reverse_ordering(const std::strong_ordering ordering) + -> std::strong_ordering { + if (std::is_lt(ordering)) { + return std::strong_ordering::greater; + } + + if (std::is_gt(ordering)) { + return std::strong_ordering::less; + } + + return std::strong_ordering::equal; +} + +// Order a 64-bit integer against a real by exact value, without ever converting +// the integer to a double, which would lose precision beyond 2^53 +auto integer_real_ordering(const std::int64_t left, const double right) + -> std::strong_ordering { + // Real values in a JSON document are always finite + constexpr double lowest{-9223372036854775808.0}; + constexpr double past_highest{9223372036854775808.0}; + const double floor_of_right{std::floor(right)}; + if (floor_of_right >= past_highest) { + return std::strong_ordering::less; + } + + if (floor_of_right < lowest) { + return std::strong_ordering::greater; + } + + const auto integer_floor{static_cast(floor_of_right)}; + if (left != integer_floor) { + return left < integer_floor ? std::strong_ordering::less + : std::strong_ordering::greater; + } + + // The integer equals the floor of the real, so they are equal only when the + // real has no fractional part, and otherwise the real is the larger of the + // two + return floor_of_right == right ? std::strong_ordering::equal + : std::strong_ordering::less; +} + +// Order two numbers held in different representations by exact mathematical +// value. Integer and integral operands are compared without allocating, and +// only a real against a non-integral or out-of-range decimal falls back to the +// exact arbitrary precision expansion +auto cross_numeric_ordering(const JSON &left, const JSON &right) + -> std::strong_ordering { + if (left.is_integer() && right.is_real()) { + return integer_real_ordering(left.to_integer(), right.to_real()); + } + + if (left.is_real() && right.is_integer()) { + return reverse_ordering( + integer_real_ordering(right.to_integer(), left.to_real())); + } + + if (left.is_real() && right.is_decimal() && + right.to_decimal().is_integral() && right.to_decimal().is_int64()) { + return reverse_ordering( + integer_real_ordering(right.to_decimal().to_int64(), left.to_real())); + } + + if (left.is_decimal() && right.is_real() && left.to_decimal().is_integral() && + left.to_decimal().is_int64()) { + return integer_real_ordering(left.to_decimal().to_int64(), right.to_real()); + } + + const Decimal left_decimal = left.is_decimal() ? left.to_decimal() + : left.is_integer() + ? Decimal{left.to_integer()} + : Decimal::exact_from(left.to_real()); + const Decimal right_decimal = right.is_decimal() ? right.to_decimal() + : right.is_integer() + ? Decimal{right.to_integer()} + : Decimal::exact_from(right.to_real()); + if (left_decimal == right_decimal) { + return std::strong_ordering::equal; + } + + return left_decimal < right_decimal ? std::strong_ordering::less + : std::strong_ordering::greater; +} + +} // namespace + JSON::JSON(const std::int64_t value) : current_type{Type::Integer} { this->data_integer = value; } @@ -433,26 +524,12 @@ auto JSON::size(const String &value) noexcept -> std::size_t { return result; } -// `as_real` is reached only for integer and real operands here, never a -// decimal, so it cannot throw even though it is no longer noexcept -// NOLINTNEXTLINE(bugprone-exception-escape) -auto JSON::operator<(const JSON &other) const noexcept -> bool { - if ((this->type() == Type::Integer && other.type() == Type::Real) || - (this->type() == Type::Real && other.type() == Type::Integer)) { - return this->as_real() < other.as_real(); - } - - if ((this->type() == Type::Decimal && - (other.type() == Type::Integer || other.type() == Type::Real)) || - ((this->type() == Type::Integer || this->type() == Type::Real) && - other.type() == Type::Decimal)) { - const Decimal left = this->is_decimal() ? this->to_decimal() - : this->is_integer() ? Decimal{this->to_integer()} - : Decimal{this->to_real()}; - const Decimal right = other.is_decimal() ? other.to_decimal() - : other.is_integer() ? Decimal{other.to_integer()} - : Decimal{other.to_real()}; - return left < right; +// Ordering numbers of different representations by exact value may allocate an +// arbitrary precision expansion, so this comparison is not noexcept +auto JSON::operator<(const JSON &other) const -> bool { + if (this->is_number() && other.is_number() && + this->current_type != other.current_type) { + return std::is_lt(cross_numeric_ordering(*this, other)); } if (this->type() != other.type()) { @@ -481,38 +558,24 @@ auto JSON::operator<(const JSON &other) const noexcept -> bool { } } -auto JSON::operator<=(const JSON &other) const noexcept -> bool { +auto JSON::operator<=(const JSON &other) const -> bool { return *this < other || *this == other; } -auto JSON::operator>(const JSON &other) const noexcept -> bool { +auto JSON::operator>(const JSON &other) const -> bool { return !(*this < other) && *this != other; } -auto JSON::operator>=(const JSON &other) const noexcept -> bool { +auto JSON::operator>=(const JSON &other) const -> bool { return *this > other || *this == other; } -// `as_real` is reached only for integer and real operands here, never a -// decimal, so it cannot throw even though it is no longer noexcept -// NOLINTNEXTLINE(bugprone-exception-escape) -auto JSON::operator==(const JSON &other) const noexcept -> bool { - if ((this->type() == Type::Integer && other.type() == Type::Real) || - (this->type() == Type::Real && other.type() == Type::Integer)) { - return this->as_real() == other.as_real(); - } - - if ((this->type() == Type::Decimal && - (other.type() == Type::Integer || other.type() == Type::Real)) || - ((this->type() == Type::Integer || this->type() == Type::Real) && - other.type() == Type::Decimal)) { - const Decimal left = this->is_decimal() ? this->to_decimal() - : this->is_integer() ? Decimal{this->to_integer()} - : Decimal{this->to_real()}; - const Decimal right = other.is_decimal() ? other.to_decimal() - : other.is_integer() ? Decimal{other.to_integer()} - : Decimal{other.to_real()}; - return left == right; +// Comparing numbers of different representations by exact value may allocate an +// arbitrary precision expansion, so this comparison is not noexcept +auto JSON::operator==(const JSON &other) const -> bool { + if (this->is_number() && other.is_number() && + this->current_type != other.current_type) { + return std::is_eq(cross_numeric_ordering(*this, other)); } if (this->current_type != other.current_type) { @@ -552,7 +615,16 @@ auto JSON::operator+(const JSON &other) const -> JSON { : Decimal{other.to_real()}; return JSON{left + right}; } else if (this->is_integer() && other.is_integer()) { - return JSON{this->to_integer() + other.to_integer()}; + const auto left{this->to_integer()}; + const auto right{other.to_integer()}; + // Promote to arbitrary precision when the sum would not fit, since signed + // integer overflow is undefined behavior + if ((right > 0 && left > std::numeric_limits::max() - right) || + (right < 0 && left < std::numeric_limits::min() - right)) { + return JSON{Decimal{left} + Decimal{right}}; + } + + return JSON{left + right}; } else if (this->is_integer() && other.is_real()) { return JSON{this->as_real() + other.to_real()}; } else if (this->is_real() && other.is_integer()) { @@ -575,7 +647,16 @@ auto JSON::operator-(const JSON &other) const -> JSON { : Decimal{other.to_real()}; return JSON{left - right}; } else if (this->is_integer() && other.is_integer()) { - return JSON{this->to_integer() - other.to_integer()}; + const auto left{this->to_integer()}; + const auto right{other.to_integer()}; + // Promote to arbitrary precision when the difference would not fit, since + // signed integer overflow is undefined behavior + if ((right < 0 && left > std::numeric_limits::max() + right) || + (right > 0 && left < std::numeric_limits::min() + right)) { + return JSON{Decimal{left} - Decimal{right}}; + } + + return JSON{left - right}; } else if (this->is_integer() && other.is_real()) { return JSON{this->as_real() - other.to_real()}; } else if (this->is_real() && other.is_integer()) { @@ -668,8 +749,21 @@ auto JSON::operator-=(const JSON &substractive) -> JSON & { return this->to_boolean() ? 1 : 0; case Type::Integer: return 4 + (static_cast(this->to_integer()) % 256); - case Type::Real: + case Type::Real: { + // A number that equals an in-range integer must hash like that integer, + // otherwise equal values across representations would hash differently. + // The integer round-trip avoids a std::modf library call + const auto value{this->to_real()}; + if (value >= static_cast(std::numeric_limits::min()) && + value < static_cast(std::numeric_limits::max())) { + const auto truncated{static_cast(value)}; + if (static_cast(truncated) == value) { + return 4 + (static_cast(truncated) % 256); + } + } + return 5; + } case Type::String: return 3 + this->byte_size(); case Type::Array: @@ -687,8 +781,17 @@ auto JSON::operator-=(const JSON &substractive) -> JSON & { return accumulator + 1 + pair.first.size() + pair.second.fast_hash(); }); - case Type::Decimal: - return 8; + case Type::Decimal: { + const auto &decimal{this->to_decimal()}; + if (decimal.is_integral()) { + const auto integral{decimal.to_integral()}; + if (integral.is_int64()) { + return 4 + (static_cast(integral.to_int64()) % 256); + } + } + + return 5; + } default: assert(false); return 0; diff --git a/vendor/core/src/core/json/parser.h b/vendor/core/src/core/json/parser.h index 188c3f2b5..bf8301ee5 100644 --- a/vendor/core/src/core/json/parser.h +++ b/vendor/core/src/core/json/parser.h @@ -3,6 +3,7 @@ #include #include +#include #include "grammar.h" @@ -181,18 +182,11 @@ inline auto scan_string_unicode_code_point(const std::uint64_t line, if (cursor >= end) [[unlikely]] { throw JSONParseError(line, column); } - const char hex_char{*cursor++}; - unsigned long digit; - if (hex_char >= '0' && hex_char <= '9') { - digit = static_cast(hex_char - '0'); - } else if (hex_char >= 'a' && hex_char <= 'f') { - digit = static_cast(hex_char - 'a') + 10; - } else if (hex_char >= 'A' && hex_char <= 'F') { - digit = static_cast(hex_char - 'A') + 10; - } else [[unlikely]] { + const auto digit{hex_digit_value(*cursor++)}; + if (digit < 0) [[unlikely]] { throw JSONParseError(line, column); } - result = (result << 4) | digit; + result = (result << 4) | static_cast(digit); } assert(result <= 0xFFFF); diff --git a/vendor/core/src/core/json/stringify.h b/vendor/core/src/core/json/stringify.h index c32dcca28..93c5f9290 100644 --- a/vendor/core/src/core/json/stringify.h +++ b/vendor/core/src/core/json/stringify.h @@ -11,8 +11,6 @@ #include // std::to_chars #include // std::size_t #include // std::int64_t -#include // std::setprecision -#include // std::noshowpoint, std::fixed #include // std::next, std::cbegin, std::cend, std::back_inserter #include // std::basic_ostream #include // std::ostringstream @@ -81,11 +79,17 @@ auto stringify( if (value == static_cast(0.0)) { stream.write("0.0", 3); } else if (is_integral) { - const auto flags{stream.flags()}; - const auto precision{stream.precision()}; - stream << std::fixed << std::setprecision(1) << value; - stream.flags(flags); - stream.precision(precision); + // Write the integer digits followed by an explicit ".0" to preserve the + // real type. Using to_chars rather than a formatted stream keeps the + // decimal separator independent of the global locale, which otherwise + // corrupts the output under a comma-decimal locale + std::array buffer{}; + const auto result{std::to_chars(buffer.data(), + buffer.data() + buffer.size(), value, + std::chars_format::fixed)}; + assert(result.ec == std::errc{}); + stream.write(buffer.data(), result.ptr - buffer.data()); + stream.write(".0", 2); } else { std::array buffer{}; const auto result{ diff --git a/vendor/core/src/core/jsonl/include/sourcemeta/core/jsonl.h b/vendor/core/src/core/jsonl/include/sourcemeta/core/jsonl.h index 7942caa92..6cdded6f0 100644 --- a/vendor/core/src/core/jsonl/include/sourcemeta/core/jsonl.h +++ b/vendor/core/src/core/jsonl/include/sourcemeta/core/jsonl.h @@ -30,6 +30,7 @@ namespace sourcemeta::core { /// @ingroup jsonl +/// A range over the JSON documents contained in a JSON Lines stream. class SOURCEMETA_CORE_JSONL_EXPORT JSONL { public: /// The mode of operation for the JSONL parser diff --git a/vendor/core/src/core/jsonl/include/sourcemeta/core/jsonl_iterator.h b/vendor/core/src/core/jsonl/include/sourcemeta/core/jsonl_iterator.h index ddba724d7..2ef65cecd 100644 --- a/vendor/core/src/core/jsonl/include/sourcemeta/core/jsonl_iterator.h +++ b/vendor/core/src/core/jsonl/include/sourcemeta/core/jsonl_iterator.h @@ -19,6 +19,7 @@ namespace sourcemeta::core { /// A forward iterator to parse JSON documents out of a JSON Lines stream. class SOURCEMETA_CORE_JSONL_EXPORT ConstJSONLIterator { public: + /// Construct an iterator over the JSON documents in a stream. ConstJSONLIterator(std::basic_istream *stream); ~ConstJSONLIterator(); using iterator_category = std::forward_iterator_tag; diff --git a/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld.h b/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld.h index 2a4ccfa33..bf95c51d3 100644 --- a/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld.h +++ b/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld.h @@ -33,7 +33,12 @@ using JSONLDResolver = std::function(JSON::StringView)>; /// @ingroup jsonld /// The JSON-LD processing mode -enum class JSONLDVersion : std::uint8_t { V1_0, V1_1 }; +enum class JSONLDVersion : std::uint8_t { + /// The JSON-LD 1.0 processing mode + V1_0, + /// The JSON-LD 1.1 processing mode + V1_1 +}; /// @ingroup jsonld /// diff --git a/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld_error.h b/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld_error.h index 4ba412424..02ad9497a 100644 --- a/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld_error.h +++ b/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld_error.h @@ -27,15 +27,16 @@ namespace sourcemeta::core { /// pointer locates the offending position in the input document class SOURCEMETA_CORE_JSONLD_EXPORT JSONLDError : public std::exception { public: + /// Locate the error at an owned pointer. JSONLDError(const char *code, Pointer pointer) : code_{code}, pointer_{std::move(pointer)} {} - // Locate the error at a weak pointer, materialising an owned pointer. + /// Locate the error at a weak pointer, materialising an owned pointer. JSONLDError(const char *code, const WeakPointer &pointer) : code_{code}, pointer_{to_pointer(pointer)} {} - // Locate the error at a weak pointer extended with the given trailing - // property tokens. + /// Locate the error at a weak pointer extended with the given trailing + /// property tokens. JSONLDError(const char *code, const WeakPointer &pointer, const std::initializer_list children) : code_{code}, pointer_{to_pointer(pointer)} { diff --git a/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld_materialize.h b/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld_materialize.h index 9a4fbf5ad..4a0383fa2 100644 --- a/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld_materialize.h +++ b/vendor/core/src/core/jsonld/include/sourcemeta/core/jsonld_materialize.h @@ -20,21 +20,31 @@ namespace sourcemeta::core { /// How a position connects to its parent. The position is the object of the /// edge, asserted in reverse when the flag is set. struct JSONLDEdge { + /// The predicate that connects the position to its parent. JSON::String predicate{}; + /// Whether the edge is asserted in reverse. bool reverse{false}; }; /// @ingroup jsonld /// The base direction of a language-tagged string -enum class JSONLDDirection : std::uint8_t { LTR, RTL }; +enum class JSONLDDirection : std::uint8_t { + /// The left-to-right base direction + LTR, + /// The right-to-left base direction + RTL +}; /// @ingroup jsonld /// A position that is an object. An absent identifier means a fresh blank node, /// and the graph flag asserts the node's descendants in the named graph the /// identifier denotes. struct JSONLDNode { + /// The node identifier, absent for a fresh blank node. std::optional id{}; + /// The types of the node. std::vector types{}; + /// Whether the node's descendants are asserted in the named graph it denotes. bool graph{false}; }; @@ -43,31 +53,56 @@ struct JSONLDNode { /// JSON value, a language may carry a direction, and the JSON flag preserves an /// opaque JSON literal verbatim. struct JSONLDLiteral { + /// The literal datatype, defaulting to the native type of the value. std::optional datatype{}; + /// The language tag of the literal. std::optional language{}; + /// The base direction of the literal. std::optional direction{}; + /// Whether the literal is preserved as an opaque JSON literal. bool json{false}; }; /// @ingroup jsonld /// A scalar promoted to an identified node struct JSONLDReference { + /// The identifier of the promoted node. JSON::String id{}; + /// The types of the promoted node. std::vector types{}; }; /// @ingroup jsonld -/// A position that is an array, asserted as an RDF list when ordered and as a -/// set otherwise +/// The container form of a collection position. List and Set range over an +/// array; Language and Index range over an object. +enum class JSONLDContainer : std::uint8_t { + /// An ordered RDF list ranging over an array + List, + /// An unordered set ranging over an array + Set, + /// A map of language tag to string ranging over an object + Language, + /// A map of index labels carrying no RDF ranging over an object + Index +}; + +/// @ingroup jsonld +/// A position that is a collection. A List is asserted as an RDF list and a Set +/// as a set, both ranging over an array. A Language container ranges over an +/// object of language tag to string, and an Index container over an object +/// whose keys are index labels that carry no RDF. struct JSONLDCollection { - bool ordered{false}; + /// The container form of the collection. + JSONLDContainer container{JSONLDContainer::Set}; }; /// @ingroup jsonld /// The semantics of a single instance position: how it connects to its parent /// and what it is struct JSONLDDescriptor { + /// How the position connects to its parent. std::vector edges{}; + /// What the position is. std::variant value{}; }; diff --git a/vendor/core/src/core/jsonld/jsonld_algorithms.h b/vendor/core/src/core/jsonld/jsonld_algorithms.h index 7e6362d19..693f41377 100644 --- a/vendor/core/src/core/jsonld/jsonld_algorithms.h +++ b/vendor/core/src/core/jsonld/jsonld_algorithms.h @@ -45,7 +45,28 @@ struct ActiveContext { // The mutable state shared by the expansion algorithms for the duration of a // single top-level expansion. +// RAII counter that bounds the depth of the mutually recursive expansion and +// compaction algorithms, decrementing on every exit path +struct NestingDepthScope { + std::size_t &counter; + explicit NestingDepthScope(std::size_t &value) : counter{value} { + this->counter += 1; + } + ~NestingDepthScope() { this->counter -= 1; } + NestingDepthScope(const NestingDepthScope &) = delete; + auto operator=(const NestingDepthScope &) -> NestingDepthScope & = delete; + NestingDepthScope(NestingDepthScope &&) = delete; + auto operator=(NestingDepthScope &&) -> NestingDepthScope & = delete; +}; + struct ExpansionState { + // Bounds the expansion and compaction recursion so a deeply nested document + // cannot overflow the stack on attacker-controlled input. The bound is + // conservative so that it holds on platforms with a small default stack such + // as Windows, and is still far above any realistic nesting + static constexpr std::size_t maximum_depth{100}; + std::size_t depth{0}; + // Used to load remote contexts. The chain detects recursive inclusion. const JSONLDResolver *resolver{nullptr}; std::vector remote_context_chain; diff --git a/vendor/core/src/core/jsonld/jsonld_compaction.cc b/vendor/core/src/core/jsonld/jsonld_compaction.cc index 886840fc5..78bf1b8c0 100644 --- a/vendor/core/src/core/jsonld/jsonld_compaction.cc +++ b/vendor/core/src/core/jsonld/jsonld_compaction.cc @@ -90,6 +90,11 @@ auto compact(ExpansionState &state, const ActiveContext &active_context, const JSON &inverse_context, const std::optional &active_property, const JSON &element, const bool compact_arrays) -> JSON { + const NestingDepthScope scope{state.depth}; + if (state.depth > ExpansionState::maximum_depth) { + throw JSONLDError("Maximum nesting depth exceeded", empty_weak_pointer); + } + if (!element.is_object() && !element.is_array()) { return element; } diff --git a/vendor/core/src/core/jsonld/jsonld_expansion.cc b/vendor/core/src/core/jsonld/jsonld_expansion.cc index 372abe276..cd198e8d4 100644 --- a/vendor/core/src/core/jsonld/jsonld_expansion.cc +++ b/vendor/core/src/core/jsonld/jsonld_expansion.cc @@ -871,6 +871,11 @@ auto expand_entries(ExpansionState &state, ActiveContext &active_context, auto expand(ExpansionState &state, ActiveContext &active_context, const std::optional &active_property, const JSON &element, const WeakPointer &pointer) -> JSON { + const NestingDepthScope scope{state.depth}; + if (state.depth > ExpansionState::maximum_depth) { + throw JSONLDError("Maximum nesting depth exceeded", pointer); + } + if (element.is_null()) { return JSON{nullptr}; } diff --git a/vendor/core/src/core/jsonld/jsonld_materialize.cc b/vendor/core/src/core/jsonld/jsonld_materialize.cc index 8c3643c52..7ed01d81f 100644 --- a/vendor/core/src/core/jsonld/jsonld_materialize.cc +++ b/vendor/core/src/core/jsonld/jsonld_materialize.cc @@ -4,7 +4,8 @@ #include "jsonld_keywords.h" -#include // std::ranges::sort +#include // std::ranges::sort, std::ranges::none_of +#include // assert #include // std::size_t #include // std::reference_wrapper, std::cref #include // std::optional, std::nullopt @@ -20,7 +21,9 @@ namespace { template auto materialize_value(const JSON &value, PointerT &pointer, const JSONLDBasicAnnotationMap &map, - std::vector &standalone) -> std::optional; + std::vector &standalone, + const std::vector **matched_edges = nullptr) + -> std::optional; template auto fill_node(JSON &node, const JSON &instance_object, PointerT &pointer, @@ -189,6 +192,84 @@ auto build_collection(const JSON &value, PointerT &pointer, return result; } +// The keys of an object in sorted order, so the object walk is canonical +// regardless of the instance key order. +auto sorted_keys(const JSON &value) + -> std::vector> { + std::vector> keys; + keys.reserve(value.object_size()); + for (const auto &entry : value.as_object()) { + keys.push_back(std::cref(entry.first)); + } + std::ranges::sort(keys, [](const auto &left, const auto &right) -> bool { + return left.get() < right.get(); + }); + return keys; +} + +// The reserved @none key carries no language. +auto language_literal(const JSON &value, const JSON::String &language, + const bool none) -> JSON { + auto result{JSON::make_object()}; + result.assign_assume_new(JSON::String{KEYWORD_VALUE}, JSON{value}, + KEYWORD_VALUE_HASH); + if (!none) { + result.assign_assume_new(JSON::String{KEYWORD_LANGUAGE}, JSON{language}, + KEYWORD_LANGUAGE_HASH); + } + return result; +} + +auto build_language_collection(const JSON &value) -> JSON { + auto elements{JSON::make_array()}; + for (const auto key : sorted_keys(value)) { + const auto &member{value.at(key.get())}; + const bool none{key.get() == KEYWORD_NONE}; + // A null value or array item in a language map is treated as absent + if (member.is_array()) { + for (const auto &element : member.as_array()) { + if (element.is_null()) { + continue; + } + + assert(element.is_string()); + elements.push_back(language_literal(element, key.get(), none)); + } + } else if (!member.is_null()) { + assert(member.is_string()); + elements.push_back(language_literal(member, key.get(), none)); + } + } + return elements; +} + +// The index keys carry no RDF and are dropped. +template +auto build_index_collection(const JSON &value, PointerT &pointer, + const JSONLDBasicAnnotationMap &map, + std::vector &standalone) -> JSON { + auto elements{JSON::make_array()}; + for (const auto key : sorted_keys(value)) { + push_property(pointer, key.get()); + auto element{ + materialize_value(value.at(key.get()), pointer, map, standalone)}; + pointer.pop_back(); + if (!element.has_value()) { + continue; + } + + // A nested set flattens into the enclosing collection. + if (element->is_array()) { + for (auto &nested : element->as_array()) { + elements.push_back(std::move(nested)); + } + } else { + elements.push_back(std::move(element.value())); + } + } + return elements; +} + template auto materialize_node(const JSONLDNode &descriptor, const JSON &value, PointerT &pointer, @@ -237,7 +318,13 @@ auto materialize_node(const JSONLDNode &descriptor, const JSON &value, template auto materialize_value(const JSON &value, PointerT &pointer, const JSONLDBasicAnnotationMap &map, - std::vector &standalone) -> std::optional { + std::vector &standalone, + const std::vector **matched_edges) + -> std::optional { + if (matched_edges != nullptr) { + *matched_edges = nullptr; + } + if (value.is_null()) { return std::nullopt; } @@ -259,6 +346,9 @@ auto materialize_value(const JSON &value, PointerT &pointer, } const auto &descriptor{iterator->second}; + if (matched_edges != nullptr) { + *matched_edges = &descriptor.edges; + } if (std::holds_alternative(descriptor.value)) { return materialize_node(std::get(descriptor.value), value, pointer, map, standalone); @@ -272,10 +362,27 @@ auto materialize_value(const JSON &value, PointerT &pointer, } const auto &collection{std::get(descriptor.value)}; - if (!value.is_array()) { - return std::nullopt; + switch (collection.container) { + case JSONLDContainer::List: + case JSONLDContainer::Set: + if (!value.is_array()) { + return std::nullopt; + } + return build_collection(value, pointer, map, standalone, + collection.container == JSONLDContainer::List); + case JSONLDContainer::Language: + assert(value.is_object()); + // A language-tagged literal cannot be the object of a reverse property. + assert(std::ranges::none_of( + descriptor.edges, + [](const JSONLDEdge &edge) -> bool { return edge.reverse; })); + return build_language_collection(value); + case JSONLDContainer::Index: + assert(value.is_object()); + return build_index_collection(value, pointer, map, standalone); } - return build_collection(value, pointer, map, standalone, collection.ordered); + + std::unreachable(); } template @@ -293,16 +400,14 @@ auto fill_node(JSON &node, const JSON &instance_object, PointerT &pointer, for (const auto key : keys) { push_property(pointer, key.get()); - const auto child_iterator{map.find(pointer)}; + const std::vector *edges{nullptr}; auto child_value{materialize_value(instance_object.at(key.get()), pointer, - map, standalone)}; + map, standalone, &edges)}; pointer.pop_back(); if (!child_value.has_value()) { continue; } - const std::vector *edges{ - child_iterator == map.cend() ? nullptr : &child_iterator->second.edges}; if (edges == nullptr || edges->empty()) { // Without an edge a node cannot attach to its parent, so it is asserted // as a standalone node in the current graph. A non-node cannot be diff --git a/vendor/core/src/core/jsonpointer/CMakeLists.txt b/vendor/core/src/core/jsonpointer/CMakeLists.txt index 0f89f3d78..8d169617c 100644 --- a/vendor/core/src/core/jsonpointer/CMakeLists.txt +++ b/vendor/core/src/core/jsonpointer/CMakeLists.txt @@ -13,3 +13,5 @@ target_link_libraries(sourcemeta_core_jsonpointer PUBLIC sourcemeta::core::json) target_link_libraries(sourcemeta_core_jsonpointer PUBLIC sourcemeta::core::regex) +target_link_libraries(sourcemeta_core_jsonpointer PRIVATE + sourcemeta::core::text) diff --git a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer.h b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer.h index d3f79ebc6..d02453369 100644 --- a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer.h +++ b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer.h @@ -35,9 +35,12 @@ namespace sourcemeta::core { /// @ingroup jsonpointer +/// A JSON Pointer whose object property tokens own their string values. using Pointer = GenericPointer>; /// @ingroup jsonpointer +/// A JSON Pointer whose object property tokens reference externally owned +/// strings. using WeakPointer = GenericPointer< // We use this instead of a string view as the latter occupies more memory std::reference_wrapper, PropertyHashJSON>; @@ -616,14 +619,19 @@ SOURCEMETA_CORE_JSONPOINTER_EXPORT auto to_uri(const Pointer &pointer, const URI &base) -> URI; /// @ingroup jsonpointer +/// Stringify the input JSON Pointer into a properly escaped URI fragment. SOURCEMETA_CORE_JSONPOINTER_EXPORT auto to_uri(const WeakPointer &pointer) -> URI; /// @ingroup jsonpointer +/// Stringify the input JSON Pointer into a properly escaped URI fragment +/// alongside a base URI. SOURCEMETA_CORE_JSONPOINTER_EXPORT auto to_uri(const WeakPointer &pointer, const URI &base) -> URI; /// @ingroup jsonpointer +/// Stringify the input JSON Pointer into a properly escaped URI fragment +/// alongside a base URI. SOURCEMETA_CORE_JSONPOINTER_EXPORT auto to_uri(const WeakPointer &pointer, const std::string_view base) -> URI; diff --git a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h index c17cb15ee..2a77f7192 100644 --- a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h +++ b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h @@ -20,10 +20,15 @@ namespace sourcemeta::core { /// @ingroup jsonpointer +/// A sequence of reference tokens that identifies a location within a JSON +/// document. template class GenericPointer { public: + /// A single reference token within this pointer using Token = GenericToken; + /// The JSON value type used for serialisation using Value = typename Token::Value; + /// The underlying sequence container of tokens using Container = std::vector; /// This constructor creates an empty JSON Pointer. For example: diff --git a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_position.h b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_position.h index cd14395d0..3408e9982 100644 --- a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_position.h +++ b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_position.h @@ -43,18 +43,28 @@ namespace sourcemeta::core { /// assert(std::get<3>(foo.value()) == 2); /// assert(std::get<4>(foo.value()) == 14); /// ``` +/// +/// The tracker keeps references to the property names of the document it was +/// populated from, so that document must outlive every query against the +/// tracker. Reading the tracker after the source document has been destroyed is +/// undefined behavior. class SOURCEMETA_CORE_JSONPOINTER_EXPORT PointerPositionTracker { public: + /// The pointer type used to look up positions using Pointer = GenericPointer>; + /// The start and end line and column numbers of a value using Position = std::tuple; auto operator()(const JSON::ParsePhase phase, const JSON::Type, const std::uint64_t line, const std::uint64_t column, const JSON::ParseContext context, const std::size_t index, const JSON::String &property) -> void; + /// Get the position of the value at a given pointer [[nodiscard]] auto get(const Pointer &pointer) const -> std::optional; + /// Get the number of tracked positions [[nodiscard]] auto size() const -> std::size_t; + /// Serialise the tracked positions as a JSON document [[nodiscard]] auto to_json() const -> JSON; private: diff --git a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_token.h b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_token.h index 0f97c7bd7..6602495ed 100644 --- a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_token.h +++ b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_token.h @@ -8,10 +8,15 @@ namespace sourcemeta::core { /// @ingroup jsonpointer +/// A single reference token of a JSON Pointer, holding either an object +/// property or an array index. template class GenericToken { public: + /// The JSON value type these tokens convert to using Value = JSON; + /// The stored type of an object property token using Property = PropertyT; + /// The stored type of an array index token using Index = typename Value::Array::size_type; /// This constructor creates an JSON Pointer token from a string given its diff --git a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_walker.h b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_walker.h index 610099c2a..0eb8ca183 100644 --- a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_walker.h +++ b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_walker.h @@ -3,8 +3,10 @@ #include -#include // std::size_t -#include // std::vector +#include // std::reverse +#include // std::size_t, std::ptrdiff_t +#include // std::pair, std::move +#include // std::vector namespace sourcemeta::core { @@ -16,6 +18,7 @@ template class GenericPointerWalker { using internal = typename std::vector; public: + /// Construct a walker over every location in a JSON document GenericPointerWalker(const JSON &document) { PointerT accumulator; this->walk(document, accumulator); @@ -37,19 +40,35 @@ template class GenericPointerWalker { private: auto walk(const JSON &document, PointerT &pointer) -> void { - this->pointers.push_back(pointer); - if (document.is_array()) { - for (std::size_t index = 0; index < document.size(); index++) { - pointer.emplace_back(index); - this->walk(document.at(index), pointer); - pointer.pop_back(); - } - } else if (document.is_object()) { - for (const auto &pair : document.as_object()) { - pointer.emplace_back(pair.first); - this->walk(pair.second, pointer); - pointer.pop_back(); + // Traversal is iterative with an explicit stack so that a deeply nested + // document cannot overflow the call stack. The output ordering is + // unspecified either way + std::vector> pending; + pending.emplace_back(&document, pointer); + while (!pending.empty()) { + auto entry{std::move(pending.back())}; + pending.pop_back(); + const JSON &node{*entry.first}; + // Children are queued then reversed so that they are popped in their + // natural order, preserving the pre-order traversal of the recursive form + const auto start{pending.size()}; + if (node.is_array()) { + for (std::size_t index = 0; index < node.size(); index++) { + PointerT child{entry.second}; + child.emplace_back(index); + pending.emplace_back(&node.at(index), std::move(child)); + } + } else if (node.is_object()) { + for (const auto &pair : node.as_object()) { + PointerT child{entry.second}; + child.emplace_back(pair.first); + pending.emplace_back(&pair.second, std::move(child)); + } } + + std::reverse(pending.begin() + static_cast(start), + pending.end()); + this->pointers.push_back(std::move(entry.second)); } } diff --git a/vendor/core/src/core/jsonpointer/jsonpointer.cc b/vendor/core/src/core/jsonpointer/jsonpointer.cc index 21eb6a1aa..3ca1a22e8 100644 --- a/vendor/core/src/core/jsonpointer/jsonpointer.cc +++ b/vendor/core/src/core/jsonpointer/jsonpointer.cc @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "parser.h" @@ -25,18 +26,6 @@ namespace { -auto uri_hex_value(const char character) -> int { - if (character >= '0' && character <= '9') { - return character - '0'; - } else if (character >= 'A' && character <= 'F') { - return character - 'A' + 10; - } else if (character >= 'a' && character <= 'f') { - return character - 'a' + 10; - } else { - return -1; - } -} - template