diff --git a/CMakeLists.txt b/CMakeLists.txt index 37cdb9649..539d353fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,12 +40,16 @@ message(STATUS "Sourcemeta One edition: ${ONE_EDITION}") find_package(Core REQUIRED) find_package(Blaze REQUIRED) -find_program(JSONSCHEMA_BIN NAMES jsonschema - PATHS "${PROJECT_SOURCE_DIR}/node_modules/.bin" - NO_DEFAULT_PATH REQUIRED) +find_package(JSONBinPack REQUIRED) +find_package(JSONSchema REQUIRED) include(GNUInstallDirs) +if(ONE_DEBUG_SYMBOLS) + sourcemeta_debug_symbols_extract(jsonschema_cli + COMPONENT sourcemeta_jsonschema) +endif() + if(CMAKE_BUILD_TYPE STREQUAL "Release") set(ONE_VERSION "${PROJECT_VERSION}") else() diff --git a/DEPENDENCIES b/DEPENDENCIES index 1639d5964..ade3d9b7a 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,7 +1,9 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 uwebsockets https://github.com/uNetworking/uWebSockets v20.78.0 -core https://github.com/sourcemeta/core 708730fe29acd954ab5fb26b7e5e5f9de07927d4 -blaze https://github.com/sourcemeta/blaze ef88266186084f201b806cbc921d03631b9fac17 +core https://github.com/sourcemeta/core bb1c78e8fa148a2ece951bb776798a43fe328821 +blaze https://github.com/sourcemeta/blaze 6dd44d2e59074d004020e3413c15423ddeba0925 +jsonbinpack https://github.com/sourcemeta/jsonbinpack e2f99ed5e69ab17b027c3d7bb0ef95b27953bb08 +jsonschema https://github.com/sourcemeta/jsonschema 65c553b19ef0fc758c3ec2efa76852422ae8f187 bootstrap https://github.com/twbs/bootstrap v5.3.3 bootstrap-icons https://github.com/twbs/icons v1.11.3 collections/sourcemeta/std/v0 https://github.com/sourcemeta/std v0.4.0 diff --git a/Dockerfile b/Dockerfile index 7fea4186e..4101e7bb5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,6 +39,9 @@ RUN cmake --build /build \ RUN cmake --install /build --prefix /usr --verbose \ --config ${SOURCEMETA_ONE_BUILD_TYPE} \ --component sourcemeta_one +RUN cmake --install /build --prefix /usr --verbose \ + --config ${SOURCEMETA_ONE_BUILD_TYPE} \ + --component sourcemeta_jsonschema # Linting RUN cmake --build /build --config ${SOURCEMETA_ONE_BUILD_TYPE} \ @@ -70,6 +73,12 @@ COPY --from=builder /usr/bin/sourcemeta-one-server \ /usr/bin/sourcemeta-one-server COPY --from=builder /usr/bin/sourcemeta-one-server.debug \ /usr/bin/sourcemeta-one-server.debug +COPY --from=builder /usr/bin/jsonschema \ + /usr/bin/jsonschema +COPY --from=builder /usr/bin/jsonschema.debug \ + /usr/bin/jsonschema.debug +COPY --from=builder /usr/share/bash-completion/completions/jsonschema \ + /usr/share/bash-completion/completions/jsonschema COPY --from=builder /usr/share/sourcemeta/one \ /usr/share/sourcemeta/one diff --git a/cmake/FindBlaze.cmake b/cmake/FindBlaze.cmake index 99e054af8..69cd07219 100644 --- a/cmake/FindBlaze.cmake +++ b/cmake/FindBlaze.cmake @@ -1,8 +1,6 @@ if(NOT Blaze_FOUND) set(BLAZE_INSTALL OFF CACHE BOOL "disable installation") - set(BLAZE_CODEGEN OFF CACHE BOOL "Codegen") set(BLAZE_DOCUMENTATION OFF CACHE BOOL "Documentation") - set(BLAZE_TEST OFF CACHE BOOL "Test runner") add_subdirectory("${PROJECT_SOURCE_DIR}/vendor/blaze") set(Blaze_FOUND ON) endif() diff --git a/cmake/FindCore.cmake b/cmake/FindCore.cmake index e8ec426b4..8d1a92e2a 100644 --- a/cmake/FindCore.cmake +++ b/cmake/FindCore.cmake @@ -9,13 +9,11 @@ if(NOT Core_FOUND) if(ONE_ENTERPRISE) set(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL ON CACHE BOOL "Enable OpenSSL") + set(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL ON CACHE BOOL "Enable system cURL") endif() set(SOURCEMETA_CORE_CONTRIB_GOOGLEBENCHMARK OFF CACHE BOOL "GoogleBenchmark") - set(SOURCEMETA_CORE_LANG_PROCESS OFF CACHE BOOL "Process") - set(SOURCEMETA_CORE_JSONL OFF CACHE BOOL "JSONL") - add_subdirectory("${PROJECT_SOURCE_DIR}/vendor/core") include(Sourcemeta) set(Core_FOUND ON) diff --git a/cmake/FindJSONBinPack.cmake b/cmake/FindJSONBinPack.cmake new file mode 100644 index 000000000..95c58f3ea --- /dev/null +++ b/cmake/FindJSONBinPack.cmake @@ -0,0 +1,5 @@ +if(NOT JSONBinPack_FOUND) + set(JSONBINPACK_INSTALL OFF CACHE BOOL "disable installation") + add_subdirectory("${PROJECT_SOURCE_DIR}/vendor/jsonbinpack") + set(JSONBinPack_FOUND ON) +endif() diff --git a/cmake/FindJSONSchema.cmake b/cmake/FindJSONSchema.cmake new file mode 100644 index 000000000..946e33d01 --- /dev/null +++ b/cmake/FindJSONSchema.cmake @@ -0,0 +1,8 @@ +if(NOT JSONSchema_FOUND) + if(ONE_ENTERPRISE) + set(JSONSCHEMA_USE_SYSTEM_CURL ON CACHE BOOL "Enable system cURL") + endif() + + add_subdirectory("${PROJECT_SOURCE_DIR}/vendor/jsonschema") + set(JSONSchema_FOUND ON) +endif() diff --git a/docs/guide/using-custom-metaschemas.md b/docs/guide/using-custom-metaschemas.md index 1ce3be427..bde50c32c 100644 --- a/docs/guide/using-custom-metaschemas.md +++ b/docs/guide/using-custom-metaschemas.md @@ -1209,11 +1209,11 @@ the instance. Copying the schemas from the test stage is what forces that stage, and therefore the tests, to run as part of the build: ```docker title="Dockerfile" -FROM node:24 AS tests +FROM ghcr.io/sourcemeta/one: AS tests WORKDIR /test COPY schemas schemas COPY test test -RUN npx --yes @sourcemeta/jsonschema test ./test +RUN jsonschema test ./test FROM ghcr.io/sourcemeta/one: COPY one.json . diff --git a/enterprise/Dockerfile b/enterprise/Dockerfile index 42dfbea93..9c994d540 100644 --- a/enterprise/Dockerfile +++ b/enterprise/Dockerfile @@ -3,7 +3,7 @@ FROM debian:trixie AS builder # NodeSource provides npm >= 10, required for the "npm sbom" command RUN apt-get --yes update && apt-get install --yes --no-install-recommends \ build-essential ca-certificates cmake ninja-build sassc esbuild shellcheck curl gnupg \ - openssl libssl-dev openssl-provider-fips \ + openssl libssl-dev openssl-provider-fips libcurl4-openssl-dev \ && curl --fail --silent --show-error --location \ https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ | gpg --dearmor -o /usr/share/keyrings/nodesource.gpg \ @@ -57,6 +57,9 @@ RUN cmake --build /build \ RUN cmake --install /build --prefix /usr --verbose \ --config ${SOURCEMETA_ONE_BUILD_TYPE} \ --component sourcemeta_one +RUN cmake --install /build --prefix /usr --verbose \ + --config ${SOURCEMETA_ONE_BUILD_TYPE} \ + --component sourcemeta_jsonschema # Linting RUN cmake --build /build --config ${SOURCEMETA_ONE_BUILD_TYPE} \ @@ -77,7 +80,7 @@ RUN mkdir -p /usr/share/sourcemeta/one \ FROM debian:trixie-slim RUN apt-get --yes update && apt-get install --yes --no-install-recommends \ - openssl-provider-fips \ + openssl-provider-fips libcurl4t64 \ && apt-get clean && rm -rf /var/lib/apt/lists/* COPY --from=builder /etc/ssl/openssl.cnf /etc/ssl/openssl.cnf @@ -102,6 +105,12 @@ COPY --from=builder /usr/bin/sourcemeta-one-server \ /usr/bin/sourcemeta-one-server COPY --from=builder /usr/bin/sourcemeta-one-server.debug \ /usr/bin/sourcemeta-one-server.debug +COPY --from=builder /usr/bin/jsonschema \ + /usr/bin/jsonschema +COPY --from=builder /usr/bin/jsonschema.debug \ + /usr/bin/jsonschema.debug +COPY --from=builder /usr/share/bash-completion/completions/jsonschema \ + /usr/share/bash-completion/completions/jsonschema COPY --from=builder /usr/share/sourcemeta/one \ /usr/share/sourcemeta/one @@ -111,6 +120,7 @@ RUN ldd /usr/bin/sourcemeta-one-server # Verify that the index binary uses system OpenSSL for cryptography RUN ldd /usr/bin/sourcemeta-one-index | grep libcrypto +RUN ldd /usr/lib/*/libcurl.so.4 | grep libcrypto # Verify that the OpenSSL FIPS provider is configured and present RUN grep -q 'fips = fips_sect' /etc/ssl/openssl.cnf RUN test -f /usr/lib/*/ossl-modules/fips.so diff --git a/enterprise/scripts/sbom-vendorpull.js b/enterprise/scripts/sbom-vendorpull.js index fc7ba4e0e..f89c37220 100755 --- a/enterprise/scripts/sbom-vendorpull.js +++ b/enterprise/scripts/sbom-vendorpull.js @@ -7,6 +7,8 @@ import { fileURLToPath } from "node:url"; const LICENSES = { "core": "AGPL-3.0-or-later OR LicenseRef-Commercial", "blaze": "AGPL-3.0-or-later OR LicenseRef-Commercial", + "jsonbinpack": "AGPL-3.0-or-later OR LicenseRef-Commercial", + "jsonschema": "AGPL-3.0-or-later OR LicenseRef-Commercial", "uwebsockets": "Apache-2.0", "bootstrap": "MIT", "bootstrap-icons": "MIT", @@ -24,6 +26,9 @@ const IGNORED = new Set([ "referencing-suite", "uritemplate-test", "pyca-cryptography", + "wycheproof", + "jose-cookbook", + "ctrf", "googletest", "googlebenchmark", "jsonschema-2020-12", diff --git a/package-lock.json b/package-lock.json index 0d0164233..0bbfabdb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ }, "devDependencies": { "@playwright/test": "^1.60.0", - "@sourcemeta/jsonschema": "^15.10.1", "jsdom": "^29.1.1" } }, @@ -448,31 +447,6 @@ "node": ">=18" } }, - "node_modules/@sourcemeta/jsonschema": { - "version": "15.10.1", - "resolved": "https://registry.npmjs.org/@sourcemeta/jsonschema/-/jsonschema-15.10.1.tgz", - "integrity": "sha512-kJIzx1utVGABs/FjRfsfDAGlU3HkuT4SfvWEE2zEkwTwafqcxtaBBjRlTKfTVJ9B00ThK9yw3Wbb2yskcMI3/g==", - "cpu": [ - "x64", - "arm64" - ], - "dev": true, - "license": "AGPL-3.0", - "os": [ - "darwin", - "linux", - "win32" - ], - "bin": { - "jsonschema": "npm/cli.js" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sourcemeta" - } - }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", diff --git a/package.json b/package.json index d02b6e98e..f766ca7cd 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ }, "devDependencies": { "@playwright/test": "^1.60.0", - "@sourcemeta/jsonschema": "^15.10.1", "jsdom": "^29.1.1" } } diff --git a/src/configuration/CMakeLists.txt b/src/configuration/CMakeLists.txt index 3dde16eb2..9ac27417d 100644 --- a/src/configuration/CMakeLists.txt +++ b/src/configuration/CMakeLists.txt @@ -5,11 +5,12 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT one NAME configuration set(CONFIGURATION_SCHEMA "${CMAKE_CURRENT_SOURCE_DIR}/schema") add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/template.h" - COMMAND "${JSONSCHEMA_BIN}" compile + COMMAND "$" compile --include CONFIGURATION "${CONFIGURATION_SCHEMA}/configuration.json" > "${CMAKE_CURRENT_BINARY_DIR}/template.h" DEPENDS + jsonschema_cli "${CONFIGURATION_SCHEMA}/configuration.json" "${CONFIGURATION_SCHEMA}/collection.json" "${CONFIGURATION_SCHEMA}/contents.json" diff --git a/test/e2e/meta/Dockerfile b/test/e2e/meta/Dockerfile index 7d10205f1..805c4663b 100644 --- a/test/e2e/meta/Dockerfile +++ b/test/e2e/meta/Dockerfile @@ -1,12 +1,8 @@ -FROM debian:trixie AS tests -RUN apt-get --yes update && apt-get install --yes --no-install-recommends \ - nodejs npm && apt-get clean && rm -rf /var/lib/apt/lists/* +FROM one AS tests WORKDIR /test -COPY --from=root package.json package-lock.json ./ -RUN npm ci COPY schemas schemas COPY test test -RUN npx --no-install jsonschema test ./test +RUN jsonschema test ./test FROM one COPY one.json . diff --git a/test/e2e/meta/compose.yml b/test/e2e/meta/compose.yml index 1dfe65018..714999012 100644 --- a/test/e2e/meta/compose.yml +++ b/test/e2e/meta/compose.yml @@ -3,8 +3,6 @@ services: build: context: . dockerfile: Dockerfile - additional_contexts: - root: ../../.. args: SOURCEMETA_ONE_SANDBOX_EDITION: ${EDITION} environment: diff --git a/test/unit/configuration/CMakeLists.txt b/test/unit/configuration/CMakeLists.txt index 41f3d2f53..483316c57 100644 --- a/test/unit/configuration/CMakeLists.txt +++ b/test/unit/configuration/CMakeLists.txt @@ -11,11 +11,11 @@ target_compile_definitions(sourcemeta_one_configuration_unit set(CONFIGURATION_SCHEMA "${PROJECT_SOURCE_DIR}/src/configuration/schema") add_test(NAME one.configuration.schema COMMAND - "${JSONSCHEMA_BIN}" test --extension .test.json + "$" test --extension .test.json "${CMAKE_CURRENT_SOURCE_DIR}/configuration.test.json") add_test(NAME one.configuration.schema.fmt COMMAND - "${JSONSCHEMA_BIN}" fmt --check "${CONFIGURATION_SCHEMA}") + "$" fmt --check "${CONFIGURATION_SCHEMA}") add_test(NAME one.configuration.schema.metaschema COMMAND - "${JSONSCHEMA_BIN}" metaschema "${CONFIGURATION_SCHEMA}") + "$" metaschema "${CONFIGURATION_SCHEMA}") add_test(NAME one.configuration.schema.lint COMMAND - "${JSONSCHEMA_BIN}" lint "${CONFIGURATION_SCHEMA}") + "$" lint "${CONFIGURATION_SCHEMA}") diff --git a/test/unit/self/CMakeLists.txt b/test/unit/self/CMakeLists.txt index 3a1222564..0b624086e 100644 --- a/test/unit/self/CMakeLists.txt +++ b/test/unit/self/CMakeLists.txt @@ -1,8 +1,8 @@ set(SELF_SCHEMAS "${PROJECT_SOURCE_DIR}/src/self/v1/schemas") add_test(NAME one.self.schemas.fmt COMMAND - "${JSONSCHEMA_BIN}" fmt --check "${SELF_SCHEMAS}") + "$" fmt --check "${SELF_SCHEMAS}") add_test(NAME one.self.schemas.metaschema COMMAND - "${JSONSCHEMA_BIN}" metaschema "${SELF_SCHEMAS}") + "$" metaschema "${SELF_SCHEMAS}") add_test(NAME one.self.schemas.lint COMMAND - "${JSONSCHEMA_BIN}" lint "${SELF_SCHEMAS}") + "$" lint "${SELF_SCHEMAS}") diff --git a/vendor/blaze/DEPENDENCIES b/vendor/blaze/DEPENDENCIES index 2bf88c518..a71569883 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 df8f2970ccf85a3a3f01e004ac436ff916f8c52a +core https://github.com/sourcemeta/core bb1c78e8fa148a2ece951bb776798a43fe328821 jsonschema-test-suite https://github.com/json-schema-org/JSON-Schema-Test-Suite 60755c1097769e313fae3ec4d63bcc9d49b5d2d5 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/schemas/canonical-draft3.json b/vendor/blaze/schemas/canonical-draft3.json index 9089bf0c8..f9a350759 100644 --- a/vendor/blaze/schemas/canonical-draft3.json +++ b/vendor/blaze/schemas/canonical-draft3.json @@ -137,9 +137,6 @@ }, "format": { "type": "string" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -169,9 +166,6 @@ }, "maximum": { "type": "number" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -207,9 +201,6 @@ }, "exclusiveMaximum": { "type": "boolean" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -256,9 +247,6 @@ "type": "boolean" } ] - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -292,9 +280,6 @@ }, "uniqueItems": { "type": "boolean" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -348,9 +333,6 @@ }, "uniqueItems": { "type": "boolean" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -397,6 +379,9 @@ "items": { "$ref": "#/$defs/schema" } + }, + "required": { + "type": "boolean" } }, "unevaluatedProperties": false @@ -416,6 +401,7 @@ "properties": { "disallow": { "type": "array", + "maxItems": 1, "minItems": 1, "items": { "$ref": "#/$defs/schema" diff --git a/vendor/blaze/src/alterschema/CMakeLists.txt b/vendor/blaze/src/alterschema/CMakeLists.txt index 37c5b5a15..ed3d4f501 100644 --- a/vendor/blaze/src/alterschema/CMakeLists.txt +++ b/vendor/blaze/src/alterschema/CMakeLists.txt @@ -12,13 +12,19 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema 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/exclusive_maximum_boolean_integer_fold.h @@ -43,6 +49,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema 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 @@ -50,6 +57,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema 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 diff --git a/vendor/blaze/src/alterschema/alterschema.cc b/vendor/blaze/src/alterschema/alterschema.cc index 4e511019c..0d7a23f25 100644 --- a/vendor/blaze/src/alterschema/alterschema.cc +++ b/vendor/blaze/src/alterschema/alterschema.cc @@ -118,14 +118,20 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #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/exclusive_maximum_boolean_integer_fold.h" @@ -151,6 +157,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #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" @@ -158,6 +165,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #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" @@ -511,6 +519,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); @@ -523,9 +532,16 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { 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(); diff --git a/vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h b/vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h index 2bf641b4f..360f38bbb 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h +++ b/vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h @@ -27,6 +27,15 @@ class DependentRequiredToAnyOf final : public SchemaTransformRule { ONLY_CONTINUE_IF(std::ranges::any_of( dependent_required->as_object(), [](const auto &entry) { return entry.second.is_array(); })); + + if (!vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_2020_12_Applicator})) { + throw SchemaError( + "Cannot canonicalise `dependentRequired` without the Applicator " + "vocabulary"); + } + return true; } @@ -45,19 +54,16 @@ class DependentRequiredToAnyOf final : public SchemaTransformRule { 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()}; - not_branch.assign("not", std::move(not_required)); + auto absence_branch{JSON::make_object()}; + absence_branch.assign("properties", JSON::make_object()); + absence_branch.at("properties").assign(entry.first, JSON{false}); auto required_branch{JSON::make_object()}; required_branch.assign("type", JSON{"object"}); required_branch.assign("required", std::move(required_all)); auto pair{JSON::make_array()}; - pair.push_back(std::move(not_branch)); + pair.push_back(std::move(absence_branch)); pair.push_back(std::move(required_branch)); auto wrapper{JSON::make_object()}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h b/vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h index 9ea0aa69a..989ecb2ca 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h +++ b/vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h @@ -23,6 +23,15 @@ class DependentSchemasToAnyOf final : public SchemaTransformRule { const auto *dependent_schemas{schema.try_at("dependentSchemas")}; ONLY_CONTINUE_IF(dependent_schemas && dependent_schemas->is_object() && !dependent_schemas->empty()); + + if (!vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_2020_12_Validation})) { + throw SchemaError( + "Cannot canonicalise `dependentSchemas` without the Validation " + "vocabulary"); + } + return true; } @@ -30,12 +39,9 @@ class DependentSchemasToAnyOf final : public SchemaTransformRule { auto result_branches{JSON::make_array()}; for (const auto &entry : schema.at("dependentSchemas").as_object()) { - 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()}; - not_branch.assign("not", std::move(not_required)); + auto absence_branch{JSON::make_object()}; + absence_branch.assign("properties", JSON::make_object()); + absence_branch.at("properties").assign(entry.first, JSON{false}); auto required_obj{JSON::make_object()}; required_obj.assign("type", JSON{"object"}); @@ -50,7 +56,7 @@ class DependentSchemasToAnyOf final : public SchemaTransformRule { allof_branch.assign("allOf", std::move(all_of)); auto pair{JSON::make_array()}; - pair.push_back(std::move(not_branch)); + pair.push_back(std::move(absence_branch)); pair.push_back(std::move(allof_branch)); auto wrapper{JSON::make_object()}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_array_to_extends.h b/vendor/blaze/src/alterschema/canonicalizer/disallow_array_to_extends.h new file mode 100644 index 000000000..293b9bd5c --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/disallow_array_to_extends.h @@ -0,0 +1,85 @@ +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"} {}; + + [[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_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && disallow->size() > 1); + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + auto branches{JSON::make_array()}; + for (const auto &element : schema.at("disallow").as_array()) { + auto negation{JSON::make_array()}; + negation.push_back(element); + auto branch{JSON::make_object()}; + branch.assign("disallow", std::move(negation)); + branches.push_back(std::move(branch)); + } + + schema.erase("disallow"); + + if (schema.defines("extends") && schema.at("extends").is_array()) { + this->extends_start_ = schema.at("extends").size(); + for (auto &branch : branches.as_array()) { + schema.at("extends").push_back(std::move(branch)); + } + } else if (schema.defines("extends")) { + auto extends{JSON::make_array()}; + extends.push_back(schema.at("extends")); + this->extends_start_ = extends.size(); + for (auto &branch : branches.as_array()) { + extends.push_back(std::move(branch)); + } + schema.assign("extends", std::move(extends)); + } else { + this->extends_start_ = 0; + schema.assign("extends", std::move(branches)); + } + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + const auto disallow_prefix{current.concat({"disallow"})}; + if (!target.starts_with(disallow_prefix)) { + return target; + } + + const auto relative{target.resolve_from(disallow_prefix)}; + if (relative.empty() || !relative.at(0).is_index()) { + return target; + } + + const auto index{relative.at(0).to_index()}; + return target.rebase( + current.concat({"disallow", index}), + current.concat( + {"extends", this->extends_start_ + index, "disallow", 0})); + } + +private: + mutable std::size_t extends_start_{0}; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_double_negation.h b/vendor/blaze/src/alterschema/canonicalizer/disallow_double_negation.h new file mode 100644 index 000000000..f0bb5b557 --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/disallow_double_negation.h @@ -0,0 +1,86 @@ +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"} {}; + + [[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) const + -> SchemaTransformRule::Result override { + static const 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->size() == 1); + ONLY_CONTINUE_IF(is_single_negation(disallow->at(0))); + + // Lifting the inner schema merges its keywords into this node, so the node + // must assert nothing besides `disallow` (otherwise a sibling constraint + // sharing a key with the inner schema would be silently clobbered) + ONLY_CONTINUE_IF( + wraps_single_constraint(schema, "disallow", walker, vocabularies)); + + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + auto inner{schema.at("disallow").at(0).at("disallow").at(0)}; + schema.erase("disallow"); + + while (is_single_negation(inner) && + is_single_negation(inner.at("disallow").at(0))) { + auto next{inner.at("disallow").at(0).at("disallow").at(0)}; + inner = std::move(next); + } + + if (inner.is_object()) { + schema.merge(inner.as_object()); + } + } + +private: + static auto is_single_negation(const sourcemeta::core::JSON &schema) -> bool { + return schema.is_object() && schema.size() == 1 && + schema.defines("disallow") && schema.at("disallow").is_array() && + schema.at("disallow").size() == 1; + } + + static auto wraps_single_constraint( + const sourcemeta::core::JSON &schema, const std::string_view keyword, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::Vocabularies &vocabularies) -> bool { + for (const auto &entry : schema.as_object()) { + if (entry.first == keyword) { + continue; + } + + const auto type{walker(entry.first, vocabularies).type}; + if (type != SchemaKeywordType::Annotation && + type != SchemaKeywordType::Comment && + type != SchemaKeywordType::Other && + type != SchemaKeywordType::Unknown && + type != SchemaKeywordType::LocationMembers) { + return false; + } + } + + return true; + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_extends_to_type.h b/vendor/blaze/src/alterschema/canonicalizer/disallow_extends_to_type.h new file mode 100644 index 000000000..7cb88c1bf --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/disallow_extends_to_type.h @@ -0,0 +1,109 @@ +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"} {}; + + [[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) const + -> SchemaTransformRule::Result override { + 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("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && disallow->size() == 1); + + const auto &element{disallow->at(0)}; + ONLY_CONTINUE_IF(element.is_object() && element.defines("extends") && + element.at("extends").is_array() && + !element.at("extends").empty()); + + // Only a pure negation can be distributed: the schema must assert nothing + // besides `disallow` (otherwise the new `type` would clobber a sibling + // constraint), and the negated schema must assert nothing besides `extends` + // (otherwise those conjuncts would be silently dropped) + ONLY_CONTINUE_IF( + wraps_single_constraint(schema, "disallow", walker, vocabularies) && + wraps_single_constraint(element, "extends", walker, vocabularies)); + + // 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"}; + auto wrapper_pointer{location.pointer}; + wrapper_pointer.push_back(std::cref(DISALLOW)); + wrapper_pointer.push_back(static_cast(0)); + ONLY_CONTINUE_IF(!frame.has_references_to(wrapper_pointer)); + + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + auto branches{JSON::make_array()}; + for (auto &branch : schema.at("disallow").at(0).at("extends").as_array()) { + auto negation{JSON::make_array()}; + negation.push_back(std::move(branch)); + auto element{JSON::make_object()}; + element.assign("disallow", std::move(negation)); + branches.push_back(std::move(element)); + } + + schema.erase("disallow"); + schema.assign("type", std::move(branches)); + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + const auto extends_prefix{current.concat({"disallow", 0, "extends"})}; + if (!target.starts_with(extends_prefix)) { + return target; + } + + const auto relative{target.resolve_from(extends_prefix)}; + if (relative.empty() || !relative.at(0).is_index()) { + return target; + } + + const auto index{relative.at(0).to_index()}; + return target.rebase(extends_prefix.concat({index}), + current.concat({"type", index, "disallow", 0})); + } + +private: + static auto wraps_single_constraint( + const sourcemeta::core::JSON &schema, const std::string_view keyword, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::Vocabularies &vocabularies) -> bool { + for (const auto &entry : schema.as_object()) { + if (entry.first == keyword) { + continue; + } + + const auto type{walker(entry.first, vocabularies).type}; + if (type != SchemaKeywordType::Annotation && + type != SchemaKeywordType::Comment && + type != SchemaKeywordType::Other && + type != SchemaKeywordType::Unknown && + type != SchemaKeywordType::LocationMembers) { + return false; + } + } + + return true; + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_type_union_to_extends.h b/vendor/blaze/src/alterschema/canonicalizer/disallow_type_union_to_extends.h new file mode 100644 index 000000000..6763e68da --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/disallow_type_union_to_extends.h @@ -0,0 +1,109 @@ +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"} {}; + + [[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) const + -> SchemaTransformRule::Result override { + 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("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && disallow->size() == 1); + + const auto &element{disallow->at(0)}; + ONLY_CONTINUE_IF(element.is_object() && element.defines("type") && + element.at("type").is_array() && + !element.at("type").empty()); + + // Only a pure negation can be distributed: the schema must assert nothing + // besides `disallow` (otherwise the new `extends` would clobber a sibling + // constraint), and the negated schema must assert nothing besides its + // `type` union (otherwise those conjuncts would be silently dropped) + ONLY_CONTINUE_IF( + wraps_single_constraint(schema, "disallow", walker, vocabularies) && + wraps_single_constraint(element, "type", walker, vocabularies)); + + // 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"}; + auto wrapper_pointer{location.pointer}; + wrapper_pointer.push_back(std::cref(DISALLOW)); + wrapper_pointer.push_back(static_cast(0)); + ONLY_CONTINUE_IF(!frame.has_references_to(wrapper_pointer)); + + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + auto branches{JSON::make_array()}; + for (auto &member : schema.at("disallow").at(0).at("type").as_array()) { + auto negation{JSON::make_array()}; + negation.push_back(std::move(member)); + auto branch{JSON::make_object()}; + branch.assign("disallow", std::move(negation)); + branches.push_back(std::move(branch)); + } + + schema.erase("disallow"); + schema.assign("extends", std::move(branches)); + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + const auto type_prefix{current.concat({"disallow", 0, "type"})}; + if (!target.starts_with(type_prefix)) { + return target; + } + + const auto relative{target.resolve_from(type_prefix)}; + if (relative.empty() || !relative.at(0).is_index()) { + return target; + } + + const auto index{relative.at(0).to_index()}; + return target.rebase(type_prefix.concat({index}), + current.concat({"extends", index, "disallow", 0})); + } + +private: + static auto wraps_single_constraint( + const sourcemeta::core::JSON &schema, const std::string_view keyword, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::Vocabularies &vocabularies) -> bool { + for (const auto &entry : schema.as_object()) { + if (entry.first == keyword) { + continue; + } + + const auto type{walker(entry.first, vocabularies).type}; + if (type != SchemaKeywordType::Annotation && + type != SchemaKeywordType::Comment && + type != SchemaKeywordType::Other && + type != SchemaKeywordType::Unknown && + type != SchemaKeywordType::LocationMembers) { + return false; + } + } + + return true; + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/duplicate_disallow_entries.h b/vendor/blaze/src/alterschema/canonicalizer/duplicate_disallow_entries.h new file mode 100644 index 000000000..aecdcda26 --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/duplicate_disallow_entries.h @@ -0,0 +1,58 @@ +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"} {}; + + [[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) const + -> SchemaTransformRule::Result override { + 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("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && !disallow->unique()); + + // Compacting the array would shift the index of every entry that follows a + // removed duplicate, so a reference into `disallow` could silently end up + // pointing at a different subschema. Leave such cases untouched and let + // `DisallowArrayToExtends` split them instead, which preserves every index + // as its own `extends` branch + const std::string keyword{"disallow"}; + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, WeakPointer::Token{std::cref(keyword)})); + + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + const auto &original{schema.at("disallow")}; + + std::unordered_set, + HashJSON>, + EqualJSON>> + seen; + auto result{JSON::make_array()}; + + for (const auto &element : original.as_array()) { + if (seen.emplace(std::cref(element)).second) { + result.push_back(element); + } + } + + schema.assign("disallow", std::move(result)); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/empty_disallow_drop.h b/vendor/blaze/src/alterschema/canonicalizer/empty_disallow_drop.h new file mode 100644 index 000000000..429e85c7f --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/empty_disallow_drop.h @@ -0,0 +1,29 @@ +class EmptyDisallowDrop final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + EmptyDisallowDrop() : SchemaTransformRule{"empty_disallow_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) const + -> SchemaTransformRule::Result override { + 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("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && disallow->empty()); + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.erase("disallow"); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h b/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h index 8a4d9b515..b92335042 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h +++ b/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h @@ -63,6 +63,27 @@ class EnumDropRedundantValidation final : public SchemaTransformRule { continue; } + // In Draft 3 and older, `required` and `optional` are property-presence + // flags read by the parent object validator, not value assertions that an + // `enum` could make redundant + if (entry.first == "required" && + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_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 (entry.second.is_boolean() && entry.second.to_boolean()) { if (!frame.has_references_through( location.pointer, WeakPointer::Token{std::cref(entry.first)})) { diff --git a/vendor/blaze/src/alterschema/canonicalizer/required_to_extends.h b/vendor/blaze/src/alterschema/canonicalizer/required_to_extends.h new file mode 100644 index 000000000..44e9845e8 --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/required_to_extends.h @@ -0,0 +1,99 @@ +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"} {}; + + [[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_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *required{schema.try_at("required")}; + ONLY_CONTINUE_IF(required && required->is_boolean()); + + for (const auto &entry : schema.as_object()) { + if (!stays_at_top(entry.first)) { + return true; + } + } + + return false; + } + + auto transform(JSON &schema, const Result &) const -> void override { + this->wrapped_keywords_.clear(); + for (const auto &entry : schema.as_object()) { + if (!stays_at_top(entry.first)) { + this->wrapped_keywords_.push_back(entry.first); + } + } + + auto branch{JSON::make_object()}; + for (const auto &keyword : this->wrapped_keywords_) { + branch.assign(keyword, schema.at(keyword)); + } + + for (const auto &keyword : this->wrapped_keywords_) { + schema.erase(keyword); + } + + if (schema.defines("extends") && schema.at("extends").is_array()) { + this->branch_index_ = schema.at("extends").size(); + schema.at("extends").push_back(std::move(branch)); + } 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()}; + 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()}; + 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 { + for (const auto &keyword : this->wrapped_keywords_) { + const auto keyword_prefix{current.concat({keyword})}; + if (target.starts_with(keyword_prefix)) { + return target.rebase( + keyword_prefix, + current.concat({"extends", this->branch_index_, keyword})); + } + } + + return target; + } + +private: + static auto stays_at_top(const sourcemeta::core::JSON::String &keyword) + -> bool { + return keyword == "required" || keyword == "extends" || + keyword == "$schema" || keyword == "id" || keyword == "$ref"; + } + + mutable std::vector wrapped_keywords_; + mutable std::size_t branch_index_{0}; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h b/vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h index 5dbedf63b..99b186261 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h +++ b/vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h @@ -18,15 +18,20 @@ class TypeArrayToAnyOf final : public SchemaTransformRule { const sourcemeta::blaze::SchemaResolver &, const bool) const -> SchemaTransformRule::Result override { - ONLY_CONTINUE_IF(vocabularies.contains_any( - {Vocabularies::Known::JSON_Schema_2020_12_Validation, - Vocabularies::Known::JSON_Schema_2020_12_Applicator, - Vocabularies::Known::JSON_Schema_2019_09_Validation, - 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()); + 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 *type{schema.try_at("type")}; ONLY_CONTINUE_IF(type && type->is_array()); diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_union_distribute_keywords.h b/vendor/blaze/src/alterschema/canonicalizer/type_union_distribute_keywords.h new file mode 100644 index 000000000..04ba7d5ee --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/type_union_distribute_keywords.h @@ -0,0 +1,207 @@ +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"} {}; + + [[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) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_array() && !type->empty()); + for (const auto &branch : type->as_array()) { + ONLY_CONTINUE_IF(branch.is_object()); + } + + this->moves_.clear(); + this->wrap_keywords_.clear(); + this->wrap_ = false; + 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 + if (entry.first == "type" || entry.first == "required") { + continue; + } + + const auto &metadata{walker(entry.first, vocabularies)}; + if (metadata.type == sourcemeta::blaze::SchemaKeywordType::Reference) { + continue; + } + + // A keyword that applies to every type carries no type-specific + // information to push down into a branch + if (metadata.instances.none()) { + continue; + } + + movable.push_back(entry.first); + + std::vector targets; + bool has_match{false}; + bool conflict{false}; + for (std::size_t index = 0; index < type->size(); ++index) { + const auto branch_types{branch_type_set(type->at(index))}; + if ((branch_types & metadata.instances).none()) { + continue; + } + + has_match = true; + // A matching branch already constrains this keyword, so distributing + // and erasing the sibling could drop the top-level bound from that + // branch. Wrap instead so nothing is weakened. + if (type->at(index).defines(entry.first)) { + conflict = true; + break; + } + + targets.push_back(index); + } + + if (!has_match || conflict) { + this->wrap_ = true; + } else { + this->moves_.emplace_back(entry.first, std::move(targets)); + } + } + + ONLY_CONTINUE_IF(!movable.empty()); + if (this->wrap_) { + this->moves_.clear(); + this->wrap_keywords_ = std::move(movable); + } + + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + if (this->wrap_) { + auto union_branch{JSON::make_object()}; + union_branch.assign("type", schema.at("type")); + auto sibling_branch{JSON::make_object()}; + for (const auto &keyword : this->wrap_keywords_) { + sibling_branch.assign(keyword, schema.at(keyword)); + } + + schema.erase("type"); + for (const auto &keyword : this->wrap_keywords_) { + schema.erase(keyword); + } + + if (schema.defines("extends") && schema.at("extends").is_array()) { + this->type_index_ = schema.at("extends").size(); + schema.at("extends").push_back(std::move(union_branch)); + this->sibling_index_ = schema.at("extends").size(); + schema.at("extends").push_back(std::move(sibling_branch)); + } else { + auto extends{JSON::make_array()}; + this->type_index_ = 0; + extends.push_back(std::move(union_branch)); + this->sibling_index_ = 1; + extends.push_back(std::move(sibling_branch)); + schema.assign("extends", std::move(extends)); + } + + return; + } + + for (const auto &entry : this->moves_) { + const auto value{schema.at(entry.first)}; + auto &type{schema.at("type")}; + for (const auto index : entry.second) { + type.at(index).assign(entry.first, value); + } + } + + for (const auto &entry : this->moves_) { + schema.erase(entry.first); + } + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + if (this->wrap_) { + const auto type_prefix{current.concat({"type"})}; + if (target.starts_with(type_prefix)) { + return target.rebase( + type_prefix, + current.concat({"extends", this->type_index_, "type"})); + } + + for (const auto &keyword : this->wrap_keywords_) { + const auto keyword_prefix{current.concat({keyword})}; + if (target.starts_with(keyword_prefix)) { + return target.rebase( + keyword_prefix, + current.concat({"extends", this->sibling_index_, keyword})); + } + } + + return target; + } + + for (const auto &entry : this->moves_) { + if (entry.second.empty()) { + continue; + } + + const auto keyword_prefix{current.concat({entry.first})}; + if (target.starts_with(keyword_prefix)) { + return target.rebase( + keyword_prefix, + current.concat({"type", entry.second.front(), entry.first})); + } + } + + return target; + } + +private: + static auto branch_type_set(const sourcemeta::core::JSON &branch) + -> sourcemeta::core::JSON::TypeSet { + if (!branch.is_object()) { + return {}; + } + + const auto *type{branch.try_at("type")}; + if (type && (type->is_string() || type->is_array())) { + return parse_schema_type(*type); + } + + const auto *enum_value{branch.try_at("enum")}; + if (enum_value && enum_value->is_array()) { + sourcemeta::core::JSON::TypeSet result; + for (const auto &value : enum_value->as_array()) { + result.set(std::to_underlying(value.type())); + } + return result; + } + + return {}; + } + + mutable std::vector< + std::pair>> + moves_; + mutable std::vector wrap_keywords_; + mutable bool wrap_{false}; + mutable std::size_t type_index_{0}; + mutable std::size_t sibling_index_{0}; +}; diff --git a/vendor/blaze/src/compiler/compile_helpers.h b/vendor/blaze/src/compiler/compile_helpers.h index 8bfb33416..a36190e9e 100644 --- a/vendor/blaze/src/compiler/compile_helpers.h +++ b/vendor/blaze/src/compiler/compile_helpers.h @@ -409,7 +409,10 @@ inline auto required_properties(const SchemaContext &schema_context) schema_context.schema.at("properties").is_object()) { for (const auto &entry : schema_context.schema.at("properties").as_object()) { + // In Draft 3, keywords sibling to `$ref` are never evaluated, so a + // `required` flag next to a `$ref` does not make the property mandatory if (entry.second.is_object() && entry.second.defines("required") && + !entry.second.defines("$ref") && entry.second.at("required").is_boolean() && entry.second.at("required").to_boolean()) { result.insert(entry.first); diff --git a/vendor/blaze/src/foundation/foundation.cc b/vendor/blaze/src/foundation/foundation.cc index 438ffba3c..e3f54b0ad 100644 --- a/vendor/blaze/src/foundation/foundation.cc +++ b/vendor/blaze/src/foundation/foundation.cc @@ -265,6 +265,102 @@ auto sourcemeta::blaze::dialect(const sourcemeta::core::JSON &schema, return dialect_value.to_string(); } +// A meta-schema that is not known to the resolver may still be embedded in +// the document itself. Across every official base dialect, the only +// containers that can hold embedded resources are `$defs` and `definitions`, +// which no custom dialect can redefine away. A candidate only counts if its +// entire meta-schema chain terminates at an official base dialect and every +// embedded link declares its identifier and sits in a container in a way +// that is valid for such base dialect +auto sourcemeta::blaze::metaschema_try_embedded( + const sourcemeta::core::JSON &schema, const std::string_view identifier, + const SchemaResolver &resolver) -> const sourcemeta::core::JSON * { + // Relative or invalid meta-schema references are not acceptable + // according to the JSON Schema specifications + if (!sourcemeta::core::URI::is_uri(identifier)) { + return nullptr; + } + + const auto candidate{ + sourcemeta::blaze::embedded_metaschema_candidate(schema, identifier)}; + if (!candidate.first) { + return nullptr; + } + + std::unordered_set visited; + std::vector links{ + {.schema = candidate.first, + .identifier = identifier, + .container = candidate.second}}; + // Chain links that the resolver knows about are returned by value, so we + // keep them alive while we walk the chain, in a container that never + // relocates its elements, as we hold views into them + std::deque resolved; + const auto *current{candidate.first}; + std::string_view current_identifier{identifier}; + std::optional terminal; + + while (true) { + // The meta-schema is present, but its chain can never terminate at an + // official base dialect, just like a self-descriptive or cyclic + // meta-schema that the resolver knows about + if (!visited.emplace(current_identifier).second) { + throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); + } + + if (!current->is_object()) { + throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); + } + + const auto *metaschema_dialect{current->try_at("$schema")}; + if (!metaschema_dialect || !metaschema_dialect->is_string()) { + throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); + } + + const auto &dialect_uri{metaschema_dialect->to_string()}; + const auto known{sourcemeta::blaze::to_base_dialect(dialect_uri)}; + if (known.has_value()) { + terminal = known; + break; + } + + auto remote{resolver(dialect_uri)}; + if (remote.has_value()) { + resolved.push_back(std::move(remote).value()); + current = &resolved.back(); + current_identifier = dialect_uri; + continue; + } + + if (!sourcemeta::core::URI::is_uri(dialect_uri)) { + return nullptr; + } + + const auto next{ + sourcemeta::blaze::embedded_metaschema_candidate(schema, dialect_uri)}; + if (!next.first) { + return nullptr; + } + + links.push_back({.schema = next.first, + .identifier = dialect_uri, + .container = next.second}); + current = next.first; + current_identifier = dialect_uri; + } + + assert(terminal.has_value()); + for (const auto &link : links) { + if (!sourcemeta::blaze::embedded_metaschema_link_valid( + *(link.schema), link.identifier, link.container, + terminal.value())) { + return nullptr; + } + } + + return candidate.first; +} + auto sourcemeta::blaze::metaschema( const sourcemeta::core::JSON &schema, const sourcemeta::blaze::SchemaResolver &resolver, @@ -275,6 +371,15 @@ auto sourcemeta::blaze::metaschema( throw sourcemeta::blaze::SchemaUnknownDialectError(); } + // A meta-schema that is embedded in the schema itself takes precedence + // over what the resolver knows about, as the schema pins the exact + // meta-schema it is described by + const auto *embedded{sourcemeta::blaze::metaschema_try_embedded( + schema, effective_dialect, resolver)}; + if (embedded) { + return *embedded; + } + const auto maybe_metaschema{resolver(effective_dialect)}; if (!maybe_metaschema.has_value()) { // Relative meta-schema references are invalid according to the @@ -297,7 +402,8 @@ base_dialect_with_visited(const sourcemeta::core::JSON &schema, const sourcemeta::blaze::SchemaResolver &resolver, std::string_view default_dialect, std::unordered_set &visited, - const bool allow_dialect_override) + const bool allow_dialect_override, + const sourcemeta::core::JSON &document) -> std::optional { assert(sourcemeta::blaze::is_schema(schema)); const std::string_view effective_dialect{sourcemeta::blaze::dialect( @@ -320,6 +426,22 @@ base_dialect_with_visited(const sourcemeta::core::JSON &schema, throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); } + // A meta-schema that is embedded in the original document itself takes + // precedence over what the resolver knows about, as the document pins + // the exact meta-schema it is described by + const auto *embedded{sourcemeta::blaze::metaschema_try_embedded( + document, effective_dialect, resolver)}; + if (embedded) { + const std::string_view embedded_dialect{sourcemeta::blaze::dialect( + *embedded, effective_dialect, allow_dialect_override)}; + if (embedded_dialect == effective_dialect) { + throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); + } + + return base_dialect_with_visited(*embedded, resolver, effective_dialect, + visited, allow_dialect_override, document); + } + // Otherwise, traverse the metaschema hierarchy up const std::optional metaschema{ resolver(effective_dialect)}; @@ -353,7 +475,7 @@ base_dialect_with_visited(const sourcemeta::core::JSON &schema, return base_dialect_with_visited(metaschema.value(), resolver, effective_dialect, visited, - allow_dialect_override); + allow_dialect_override, document); } auto sourcemeta::blaze::base_dialect( @@ -363,7 +485,7 @@ auto sourcemeta::blaze::base_dialect( -> std::optional { std::unordered_set visited; return base_dialect_with_visited(schema, resolver, default_dialect, visited, - allow_dialect_override); + allow_dialect_override, schema); } namespace { @@ -562,8 +684,21 @@ auto sourcemeta::blaze::vocabularies( throw sourcemeta::blaze::SchemaUnknownDialectError(); } - return vocabularies(resolver, resolved_base_dialect.value(), - resolved_dialect); + // A meta-schema that is embedded in the schema itself takes precedence + // over what the resolver knows about, as the schema pins the exact + // meta-schema it is described by + return vocabularies( + [&schema, &resolver](const std::string_view identifier) + -> std::optional { + const auto *embedded{sourcemeta::blaze::metaschema_try_embedded( + schema, identifier, resolver)}; + if (embedded) { + return *embedded; + } + + return resolver(identifier); + }, + resolved_base_dialect.value(), resolved_dialect); } auto sourcemeta::blaze::vocabularies(const SchemaResolver &resolver, diff --git a/vendor/blaze/src/foundation/helpers.h b/vendor/blaze/src/foundation/helpers.h index 8a3d3b197..9345c288e 100644 --- a/vendor/blaze/src/foundation/helpers.h +++ b/vendor/blaze/src/foundation/helpers.h @@ -3,8 +3,16 @@ #include -#include // assert -#include // std::string_view +#include + +#include // assert +#include // std::deque +#include // std::initializer_list +#include // std::optional +#include // std::string_view +#include // std::unordered_set +#include // std::pair, std::move +#include // std::vector namespace sourcemeta::blaze { @@ -84,6 +92,126 @@ ref_overrides_adjacent_keywords(const SchemaBaseDialect base_dialect) -> bool { } } +inline auto embedded_metaschema_identifier_matches( + const sourcemeta::core::JSON &candidate, const std::string_view keyword, + const std::string_view identifier, + const std::optional &canonical) -> bool { + const auto *value{ + candidate.try_at(sourcemeta::core::JSON::StringView{keyword})}; + if (!value || !value->is_string()) { + return false; + } + + const auto ¤t{value->to_string()}; + if (current == identifier) { + return true; + } + + if (canonical.has_value()) { + try { + return sourcemeta::core::URI::canonicalize(current) == canonical.value(); + } catch (const sourcemeta::core::URIParseError &) { + return false; + } + } + + return false; +} + +inline auto embedded_metaschema_matches( + const sourcemeta::core::JSON &candidate, const std::string_view identifier, + const std::optional &canonical) -> bool { + if (!candidate.is_object()) { + return false; + } + + for (const auto *const keyword : {"$id", "id"}) { + if (embedded_metaschema_identifier_matches(candidate, keyword, identifier, + canonical)) { + return true; + } + } + + return false; +} + +inline auto +embedded_metaschema_candidate(const sourcemeta::core::JSON &document, + const std::string_view identifier) + -> std::pair { + if (!document.is_object()) { + return {nullptr, ""}; + } + + std::optional canonical; + try { + canonical = sourcemeta::core::URI::canonicalize(identifier); + } catch (const sourcemeta::core::URIParseError &) { + canonical = std::nullopt; + } + + for (const auto *const container : {"$defs", "definitions"}) { + const auto *entries{document.try_at(container)}; + if (!entries || !entries->is_object()) { + continue; + } + + const auto *direct{ + entries->try_at(sourcemeta::core::JSON::StringView{identifier})}; + if (direct && embedded_metaschema_matches(*direct, identifier, canonical)) { + return {direct, container}; + } + + for (const auto &entry : entries->as_object()) { + if (embedded_metaschema_matches(entry.second, identifier, canonical)) { + return {&entry.second, container}; + } + } + } + + return {nullptr, ""}; +} + +inline auto embedded_metaschema_link_valid(const sourcemeta::core::JSON &link, + const std::string_view identifier, + const std::string_view container, + const SchemaBaseDialect base_dialect) + -> bool { + // In 2019-09 and 2020-12, `definitions` is still supported + // for backwards compatibility + switch (base_dialect) { + case SchemaBaseDialect::JSON_Schema_2020_12: + case SchemaBaseDialect::JSON_Schema_2020_12_Hyper: + case SchemaBaseDialect::JSON_Schema_2019_09: + case SchemaBaseDialect::JSON_Schema_2019_09_Hyper: + if (container != "$defs" && container != "definitions") { + return false; + } + + break; + default: + if (container != definitions_keyword(base_dialect)) { + return false; + } + } + + std::optional canonical; + try { + canonical = sourcemeta::core::URI::canonicalize(identifier); + } catch (const sourcemeta::core::URIParseError &) { + canonical = std::nullopt; + } + + return embedded_metaschema_identifier_matches(link, id_keyword(base_dialect), + identifier, canonical); +} + +struct EmbeddedMetaschemaLink { + const sourcemeta::core::JSON *schema; + sourcemeta::core::JSON::StringView identifier; + std::string_view container; +}; + } // namespace sourcemeta::blaze #endif diff --git a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h index 4f8cc5cd4..ee3945167 100644 --- a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h +++ b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h @@ -268,6 +268,43 @@ auto dialect(const sourcemeta::core::JSON &schema, std::string_view default_dialect = "", bool allow_dialect_override = true) -> std::string_view; +/// @ingroup foundation +/// +/// Try to locate the meta-schema that the given schema declares from within +/// the schema itself, as self-contained schemas embed the meta-schemas they +/// depend on. The result points into the given document and is null if no +/// valid embedded meta-schema could be found. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const sourcemeta::core::JSON schema = +/// sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "https://example.com/meta", +/// "$defs": { +/// "https://example.com/meta": { +/// "$id": "https://example.com/meta", +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "object" +/// } +/// } +/// })JSON"); +/// +/// const auto *metaschema{sourcemeta::blaze::metaschema_try_embedded( +/// schema, "https://example.com/meta", +/// sourcemeta::blaze::schema_resolver)}; +/// +/// assert(metaschema); +/// assert(metaschema == &schema.at("$defs").at("https://example.com/meta")); +/// ``` +SOURCEMETA_BLAZE_FOUNDATION_EXPORT +auto metaschema_try_embedded(const sourcemeta::core::JSON &schema, + std::string_view identifier, + const SchemaResolver &resolver) + -> const sourcemeta::core::JSON *; + /// @ingroup foundation /// /// Get the metaschema document that describes the given schema. For example: diff --git a/vendor/blaze/src/frame/frame.cc b/vendor/blaze/src/frame/frame.cc index dd5aa18fb..4fca9ba59 100644 --- a/vendor/blaze/src/frame/frame.cc +++ b/vendor/blaze/src/frame/frame.cc @@ -184,11 +184,17 @@ auto find_anchors(const sourcemeta::core::JSON &schema, } } - // Draft 4 + // Draft 4 and 3 // Old `id` anchor form if (schema.is_object() && - vocabularies.contains( - sourcemeta::blaze::Vocabularies::Known::JSON_Schema_Draft_4)) { + (vocabularies.contains( + sourcemeta::blaze::Vocabularies::Known::JSON_Schema_Draft_4) || + vocabularies.contains( + sourcemeta::blaze::Vocabularies::Known::JSON_Schema_Draft_4_Hyper) || + vocabularies.contains( + sourcemeta::blaze::Vocabularies::Known::JSON_Schema_Draft_3) || + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_Draft_3_Hyper))) { const auto *id_value{schema.try_at("id")}; if (id_value) { assert(id_value->is_string()); @@ -196,7 +202,7 @@ auto find_anchors(const sourcemeta::core::JSON &schema, // A bare "#" carries no anchor name, so we treat it as no anchor at // all. if (id_view.starts_with('#') && id_view.size() > 1) { - // Draft 4 imposes no plain-name pattern on the fragment, but the + // Draft 4 and 3 impose no plain-name pattern on the fragment, but the // value must still be a valid URI reference per RFC 3986 if (!sourcemeta::core::URI::is_uri_reference(id_view)) { throw sourcemeta::blaze::SchemaKeywordError( @@ -315,6 +321,8 @@ auto supports_id_anchors( case SchemaBaseDialect::JSON_Schema_Draft_6_Hyper: case SchemaBaseDialect::JSON_Schema_Draft_4: case SchemaBaseDialect::JSON_Schema_Draft_4_Hyper: + case SchemaBaseDialect::JSON_Schema_Draft_3: + case SchemaBaseDialect::JSON_Schema_Draft_3_Hyper: return true; default: return false; @@ -567,6 +575,28 @@ auto SchemaFrame::analyse(const sourcemeta::core::JSON &root, sourcemeta::core::WeakPointer::Hasher>( paths.cbegin(), paths.cend()) .size() == paths.size())); + + // A meta-schema that is embedded in the document itself takes precedence + // over what the resolver knows about, as the document pins the exact + // meta-schema it is described by + const SchemaResolver effective_resolver{ + [&root, &resolver, this](const std::string_view identifier) + -> std::optional { + const sourcemeta::core::JSON::String key{identifier}; + const auto hit{this->probed_metaschemas_.find(key)}; + if (hit != this->probed_metaschemas_.cend()) { + return *(hit->second); + } + + const auto *match{ + sourcemeta::blaze::metaschema_try_embedded(root, key, resolver)}; + if (match) { + this->probed_metaschemas_.emplace(key, match); + return *match; + } + + return resolver(identifier); + }}; std::vector subschema_entries; std::unordered_map @@ -588,8 +618,8 @@ auto SchemaFrame::analyse(const sourcemeta::core::JSON &root, const auto &schema{sourcemeta::core::get(root, path)}; - const auto root_base_dialect{ - sourcemeta::blaze::base_dialect(schema, resolver, default_dialect)}; + const auto root_base_dialect{sourcemeta::blaze::base_dialect( + schema, effective_resolver, default_dialect)}; if (!root_base_dialect.has_value()) { throw SchemaUnknownBaseDialectError(); } @@ -637,7 +667,7 @@ auto SchemaFrame::analyse(const sourcemeta::core::JSON &root, std::vector current_subschema_entries; for (const auto &relative_entry : sourcemeta::blaze::SchemaIterator{ - schema, walker, resolver, default_dialect}) { + schema, walker, effective_resolver, default_dialect}) { // Rephrase the iterator entry as being for the current base auto entry{relative_entry}; entry.pointer = path.concat(relative_entry.pointer); @@ -1283,8 +1313,25 @@ auto SchemaFrame::root() const noexcept auto SchemaFrame::vocabularies(const Location &location, const SchemaResolver &resolver) const -> Vocabularies { - return sourcemeta::blaze::vocabularies(resolver, location.base_dialect, - location.dialect); + if (this->probed_metaschemas_.empty()) { + return sourcemeta::blaze::vocabularies(resolver, location.base_dialect, + location.dialect); + } + + // Meta-schemas embedded in the analysed document take precedence + // over what the caller's resolver knows about + return sourcemeta::blaze::vocabularies( + [this, &resolver](const std::string_view identifier) + -> std::optional { + const auto hit{this->probed_metaschemas_.find( + sourcemeta::core::JSON::String{identifier})}; + if (hit != this->probed_metaschemas_.cend()) { + return *(hit->second); + } + + return resolver(identifier); + }, + location.base_dialect, location.dialect); } auto SchemaFrame::uri( @@ -1535,6 +1582,7 @@ auto SchemaFrame::reset() -> void { this->root_.clear(); this->locations_.clear(); this->references_.clear(); + this->probed_metaschemas_.clear(); this->standalone_ = false; } diff --git a/vendor/blaze/src/frame/include/sourcemeta/blaze/frame.h b/vendor/blaze/src/frame/include/sourcemeta/blaze/frame.h index 7481f64db..d78a02e24 100644 --- a/vendor/blaze/src/frame/include/sourcemeta/blaze/frame.h +++ b/vendor/blaze/src/frame/include/sourcemeta/blaze/frame.h @@ -298,6 +298,12 @@ class SOURCEMETA_BLAZE_FRAME_EXPORT SchemaFrame { sourcemeta::core::JSON::String root_; Locations locations_; References references_; + // Custom meta-schemas that the resolver could not resolve but that were + // found embedded in the analysed document itself. The values point into + // the analysed document, which the frame must not outlive anyway + std::unordered_map + probed_metaschemas_; mutable std::unordered_map< std::reference_wrapper, std::vector, sourcemeta::core::WeakPointer::Hasher, diff --git a/vendor/core/CMakeLists.txt b/vendor/core/CMakeLists.txt index d92fbad67..158c5bcfe 100644 --- a/vendor/core/CMakeLists.txt +++ b/vendor/core/CMakeLists.txt @@ -31,12 +31,15 @@ option(SOURCEMETA_CORE_YAML "Build the Sourcemeta Core YAML library" ON) option(SOURCEMETA_CORE_JSONRPC "Build the Sourcemeta Core JSON-RPC library" ON) option(SOURCEMETA_CORE_MCP "Build the Sourcemeta Core MCP library" ON) option(SOURCEMETA_CORE_HTTP "Build the Sourcemeta Core HTTP library" ON) +option(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL "Use system cURL for the Sourcemeta Core HTTP library" OFF) +option(SOURCEMETA_CORE_JOSE "Build the Sourcemeta Core JOSE library" ON) option(SOURCEMETA_CORE_SEMVER "Build the Sourcemeta Core SemVer library" ON) option(SOURCEMETA_CORE_GZIP "Build the Sourcemeta Core GZIP library" ON) option(SOURCEMETA_CORE_HTML "Build the Sourcemeta Core HTML library" ON) option(SOURCEMETA_CORE_CSS "Build the Sourcemeta Core CSS library" ON) option(SOURCEMETA_CORE_MARKDOWN "Build the Sourcemeta Core Markdown library" ON) option(SOURCEMETA_CORE_TESTS "Build the Sourcemeta Core tests" OFF) +option(SOURCEMETA_CORE_TESTS_CI "Build the Sourcemeta Core CI tests" OFF) option(SOURCEMETA_CORE_BENCHMARK "Build the Sourcemeta Core benchmarks" OFF) option(SOURCEMETA_CORE_DOCS "Build the Sourcemeta Core docs" OFF) option(SOURCEMETA_CORE_INSTALL "Install the Sourcemeta Core library" ON) @@ -188,6 +191,10 @@ if(SOURCEMETA_CORE_HTTP) add_subdirectory(src/core/http) endif() +if(SOURCEMETA_CORE_JOSE) + add_subdirectory(src/core/jose) +endif() + if(SOURCEMETA_CORE_SEMVER) add_subdirectory(src/core/semver) endif() @@ -225,7 +232,7 @@ endif() # Testing -if(SOURCEMETA_CORE_CONTRIB_GOOGLETEST OR SOURCEMETA_CORE_TESTS) +if(SOURCEMETA_CORE_CONTRIB_GOOGLETEST OR SOURCEMETA_CORE_TESTS OR SOURCEMETA_CORE_TESTS_CI) find_package(GoogleTest REQUIRED) endif() @@ -346,6 +353,10 @@ if(SOURCEMETA_CORE_TESTS) add_subdirectory(test/http) endif() + if(SOURCEMETA_CORE_JOSE) + add_subdirectory(test/jose) + endif() + if(SOURCEMETA_CORE_SEMVER) add_subdirectory(test/semver) endif() @@ -371,6 +382,14 @@ if(SOURCEMETA_CORE_TESTS) endif() endif() +if(SOURCEMETA_CORE_TESTS_CI) + enable_testing() + + if(SOURCEMETA_CORE_HTTP) + add_subdirectory(test/http/ci) + endif() +endif() + if(SOURCEMETA_CORE_BENCHMARK) add_subdirectory(benchmark) endif() diff --git a/vendor/core/DEPENDENCIES b/vendor/core/DEPENDENCIES index 73fe46d9a..fb683d434 100644 --- a/vendor/core/DEPENDENCIES +++ b/vendor/core/DEPENDENCIES @@ -3,9 +3,11 @@ jsontestsuite https://github.com/nst/JSONTestSuite d64aefb55228d9584d3e5b2433f72 yaml-test-suite https://github.com/yaml/yaml-test-suite data-2022-01-17 cmark-gfm https://github.com/github/cmark-gfm 587a12bb54d95ac37241377e6ddc93ea0e45439b uritemplate-test https://github.com/uri-templates/uritemplate-test 1eb27ab4462b9e5819dc47db99044f5fd1fa9bc7 -pyca-cryptography https://github.com/pyca/cryptography c4935a7021af37c38e0684b0546c1b4378518342 +pyca-cryptography https://github.com/pyca/cryptography 9747d06e83764e7f1ea4c04daf134cb8f861700b +wycheproof https://github.com/C2SP/wycheproof 6d7cccd0fcb1917368579adeeac10fe802f1b521 pcre2 https://github.com/PCRE2Project/pcre2 pcre2-10.47 googletest https://github.com/google/googletest a7f443b80b105f940225332ed3c31f2790092f47 googlebenchmark https://github.com/google/benchmark 378fe693a1ef51500db21b11ff05a8018c5f0e55 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 diff --git a/vendor/core/cmake/common/compiler/options.cmake b/vendor/core/cmake/common/compiler/options.cmake index 150799252..20c05eaa9 100644 --- a/vendor/core/cmake/common/compiler/options.cmake +++ b/vendor/core/cmake/common/compiler/options.cmake @@ -116,6 +116,10 @@ function(sourcemeta_add_default_options visibility target) -Wno-exit-time-destructors -Wrange-loop-analysis + # Manage Objective-C and Objective-C++ object lifetimes with Automatic + # Reference Counting + $<$,$>:-fobjc-arc> + # Enable loop vectorization for performance reasons $<$>:-fvectorize> # Enable vectorization of straight-line code for performance diff --git a/vendor/core/cmake/common/variables.cmake b/vendor/core/cmake/common/variables.cmake index ee6359cb0..c7d6adbb0 100644 --- a/vendor/core/cmake/common/variables.cmake +++ b/vendor/core/cmake/common/variables.cmake @@ -1,3 +1,10 @@ +# Objective-C++ powers the Apple-specific backends and must be enabled before +# we capture the project languages below, so its standard and visibility +# defaults get applied +if(APPLE) + enable_language(OBJCXX) +endif() + # Get the list of languages defined in the project get_property(SOURCEMETA_LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES) diff --git a/vendor/core/config.cmake.in b/vendor/core/config.cmake.in index 45c1fddf4..16d87fed7 100644 --- a/vendor/core/config.cmake.in +++ b/vendor/core/config.cmake.in @@ -27,6 +27,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonrpc) list(APPEND SOURCEMETA_CORE_COMPONENTS mcp) list(APPEND SOURCEMETA_CORE_COMPONENTS http) + list(APPEND SOURCEMETA_CORE_COMPONENTS jose) list(APPEND SOURCEMETA_CORE_COMPONENTS semver) list(APPEND SOURCEMETA_CORE_COMPONENTS gzip) list(APPEND SOURCEMETA_CORE_COMPONENTS html) @@ -157,6 +158,9 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonrpc.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_mcp.cmake") elseif(component STREQUAL "http") + if(@SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL@) + find_dependency(CURL) + endif() include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") @@ -165,6 +169,19 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_time.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_http.cmake") + elseif(component STREQUAL "jose") + if(@SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL@) + find_dependency(OpenSSL 3.0) + endif() + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_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_time.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_crypto.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jose.cmake") elseif(component STREQUAL "semver") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_semver.cmake") diff --git a/vendor/core/src/core/crypto/CMakeLists.txt b/vendor/core/src/core/crypto/CMakeLists.txt index 17c5104a2..679bfd55f 100644 --- a/vendor/core/src/core/crypto/CMakeLists.txt +++ b/vendor/core/src/core/crypto/CMakeLists.txt @@ -12,30 +12,127 @@ 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_sha512_openssl.cc crypto_random_openssl.cc - crypto_verify_rsa_openssl.cc crypto_verify_ecdsa_openssl.cc) + crypto_verify_openssl.cc) target_link_libraries(sourcemeta_core_crypto PRIVATE OpenSSL::Crypto) elseif(APPLE) + enable_language(OBJCXX) + + # Ed25519 is verified through CryptoKit (Swift), reached from Objective-C++. + # The Swift shim is compiled to an object and its generated Objective-C + # interface header out of band, since the rest of the library is C++ + set(CRYPTOKIT_SWIFT "${CMAKE_CURRENT_SOURCE_DIR}/crypto_eddsa_cryptokit.swift") + set(CRYPTOKIT_HEADER + "${CMAKE_CURRENT_BINARY_DIR}/sourcemeta_core_cryptokit-Swift.h") + if(CMAKE_OSX_SYSROOT) + set(CRYPTOKIT_SDK "${CMAKE_OSX_SYSROOT}") + else() + execute_process(COMMAND xcrun --show-sdk-path + OUTPUT_VARIABLE CRYPTOKIT_SDK OUTPUT_STRIP_TRAILING_WHITESPACE) + endif() + + # The toolchain directory holding the Swift runtime back-deployment archives + # that the shim autolinks for older deployment targets + execute_process(COMMAND xcrun --find swiftc + OUTPUT_VARIABLE CRYPTOKIT_SWIFTC OUTPUT_STRIP_TRAILING_WHITESPACE) + get_filename_component(CRYPTOKIT_TOOLCHAIN_BIN "${CRYPTOKIT_SWIFTC}" DIRECTORY) + get_filename_component(CRYPTOKIT_RUNTIME_LIB + "${CRYPTOKIT_TOOLCHAIN_BIN}/../lib/swift/macosx" ABSOLUTE) + if(CMAKE_OSX_DEPLOYMENT_TARGET) + set(CRYPTOKIT_DEPLOYMENT "${CMAKE_OSX_DEPLOYMENT_TARGET}") + else() + # CryptoKit signing over Curve25519 is available since macOS 10.15 + set(CRYPTOKIT_DEPLOYMENT "10.15") + endif() + if(CMAKE_OSX_ARCHITECTURES) + set(CRYPTOKIT_ARCHITECTURES ${CMAKE_OSX_ARCHITECTURES}) + else() + set(CRYPTOKIT_ARCHITECTURES "${CMAKE_SYSTEM_PROCESSOR}") + endif() + + # One object per architecture, emitting the architecture independent header + # only on the first, combined afterwards into a single object + set(CRYPTOKIT_OBJECTS) + set(CRYPTOKIT_HEADER_OUTPUT "${CRYPTOKIT_HEADER}") + set(CRYPTOKIT_EMIT_HEADER + -emit-objc-header -emit-objc-header-path "${CRYPTOKIT_HEADER}") + foreach(architecture IN LISTS CRYPTOKIT_ARCHITECTURES) + set(CRYPTOKIT_ARCH_OBJECT + "${CMAKE_CURRENT_BINARY_DIR}/crypto_eddsa_cryptokit_${architecture}.o") + add_custom_command( + OUTPUT "${CRYPTOKIT_ARCH_OBJECT}" ${CRYPTOKIT_HEADER_OUTPUT} + COMMAND xcrun swiftc + -sdk "${CRYPTOKIT_SDK}" + -target "${architecture}-apple-macosx${CRYPTOKIT_DEPLOYMENT}" + -module-name sourcemeta_core_cryptokit + -parse-as-library -O + ${CRYPTOKIT_EMIT_HEADER} + -emit-object -o "${CRYPTOKIT_ARCH_OBJECT}" + "${CRYPTOKIT_SWIFT}" + DEPENDS "${CRYPTOKIT_SWIFT}" + COMMENT "Building CryptoKit Swift shim (${architecture})" + VERBATIM) + list(APPEND CRYPTOKIT_OBJECTS "${CRYPTOKIT_ARCH_OBJECT}") + set(CRYPTOKIT_HEADER_OUTPUT) + set(CRYPTOKIT_EMIT_HEADER) + endforeach() + + list(LENGTH CRYPTOKIT_OBJECTS CRYPTOKIT_OBJECT_COUNT) + if(CRYPTOKIT_OBJECT_COUNT GREATER 1) + set(CRYPTOKIT_OBJECT "${CMAKE_CURRENT_BINARY_DIR}/crypto_eddsa_cryptokit.o") + add_custom_command( + OUTPUT "${CRYPTOKIT_OBJECT}" + COMMAND lipo -create ${CRYPTOKIT_OBJECTS} -output "${CRYPTOKIT_OBJECT}" + DEPENDS ${CRYPTOKIT_OBJECTS} + COMMENT "Combining CryptoKit Swift shim architectures" + VERBATIM) + else() + set(CRYPTOKIT_OBJECT "${CRYPTOKIT_OBJECTS}") + endif() + + set_source_files_properties("${CRYPTOKIT_OBJECT}" + PROPERTIES EXTERNAL_OBJECT TRUE GENERATED TRUE) + set_source_files_properties("${CRYPTOKIT_HEADER}" PROPERTIES GENERATED TRUE) + set_source_files_properties(crypto_eddsa_cryptokit.mm + PROPERTIES OBJECT_DEPENDS "${CRYPTOKIT_HEADER}") + target_sources(sourcemeta_core_crypto PRIVATE crypto_sha1_apple.cc crypto_sha256_apple.cc crypto_sha384_apple.cc crypto_sha512_apple.cc crypto_random_apple.cc - crypto_verify_rsa_apple.cc crypto_verify_ecdsa_apple.cc) + crypto_verify_apple.cc + crypto_eddsa_cryptokit.mm "${CRYPTOKIT_OBJECT}" + crypto_eddsa.h crypto_eddsa_apple.h crypto_bignum.h crypto_shake256.h) + + # The generated Objective-C interface header lives in the build tree + target_include_directories(sourcemeta_core_crypto + PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") + target_link_libraries(sourcemeta_core_crypto PRIVATE "-framework Security") target_link_libraries(sourcemeta_core_crypto PRIVATE "-framework CoreFoundation") + target_link_libraries(sourcemeta_core_crypto PRIVATE "-framework CryptoKit") + target_link_libraries(sourcemeta_core_crypto PRIVATE "-framework Foundation") + + # Resolve the Swift runtime that the shim autolinks, both at link and at load. + # PUBLIC rather than INTERFACE so that a shared build of this library, which + # has its own link step pulling in the Swift object, also gets the flags + target_link_options(sourcemeta_core_crypto PUBLIC + "SHELL:-L ${CRYPTOKIT_SDK}/usr/lib/swift" + "SHELL:-L ${CRYPTOKIT_RUNTIME_LIB}" + "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_sha512_windows.cc crypto_random_windows.cc - crypto_verify_rsa_windows.cc crypto_verify_ecdsa_windows.cc) + crypto_verify_windows.cc crypto_eddsa.h + crypto_bignum.h crypto_shake256.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_sha512_other.cc crypto_random_other.cc - crypto_verify_rsa_other.cc crypto_verify_ecdsa_other.cc - crypto_bignum.h crypto_ecc.h) + crypto_sha512_other.cc crypto_random_other.cc crypto_verify_other.cc + crypto_bignum.h crypto_ecc.h crypto_eddsa.h crypto_shake256.h) endif() if(SOURCEMETA_CORE_INSTALL) diff --git a/vendor/core/src/core/crypto/crypto_bignum.h b/vendor/core/src/core/crypto/crypto_bignum.h index cd025381e..5244bc253 100644 --- a/vendor/core/src/core/crypto/crypto_bignum.h +++ b/vendor/core/src/core/crypto/crypto_bignum.h @@ -3,8 +3,8 @@ // Fixed-capacity unsigned big integer arithmetic for the reference // signature verification backend. Capacity fits 4096-bit RSA operands -// and their double-width products. Performance and constant-time -// execution are non-goals, verification consumes only public inputs +// and their double-width products. Constant-time execution is not +// required, since verification consumes only public inputs #include @@ -61,7 +61,7 @@ inline auto bignum_from_u64(const std::uint64_t value) noexcept -> Bignum { return result; } -inline auto bignum_from_hex(const std::string_view hex) 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'); @@ -127,7 +127,12 @@ inline auto bignum_bit_length(const Bignum &value) noexcept -> std::size_t { inline auto bignum_get_bit(const Bignum &value, const std::size_t bit) noexcept -> bool { - return ((value.words[bit / 64] >> (bit % 64)) & 1u) != 0; + const auto word{bit / 64}; + if (word >= value.size) { + return false; + } + + return ((value.words[word] >> (bit % 64)) & 1u) != 0; } // Assumes the result fits in the capacity @@ -173,20 +178,118 @@ inline auto bignum_subtract_in_place(Bignum &left, const Bignum &right) noexcept bignum_normalize(left); } +inline auto bignum_shift_right(const Bignum &value, + const std::size_t bits) noexcept -> Bignum; + +// Reduce a value modulo the modulus with Knuth's Algorithm D (TAOCP Volume 2, +// Section 4.3.1), the schoolbook long division that estimates one quotient +// word per step rather than one bit, so the cost is quadratic in the number of +// words rather than the number of bits inline auto bignum_reduce(Bignum &value, const Bignum &modulus) noexcept -> void { - const auto modulus_bits{bignum_bit_length(modulus)}; - while (bignum_compare(value, modulus) >= 0) { - const auto value_bits{bignum_bit_length(value)}; - auto shift{value_bits - modulus_bits}; - auto shifted{bignum_shift_left(modulus, shift)}; - if (bignum_compare(shifted, value) > 0) { - shift -= 1; - shifted = bignum_shift_left(modulus, shift); + if (bignum_compare(value, modulus) < 0) { + return; + } + + const auto divisor_words{modulus.size}; + + // A single-word divisor folds the value down word by word + if (divisor_words == 1) { + const auto divisor{modulus.words[0]}; + BignumDoubleWord remainder{0}; + for (std::size_t index = value.size; index > 0; --index) { + remainder = (remainder << 64u) | value.words[index - 1]; + remainder %= divisor; } - bignum_subtract_in_place(value, shifted); + value = bignum_from_u64(static_cast(remainder)); + return; } + + // Normalize so the divisor's top word has its high bit set, which bounds the + // error of each quotient word estimate to at most two + const auto shift{static_cast( + (64u - (bignum_bit_length(modulus) % 64u)) % 64u)}; + const auto divisor{shift > 0 ? bignum_shift_left(modulus, shift) : modulus}; + auto dividend{shift > 0 ? bignum_shift_left(value, shift) : value}; + const auto dividend_words{dividend.size}; + const auto quotient_words{dividend_words - divisor_words}; + const auto top{divisor.words[divisor_words - 1]}; + const auto next{divisor.words[divisor_words - 2]}; + const BignumDoubleWord base{static_cast(1) << 64u}; + + for (std::size_t step = quotient_words + 1; step > 0; --step) { + const auto offset{step - 1}; + + // Estimate the quotient word from the top two words of the running value + const auto numerator{ + (static_cast(dividend.words[offset + divisor_words]) + << 64u) | + dividend.words[offset + divisor_words - 1]}; + auto estimate{numerator / top}; + auto estimate_remainder{numerator % top}; + while (estimate >= base || + estimate * next > (estimate_remainder << 64u) + + dividend.words[offset + divisor_words - 2]) { + estimate -= 1; + estimate_remainder += top; + if (estimate_remainder >= base) { + break; + } + } + + // Multiply the divisor by the estimate and subtract from the running value + const auto quotient_word{static_cast(estimate)}; + BignumDoubleWord carry{0}; + std::uint64_t borrow{0}; + for (std::size_t index = 0; index < divisor_words; ++index) { + const auto product{static_cast(quotient_word) * + divisor.words[index] + + carry}; + carry = product >> 64u; + const auto subtrahend{static_cast(product)}; + const auto current{dividend.words[offset + index]}; + const auto without_subtrahend{current - subtrahend}; + auto next_borrow{current < subtrahend ? 1u : 0u}; + const auto result_word{without_subtrahend - borrow}; + if (without_subtrahend < borrow) { + next_borrow += 1u; + } + + dividend.words[offset + index] = result_word; + borrow = next_borrow; + } + + const auto current{dividend.words[offset + divisor_words]}; + const auto subtrahend{static_cast(carry)}; + const auto without_subtrahend{current - subtrahend}; + auto next_borrow{current < subtrahend ? 1u : 0u}; + dividend.words[offset + divisor_words] = without_subtrahend - borrow; + if (without_subtrahend < borrow) { + next_borrow += 1u; + } + + // The estimate was at most one too large, so add the divisor back when the + // subtraction borrowed past the top + if (next_borrow != 0) { + BignumDoubleWord add_carry{0}; + for (std::size_t index = 0; index < divisor_words; ++index) { + const auto sum{ + static_cast(dividend.words[offset + index]) + + divisor.words[index] + add_carry}; + dividend.words[offset + index] = static_cast(sum); + add_carry = sum >> 64u; + } + + dividend.words[offset + divisor_words] += + static_cast(add_carry); + } + } + + // The remainder occupies the low words, still scaled by the normalization + dividend.size = divisor_words; + bignum_normalize(dividend); + value = shift > 0 ? bignum_shift_right(dividend, shift) : dividend; } // Assumes both operands fit in half the capacity @@ -324,13 +427,62 @@ inline auto bignum_mod_multiply(const Bignum &left, const Bignum &right, return result; } -// The modulus must be prime, so that Fermat's little theorem gives the -// inverse as the modulus-minus-two power +// Halve a value modulo an odd modulus: an even value shifts down, an odd one +// becomes even by adding the modulus first, so the result stays an integer +inline auto bignum_mod_halve(const Bignum &value, + const Bignum &modulus) noexcept -> Bignum { + if ((value.words[0] & 1u) == 0) { + return bignum_shift_right(value, 1); + } + + return bignum_shift_right(bignum_add(value, modulus), 1); +} + +// Modular inverse by the binary extended Euclidean algorithm, which needs only +// halving, subtraction, and comparison rather than the modular exponentiation +// a Fermat inverse over a prime modulus would spend. The modulus must be odd. +// Returns zero when the value has no inverse, which is when it shares a factor +// with the modulus or reduces to zero inline auto bignum_mod_inverse(const Bignum &value, const Bignum &modulus) noexcept -> Bignum { - auto exponent{modulus}; - bignum_subtract_in_place(exponent, bignum_from_u64(2)); - return bignum_mod_exp(value, exponent, modulus); + const auto one{bignum_from_u64(1)}; + auto first{value}; + bignum_reduce(first, modulus); + auto second{modulus}; + auto first_coefficient{one}; + Bignum second_coefficient; + + while (bignum_compare(first, one) != 0 && bignum_compare(second, one) != 0) { + // A side reaching zero means the greatest common divisor exceeds one, so no + // inverse exists. Stopping here also keeps the halving below from spinning + // forever on a zero value + if (bignum_is_zero(first) || bignum_is_zero(second)) { + return {}; + } + + while ((first.words[0] & 1u) == 0) { + first = bignum_shift_right(first, 1); + first_coefficient = bignum_mod_halve(first_coefficient, modulus); + } + + while ((second.words[0] & 1u) == 0) { + second = bignum_shift_right(second, 1); + second_coefficient = bignum_mod_halve(second_coefficient, modulus); + } + + if (bignum_compare(first, second) >= 0) { + bignum_subtract_in_place(first, second); + first_coefficient = + bignum_mod_subtract(first_coefficient, second_coefficient, modulus); + } else { + bignum_subtract_in_place(second, first); + second_coefficient = + bignum_mod_subtract(second_coefficient, first_coefficient, modulus); + } + } + + return bignum_compare(first, one) == 0 ? first_coefficient + : second_coefficient; } inline auto bignum_to_bytes(const Bignum &value, const std::size_t length) diff --git a/vendor/core/src/core/crypto/crypto_ecc.h b/vendor/core/src/core/crypto/crypto_ecc.h index 04f53d985..2a4de5e0d 100644 --- a/vendor/core/src/core/crypto/crypto_ecc.h +++ b/vendor/core/src/core/crypto/crypto_ecc.h @@ -4,16 +4,22 @@ // 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. Performance and constant -// time execution are non-goals, verification consumes only public inputs +// inversion at the end rather than one per step. Constant time execution is +// not required, since verification consumes only public inputs #include "crypto_bignum.h" +#include // std::array #include // std::size_t +#include // std::uint8_t, std::uint64_t #include // std::string_view namespace sourcemeta::core { +// Identifies the field prime so that modular reduction can take the fast +// generalized Mersenne path specific to each NIST curve +enum class NISTPrime : std::uint8_t { P256, P384, P521 }; + struct EllipticCurveParameters { Bignum prime; Bignum coefficient_a; @@ -22,6 +28,7 @@ struct EllipticCurveParameters { Bignum generator_y; Bignum order; std::size_t field_bytes; + NISTPrime reduction; }; // A point in Jacobian coordinates, where the affine point is @@ -34,19 +41,26 @@ struct JacobianPoint { // FIPS 186-4 Appendix D.1.2 curve domain parameters inline auto curve_p256() -> EllipticCurveParameters { - return {bignum_from_hex("ffffffff00000001000000000000000000000000ffffffff" - "ffffffffffffffff"), - bignum_from_hex("ffffffff00000001000000000000000000000000ffffffff" - "fffffffffffffffc"), - bignum_from_hex("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f6" - "3bce3c3e27d2604b"), - bignum_from_hex("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0" - "f4a13945d898c296"), - bignum_from_hex("4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ece" - "cbb6406837bf51f5"), - bignum_from_hex("ffffffff00000000ffffffffffffffffbce6faada7179e84" - "f3b9cac2fc632551"), - 32}; + return {.prime = + bignum_from_hex("ffffffff00000001000000000000000000000000ffffffff" + "ffffffffffffffff"), + .coefficient_a = + bignum_from_hex("ffffffff00000001000000000000000000000000ffffffff" + "fffffffffffffffc"), + .coefficient_b = + bignum_from_hex("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f6" + "3bce3c3e27d2604b"), + .generator_x = + bignum_from_hex("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0" + "f4a13945d898c296"), + .generator_y = + bignum_from_hex("4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ece" + "cbb6406837bf51f5"), + .order = + bignum_from_hex("ffffffff00000000ffffffffffffffffbce6faada7179e84" + "f3b9cac2fc632551"), + .field_bytes = 32, + .reduction = NISTPrime::P256}; } inline auto curve_p384() -> EllipticCurveParameters { @@ -54,29 +68,233 @@ inline auto curve_p384() -> EllipticCurveParameters { // is ever lost across a line break // clang-format off return { - bignum_from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff"), - bignum_from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc"), - bignum_from_hex("b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef"), - bignum_from_hex("aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7"), - bignum_from_hex("3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f"), - bignum_from_hex("ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973"), - 48}; + .prime = bignum_from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff"), + .coefficient_a = bignum_from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc"), + .coefficient_b = bignum_from_hex("b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef"), + .generator_x = bignum_from_hex("aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7"), + .generator_y = bignum_from_hex("3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f"), + .order = bignum_from_hex("ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973"), + .field_bytes = 48, + .reduction = NISTPrime::P384}; // clang-format on } inline auto curve_p521() -> EllipticCurveParameters { // clang-format off return { - bignum_from_hex("01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - bignum_from_hex("01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc"), - bignum_from_hex("0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00"), - bignum_from_hex("00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66"), - bignum_from_hex("011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650"), - bignum_from_hex("01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409"), - 66}; + .prime = bignum_from_hex("01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + .coefficient_a = bignum_from_hex("01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc"), + .coefficient_b = bignum_from_hex("0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00"), + .generator_x = bignum_from_hex("00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66"), + .generator_y = bignum_from_hex("011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650"), + .order = bignum_from_hex("01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409"), + .field_bytes = 66, + .reduction = NISTPrime::P521}; // clang-format on } +// NIST P-521 field reduction. The prime is 2^521 - 1, so 2^521 is congruent +// to 1 modulo it, and a value below the square of the prime folds the bits +// above position 521 back into the low 521 bits with a single addition +inline auto field_reduce_p521(Bignum &value, const Bignum &prime) noexcept + -> void { + auto high{bignum_shift_right(value, 521)}; + if (value.size > 8) { + value.words[8] &= 0x1ffULL; + for (std::size_t index = 9; index < value.size; ++index) { + value.words[index] = 0; + } + + value.size = 9; + bignum_normalize(value); + } + + value = bignum_add(value, high); + while (bignum_compare(value, prime) >= 0) { + bignum_subtract_in_place(value, prime); + } +} + +// Read the 32-bit limb at the given position, counting from the least +// significant, returning zero past the end of the value +inline auto field_word(const Bignum &value, const std::size_t index) noexcept + -> std::uint64_t { + const auto word{index / 2}; + if (word >= value.size) { + return 0; + } + + return (value.words[word] >> (32 * (index % 2))) & 0xffffffffULL; +} + +// Add a reduction term, given as its 32-bit limbs most significant first, +// into the column accumulator scaled by the multiplier. Column zero is the +// least significant, and accumulating into 64-bit columns rather than +// materializing a value per term keeps the reduction free of temporaries +template +inline auto field_accumulate(std::array &columns, + const std::array &limbs, + const std::uint64_t multiplier) noexcept -> void { + for (std::size_t index = 0; index < Count; ++index) { + columns[Count - 1 - index] += multiplier * limbs[index]; + } +} + +// Carry propagate the 32-bit columns and pack them into a value +template +inline auto field_from_columns(std::array &columns) + -> Bignum { + std::uint64_t carry{0}; + Bignum result; + for (std::size_t index = 0; index <= Count; ++index) { + const auto current{columns[index] + carry}; + result.words[index / 2] |= (current & 0xffffffffULL) << (32 * (index % 2)); + carry = current >> 32; + } + + result.size = (Count + 2) / 2; + bignum_normalize(result); + return result; +} + +// Combine the positive and negative column sums of a generalized Mersenne +// reduction into the single reduced value below the prime +inline auto field_combine(Bignum &positive, const Bignum &negative, + const Bignum &prime) noexcept -> void { + while (bignum_compare(positive, negative) < 0) { + positive = bignum_add(positive, prime); + } + + bignum_subtract_in_place(positive, negative); + while (bignum_compare(positive, prime) >= 0) { + bignum_subtract_in_place(positive, prime); + } +} + +// NIST P-256 field reduction. The prime is 2^256 - 2^224 + 2^192 + 2^96 - 1, +// a generalized Mersenne prime whose reduction recombines the 32-bit limbs of +// the product into a small signed sum of nine field-width terms +// (FIPS 186-4 Appendix D.2.3) +inline auto field_reduce_p256(Bignum &value, const Bignum &prime) noexcept + -> void { + std::array c{}; + for (std::size_t index = 0; index < 16; ++index) { + c[index] = field_word(value, index); + } + + std::array positive_columns{}; + std::array negative_columns{}; + field_accumulate<8>(positive_columns, + {{c[7], c[6], c[5], c[4], c[3], c[2], c[1], c[0]}}, 1); + field_accumulate<8>(positive_columns, + {{c[15], c[14], c[13], c[12], c[11], 0, 0, 0}}, 2); + field_accumulate<8>(positive_columns, + {{0, c[15], c[14], c[13], c[12], 0, 0, 0}}, 2); + field_accumulate<8>(positive_columns, + {{c[15], c[14], 0, 0, 0, c[10], c[9], c[8]}}, 1); + field_accumulate<8>(positive_columns, + {{c[8], c[13], c[15], c[14], c[13], c[11], c[10], c[9]}}, + 1); + field_accumulate<8>(negative_columns, + {{c[10], c[8], 0, 0, 0, c[13], c[12], c[11]}}, 1); + field_accumulate<8>(negative_columns, + {{c[11], c[9], 0, 0, c[15], c[14], c[13], c[12]}}, 1); + field_accumulate<8>(negative_columns, + {{c[12], 0, c[10], c[9], c[8], c[15], c[14], c[13]}}, 1); + field_accumulate<8>(negative_columns, + {{c[13], 0, c[11], c[10], c[9], 0, c[15], c[14]}}, 1); + + auto positive{field_from_columns<8>(positive_columns)}; + const auto negative{field_from_columns<8>(negative_columns)}; + field_combine(positive, negative, prime); + value = positive; +} + +// NIST P-384 field reduction. The prime is 2^384 - 2^128 - 2^96 + 2^32 - 1, +// a generalized Mersenne prime whose reduction recombines the 32-bit limbs of +// the product into a small signed sum of ten field-width terms +// (FIPS 186-4 Appendix D.2.4) +inline auto field_reduce_p384(Bignum &value, const Bignum &prime) noexcept + -> void { + std::array c{}; + for (std::size_t index = 0; index < 24; ++index) { + c[index] = field_word(value, index); + } + + std::array positive_columns{}; + std::array negative_columns{}; + field_accumulate<12>(positive_columns, + {{c[11], c[10], c[9], c[8], c[7], c[6], c[5], c[4], c[3], + c[2], c[1], c[0]}}, + 1); + field_accumulate<12>(positive_columns, + {{0, 0, 0, 0, 0, c[23], c[22], c[21], 0, 0, 0, 0}}, 2); + field_accumulate<12>(positive_columns, + {{c[23], c[22], c[21], c[20], c[19], c[18], c[17], c[16], + c[15], c[14], c[13], c[12]}}, + 1); + field_accumulate<12>(positive_columns, + {{c[20], c[19], c[18], c[17], c[16], c[15], c[14], c[13], + c[12], c[23], c[22], c[21]}}, + 1); + field_accumulate<12>(positive_columns, + {{c[19], c[18], c[17], c[16], c[15], c[14], c[13], c[12], + c[20], 0, c[23], 0}}, + 1); + field_accumulate<12>(positive_columns, + {{0, 0, 0, 0, c[23], c[22], c[21], c[20], 0, 0, 0, 0}}, + 1); + field_accumulate<12>(positive_columns, + {{0, 0, 0, 0, 0, 0, c[23], c[22], c[21], 0, 0, c[20]}}, + 1); + field_accumulate<12>(negative_columns, + {{c[22], c[21], c[20], c[19], c[18], c[17], c[16], c[15], + c[14], c[13], c[12], c[23]}}, + 1); + field_accumulate<12>(negative_columns, + {{0, 0, 0, 0, 0, 0, 0, c[23], c[22], c[21], c[20], 0}}, + 1); + field_accumulate<12>(negative_columns, + {{0, 0, 0, 0, 0, 0, 0, c[23], c[23], 0, 0, 0}}, 1); + + auto positive{field_from_columns<12>(positive_columns)}; + const auto negative{field_from_columns<12>(negative_columns)}; + field_combine(positive, negative, prime); + value = positive; +} + +// Reduce a product below the square of the field prime, taking the fast +// generalized Mersenne path for the curve rather than long division +inline auto field_reduce(Bignum &value, + const EllipticCurveParameters &curve) noexcept + -> void { + switch (curve.reduction) { + case NISTPrime::P521: + field_reduce_p521(value, curve.prime); + return; + case NISTPrime::P256: + field_reduce_p256(value, curve.prime); + return; + case NISTPrime::P384: + field_reduce_p384(value, curve.prime); + return; + } +} + +inline auto field_mod_multiply(const Bignum &left, const Bignum &right, + const EllipticCurveParameters &curve) noexcept + -> Bignum { + auto result{bignum_multiply(left, right)}; + field_reduce(result, curve); + return result; +} + +inline auto field_square(const Bignum &value, + const EllipticCurveParameters &curve) noexcept + -> Bignum { + return field_mod_multiply(value, value, curve); +} + inline auto point_is_infinity(const JacobianPoint &point) noexcept -> bool { return bignum_is_zero(point.z); } @@ -87,32 +305,43 @@ inline auto point_double(const JacobianPoint &point, const EllipticCurveParameters &curve) -> JacobianPoint { if (point_is_infinity(point) || bignum_is_zero(point.y)) { - return {Bignum{}, Bignum{}, Bignum{}}; + return {}; } + // The doubling formula for curves with coefficient -3 (EFD dbl-2001-b), + // which trades the coefficient multiplication and a squaring for one more + // subtraction, and computes every small multiple as a chain of modular + // additions so that no division-based reduction is spent on a constant const auto &prime{curve.prime}; - const auto two{bignum_from_u64(2)}; - const auto three{bignum_from_u64(3)}; - const auto y_squared{bignum_mod_multiply(point.y, point.y, prime)}; - auto subterm{bignum_mod_multiply(point.x, y_squared, prime)}; - subterm = bignum_mod_multiply(bignum_from_u64(4), subterm, prime); - const auto x_squared{bignum_mod_multiply(point.x, point.x, prime)}; - const auto z_squared{bignum_mod_multiply(point.z, point.z, prime)}; - const auto z_fourth{bignum_mod_multiply(z_squared, z_squared, prime)}; - const auto slope{bignum_mod_add( - bignum_mod_multiply(three, x_squared, prime), - bignum_mod_multiply(curve.coefficient_a, z_fourth, prime), prime)}; + const auto delta{field_square(point.z, curve)}; + const auto gamma{field_square(point.y, curve)}; + const auto beta{field_mod_multiply(point.x, gamma, curve)}; + const auto difference{ + field_mod_multiply(bignum_mod_subtract(point.x, delta, prime), + bignum_mod_add(point.x, delta, prime), curve)}; + const auto alpha{bignum_mod_add(bignum_mod_add(difference, difference, prime), + difference, prime)}; + const auto two_beta{bignum_mod_add(beta, beta, prime)}; + const auto four_beta{bignum_mod_add(two_beta, two_beta, prime)}; + const auto eight_beta{bignum_mod_add(four_beta, four_beta, prime)}; const auto result_x{ - bignum_mod_subtract(bignum_mod_multiply(slope, slope, prime), - bignum_mod_multiply(two, subterm, prime), prime)}; - const auto y_fourth{bignum_mod_multiply(y_squared, y_squared, prime)}; + bignum_mod_subtract(field_square(alpha, curve), eight_beta, prime)}; + const auto y_plus_z{bignum_mod_add(point.y, point.z, prime)}; + const auto result_z{bignum_mod_subtract( + bignum_mod_subtract(field_square(y_plus_z, curve), gamma, prime), delta, + prime)}; + const auto gamma_squared{field_square(gamma, curve)}; + const auto two_gamma_squared{ + bignum_mod_add(gamma_squared, gamma_squared, prime)}; + const auto four_gamma_squared{ + bignum_mod_add(two_gamma_squared, two_gamma_squared, prime)}; + const auto eight_gamma_squared{ + bignum_mod_add(four_gamma_squared, four_gamma_squared, prime)}; const auto result_y{bignum_mod_subtract( - bignum_mod_multiply(slope, bignum_mod_subtract(subterm, result_x, prime), - prime), - bignum_mod_multiply(bignum_from_u64(8), y_fourth, prime), prime)}; - const auto result_z{bignum_mod_multiply( - bignum_mod_multiply(two, point.y, prime), point.z, prime)}; - return {result_x, result_y, result_z}; + field_mod_multiply(alpha, bignum_mod_subtract(four_beta, result_x, prime), + curve), + eight_gamma_squared, prime)}; + return {.x = result_x, .y = result_y, .z = result_z}; } // Point addition in Jacobian coordinates @@ -127,19 +356,18 @@ inline auto point_add(const JacobianPoint &left, const JacobianPoint &right, } const auto &prime{curve.prime}; - const auto left_z_squared{bignum_mod_multiply(left.z, left.z, prime)}; - const auto right_z_squared{bignum_mod_multiply(right.z, right.z, prime)}; - const auto u1{bignum_mod_multiply(left.x, right_z_squared, prime)}; - const auto u2{bignum_mod_multiply(right.x, left_z_squared, prime)}; - const auto left_z_cubed{bignum_mod_multiply(left_z_squared, left.z, prime)}; - const auto right_z_cubed{ - bignum_mod_multiply(right_z_squared, right.z, prime)}; - const auto s1{bignum_mod_multiply(left.y, right_z_cubed, prime)}; - const auto s2{bignum_mod_multiply(right.y, left_z_cubed, prime)}; + const auto left_z_squared{field_mod_multiply(left.z, left.z, curve)}; + const auto right_z_squared{field_mod_multiply(right.z, right.z, curve)}; + const auto u1{field_mod_multiply(left.x, right_z_squared, curve)}; + const auto u2{field_mod_multiply(right.x, left_z_squared, curve)}; + const auto left_z_cubed{field_mod_multiply(left_z_squared, left.z, curve)}; + const auto right_z_cubed{field_mod_multiply(right_z_squared, right.z, curve)}; + const auto s1{field_mod_multiply(left.y, right_z_cubed, curve)}; + const auto s2{field_mod_multiply(right.y, left_z_cubed, curve)}; if (bignum_compare(u1, u2) == 0) { if (bignum_compare(s1, s2) != 0) { - return {Bignum{}, Bignum{}, Bignum{}}; + return {}; } return point_double(left, curve); @@ -147,31 +375,121 @@ inline auto point_add(const JacobianPoint &left, const JacobianPoint &right, const auto h{bignum_mod_subtract(u2, u1, prime)}; const auto r{bignum_mod_subtract(s2, s1, prime)}; - const auto h_squared{bignum_mod_multiply(h, h, prime)}; - const auto h_cubed{bignum_mod_multiply(h_squared, h, prime)}; - const auto u1_h_squared{bignum_mod_multiply(u1, h_squared, prime)}; + const auto h_squared{field_square(h, curve)}; + const auto h_cubed{field_mod_multiply(h_squared, h, curve)}; + const auto u1_h_squared{field_mod_multiply(u1, h_squared, curve)}; + const auto result_x{bignum_mod_subtract( + bignum_mod_subtract(field_square(r, curve), h_cubed, prime), + field_mod_multiply(bignum_from_u64(2), u1_h_squared, curve), prime)}; + const auto result_y{bignum_mod_subtract( + field_mod_multiply(r, bignum_mod_subtract(u1_h_squared, result_x, prime), + curve), + field_mod_multiply(s1, h_cubed, curve), prime)}; + const auto result_z{ + field_mod_multiply(field_mod_multiply(h, left.z, curve), right.z, curve)}; + return {.x = result_x, .y = result_y, .z = result_z}; +} + +// Add a Jacobian point and an affine point whose Z coordinate is one, the case +// that arises when accumulating the fixed input points in the combined ladder. +// Skipping the second point's Z powers saves several multiplications over the +// general addition (EFD madd-2007-bl) +inline auto point_add_mixed(const JacobianPoint &left, + const JacobianPoint &right, + const EllipticCurveParameters &curve) + -> JacobianPoint { + if (point_is_infinity(left)) { + return right; + } + + if (point_is_infinity(right)) { + return left; + } + + const auto &prime{curve.prime}; + const auto z_squared{field_square(left.z, curve)}; + const auto u2{field_mod_multiply(right.x, z_squared, curve)}; + const auto s2{field_mod_multiply( + right.y, field_mod_multiply(left.z, z_squared, curve), curve)}; + const auto h{bignum_mod_subtract(u2, left.x, prime)}; + + if (bignum_is_zero(h)) { + if (bignum_compare(s2, left.y) == 0) { + return point_double(left, curve); + } + + return {}; + } + + const auto h_squared{field_square(h, curve)}; + const auto two_h_squared{bignum_mod_add(h_squared, h_squared, prime)}; + const auto scaled_h_squared{ + bignum_mod_add(two_h_squared, two_h_squared, prime)}; + const auto j{field_mod_multiply(h, scaled_h_squared, curve)}; + const auto s_difference{bignum_mod_subtract(s2, left.y, prime)}; + const auto r{bignum_mod_add(s_difference, s_difference, prime)}; + const auto v{field_mod_multiply(left.x, scaled_h_squared, curve)}; + const auto two_v{bignum_mod_add(v, v, prime)}; const auto result_x{bignum_mod_subtract( - bignum_mod_subtract(bignum_mod_multiply(r, r, prime), h_cubed, prime), - bignum_mod_multiply(bignum_from_u64(2), u1_h_squared, prime), prime)}; + bignum_mod_subtract(field_mod_multiply(r, r, curve), j, prime), two_v, + prime)}; + const auto y_j{field_mod_multiply(left.y, j, curve)}; + const auto two_y_j{bignum_mod_add(y_j, y_j, prime)}; const auto result_y{bignum_mod_subtract( - bignum_mod_multiply(r, bignum_mod_subtract(u1_h_squared, result_x, prime), - prime), - bignum_mod_multiply(s1, h_cubed, prime), prime)}; - const auto result_z{bignum_mod_multiply(bignum_mod_multiply(h, left.z, prime), - right.z, prime)}; - return {result_x, result_y, result_z}; + field_mod_multiply(r, bignum_mod_subtract(v, result_x, prime), curve), + two_y_j, prime)}; + const auto z_plus_h{bignum_mod_add(left.z, h, prime)}; + const auto result_z{bignum_mod_subtract( + bignum_mod_subtract(field_square(z_plus_h, curve), z_squared, prime), + h_squared, prime)}; + return {.x = result_x, .y = result_y, .z = result_z}; +} + +// Normalize a Jacobian point to the affine representation with Z coordinate +// one, so that later additions can take the cheaper mixed path +inline auto point_to_affine(const JacobianPoint &point, + const EllipticCurveParameters &curve) + -> JacobianPoint { + if (point_is_infinity(point)) { + return {}; + } + + const auto z_inverse{bignum_mod_inverse(point.z, curve.prime)}; + const auto z_inverse_squared{field_square(z_inverse, curve)}; + const auto z_inverse_cubed{ + field_mod_multiply(z_inverse_squared, z_inverse, curve)}; + return {.x = field_mod_multiply(point.x, z_inverse_squared, curve), + .y = field_mod_multiply(point.y, z_inverse_cubed, curve), + .z = bignum_from_u64(1)}; } -inline auto point_scalar_multiply(const Bignum &scalar, - const JacobianPoint &point, - const EllipticCurveParameters &curve) +// Compute scalar_one * point_one + scalar_two * point_two with Shamir's trick, +// a single double-and-add over the longer scalar that adds a precomputed sum +// whenever both scalars have a set bit, halving the doublings of two separate +// scalar multiplications. The three addable points are kept affine so every +// step takes the mixed addition +inline auto point_double_scalar_multiply(const Bignum &scalar_one, + const JacobianPoint &point_one, + const Bignum &scalar_two, + const JacobianPoint &point_two, + const EllipticCurveParameters &curve) -> JacobianPoint { - JacobianPoint result{Bignum{}, Bignum{}, Bignum{}}; - const auto bits{bignum_bit_length(scalar)}; + const auto combined{ + point_to_affine(point_add(point_one, point_two, curve), curve)}; + JacobianPoint result{}; + const auto bits_one{bignum_bit_length(scalar_one)}; + const auto bits_two{bignum_bit_length(scalar_two)}; + const auto bits{bits_one > bits_two ? bits_one : bits_two}; for (std::size_t index = bits; index > 0; --index) { result = point_double(result, curve); - if (bignum_get_bit(scalar, index - 1)) { - result = point_add(result, point, curve); + const auto bit_one{bignum_get_bit(scalar_one, index - 1)}; + const auto bit_two{bignum_get_bit(scalar_two, index - 1)}; + if (bit_one && bit_two) { + result = point_add_mixed(result, combined, curve); + } else if (bit_one) { + result = point_add_mixed(result, point_one, curve); + } else if (bit_two) { + result = point_add_mixed(result, point_two, curve); } } @@ -182,21 +500,19 @@ inline auto point_scalar_multiply(const Bignum &scalar, inline auto point_affine_x(const JacobianPoint &point, const EllipticCurveParameters &curve) -> Bignum { const auto z_inverse{bignum_mod_inverse(point.z, curve.prime)}; - const auto z_inverse_squared{ - bignum_mod_multiply(z_inverse, z_inverse, curve.prime)}; - return bignum_mod_multiply(point.x, z_inverse_squared, curve.prime); + const auto z_inverse_squared{field_square(z_inverse, curve)}; + return field_mod_multiply(point.x, z_inverse_squared, curve); } // 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 { const auto &prime{curve.prime}; - const auto left{bignum_mod_multiply(y, y, prime)}; - const auto x_cubed{ - bignum_mod_multiply(bignum_mod_multiply(x, x, prime), x, prime)}; + const auto left{field_square(y, curve)}; + const auto x_cubed{field_mod_multiply(field_square(x, curve), x, curve)}; const auto right{bignum_mod_add( - bignum_mod_add(x_cubed, - bignum_mod_multiply(curve.coefficient_a, x, prime), prime), + bignum_mod_add(x_cubed, field_mod_multiply(curve.coefficient_a, x, curve), + prime), curve.coefficient_b, prime)}; return bignum_compare(left, right) == 0; } diff --git a/vendor/core/src/core/crypto/crypto_eddsa.h b/vendor/core/src/core/crypto/crypto_eddsa.h new file mode 100644 index 000000000..1ff0da48a --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_eddsa.h @@ -0,0 +1,448 @@ +#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 + +#include + +#include "crypto_bignum.h" +#include "crypto_shake256.h" + +#include // std::size_t +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +// A point in extended Edwards coordinates (X : Y : Z : T), where the affine +// point is (X / Z, Y / Z) and T = X * Y / Z (RFC 8032 Section 5.1.4) +struct EdwardsPoint { + Bignum x; + Bignum y; + Bignum z; + Bignum t; +}; + +struct EdwardsParameters { + Bignum prime; + Bignum order; + Bignum coefficient_a; + Bignum coefficient_d; + EdwardsPoint base; +}; + +// Interpret the bytes as a little-endian unsigned integer, the encoding EdDSA +// uses throughout (RFC 8032 Section 5.1.2), by reversing into the big-endian +// conversion +inline auto bignum_from_bytes_little_endian(const std::string_view input) + -> Bignum { + const std::string reversed{input.rbegin(), input.rend()}; + return bignum_from_bytes(reversed); +} + +// The complete unified Edwards addition formulas in extended coordinates +// (Hisil, Wong, Carter, and Dawson 2008), which hold for any two points, +// including equal points and the identity, since the curve coefficient is a +// square and d is a non-square modulo p +inline auto edwards_point_add(const EdwardsPoint &left, + const EdwardsPoint &right, + const EdwardsParameters ¶meters) + -> EdwardsPoint { + const auto &prime{parameters.prime}; + const auto a{bignum_mod_multiply(left.x, right.x, prime)}; + const auto b{bignum_mod_multiply(left.y, right.y, prime)}; + const auto c{bignum_mod_multiply( + bignum_mod_multiply(parameters.coefficient_d, left.t, prime), right.t, + prime)}; + const auto d{bignum_mod_multiply(left.z, right.z, prime)}; + const auto e{bignum_mod_subtract( + bignum_mod_multiply(bignum_mod_add(left.x, left.y, prime), + bignum_mod_add(right.x, right.y, prime), prime), + bignum_mod_add(a, b, prime), prime)}; + const auto f{bignum_mod_subtract(d, c, prime)}; + const auto g{bignum_mod_add(d, c, prime)}; + const auto h{bignum_mod_subtract( + b, bignum_mod_multiply(parameters.coefficient_a, a, prime), prime)}; + return EdwardsPoint{.x = bignum_mod_multiply(e, f, prime), + .y = bignum_mod_multiply(g, h, prime), + .z = bignum_mod_multiply(f, g, prime), + .t = bignum_mod_multiply(e, h, prime)}; +} + +inline auto edwards_point_scalar_multiply(const Bignum &scalar, + const EdwardsPoint &point, + const EdwardsParameters ¶meters) + -> EdwardsPoint { + // The identity element is (0 : 1 : 1 : 0) + EdwardsPoint result{.x = Bignum{}, + .y = bignum_from_u64(1), + .z = bignum_from_u64(1), + .t = Bignum{}}; + const auto bits{bignum_bit_length(scalar)}; + for (std::size_t index = bits; index > 0; --index) { + result = edwards_point_add(result, result, parameters); + if (bignum_get_bit(scalar, index - 1)) { + result = edwards_point_add(result, point, parameters); + } + } + + 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, + const EdwardsPoint &right, const Bignum &prime) + -> bool { + return bignum_compare(bignum_mod_multiply(left.x, right.z, prime), + bignum_mod_multiply(right.x, left.z, prime)) == 0 && + bignum_compare(bignum_mod_multiply(left.y, right.z, prime), + bignum_mod_multiply(right.y, left.z, prime)) == 0; +} + +// 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, + const Bignum &prime, + const Bignum &coefficient_d, + const Bignum &square_root_of_minus_one) + -> std::optional { + if (encoding.size() != 32) { + return std::nullopt; + } + + // The final bit holds the sign of x, the remaining bits the little-endian y + std::string bytes{encoding}; + const auto sign_bit{ + static_cast(static_cast(bytes.back()) >> 7) & 1u}; + bytes.back() = + static_cast(static_cast(bytes.back()) & 0x7fu); + const auto y{bignum_from_bytes_little_endian(bytes)}; + + // A y coordinate at or beyond the field prime is not a canonical encoding + if (bignum_compare(y, prime) >= 0) { + return std::nullopt; + } + + const auto one{bignum_from_u64(1)}; + const auto y_squared{bignum_mod_multiply(y, y, prime)}; + + // Solve x^2 = (y^2 - 1) / (d * y^2 + 1) (mod p) + const auto numerator{bignum_mod_subtract(y_squared, one, prime)}; + const auto denominator{bignum_mod_add( + bignum_mod_multiply(coefficient_d, y_squared, prime), one, prime)}; + + // The candidate root is x = numerator * denominator^3 * + // (numerator * denominator^7)^((p - 5) / 8) (mod p), a single powering that + // folds in the inversion of the denominator + const auto denominator_squared{ + bignum_mod_multiply(denominator, denominator, prime)}; + const auto denominator_cubed{ + bignum_mod_multiply(denominator_squared, denominator, prime)}; + const auto denominator_seventh{bignum_mod_multiply( + bignum_mod_multiply(denominator_cubed, denominator_cubed, prime), + denominator, prime)}; + auto exponent{prime}; + bignum_subtract_in_place(exponent, bignum_from_u64(5)); + exponent = bignum_shift_right(exponent, 3); + const auto root{ + bignum_mod_exp(bignum_mod_multiply(numerator, denominator_seventh, prime), + exponent, prime)}; + auto candidate{bignum_mod_multiply( + bignum_mod_multiply(numerator, denominator_cubed, prime), root, prime)}; + + // The candidate is correct when denominator * x^2 equals the numerator, off + // by sqrt(-1) when it equals its negation, and otherwise no root exists + const auto check{bignum_mod_multiply( + denominator, bignum_mod_multiply(candidate, candidate, prime), prime)}; + if (bignum_compare(check, numerator) != 0) { + const auto negated_numerator{ + bignum_mod_subtract(Bignum{}, numerator, prime)}; + if (bignum_compare(check, negated_numerator) != 0) { + return std::nullopt; + } + + candidate = bignum_mod_multiply(candidate, square_root_of_minus_one, prime); + } + + // Reject the non-canonical zero root with a set sign bit, then select the + // root whose low bit matches the encoded sign + if (bignum_is_zero(candidate) && sign_bit == 1) { + return std::nullopt; + } + + if (static_cast(bignum_get_bit(candidate, 0)) != sign_bit) { + auto negated{prime}; + bignum_subtract_in_place(negated, candidate); + candidate = negated; + } + + return EdwardsPoint{.x = candidate, + .y = y, + .z = one, + .t = bignum_mod_multiply(candidate, y, prime)}; +} + +// The Edwards25519 domain parameters (RFC 8032 Section 5.1) +inline auto edwards25519() -> EdwardsParameters { + EdwardsParameters parameters; + parameters.prime = bignum_from_hex( + "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed"); + parameters.order = bignum_from_hex( + "1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed"); + + // The curve coefficient a is -1 (mod p) + parameters.coefficient_a = parameters.prime; + bignum_subtract_in_place(parameters.coefficient_a, bignum_from_u64(1)); + + // d = -121665 / 121666 (mod p) + auto negated_numerator{parameters.prime}; + bignum_subtract_in_place(negated_numerator, bignum_from_u64(121665)); + parameters.coefficient_d = bignum_mod_multiply( + negated_numerator, + bignum_mod_inverse(bignum_from_u64(121666), parameters.prime), + parameters.prime); + + // sqrt(-1) = 2^((p - 1) / 4) (mod p), used to recover the second root + auto root_exponent{parameters.prime}; + bignum_subtract_in_place(root_exponent, bignum_from_u64(1)); + root_exponent = bignum_shift_right(root_exponent, 2); + const auto square_root_of_minus_one{ + bignum_mod_exp(bignum_from_u64(2), root_exponent, parameters.prime)}; + + // The base point is recovered from its canonical encoding, y = 4/5 with a + // clear sign bit (RFC 8032 Section 5.1) + std::string base_encoding; + base_encoding.push_back('\x58'); + base_encoding.append(31, '\x66'); + parameters.base = edwards25519_decode_point(base_encoding, parameters.prime, + parameters.coefficient_d, + square_root_of_minus_one) + .value(); + return parameters; +} + +// Verify an Ed25519 signature over a message (RFC 8032 Section 5.1.7), given +// the 32-byte public key and the 64-byte signature +inline auto edwards25519_verify(const std::string_view public_key, + const std::string_view message, + const std::string_view signature) -> bool { + if (public_key.size() != 32 || signature.size() != 64) { + return false; + } + + const auto parameters{edwards25519()}; + auto square_root_exponent{parameters.prime}; + bignum_subtract_in_place(square_root_exponent, bignum_from_u64(1)); + square_root_exponent = bignum_shift_right(square_root_exponent, 2); + const auto square_root_of_minus_one{bignum_mod_exp( + bignum_from_u64(2), square_root_exponent, parameters.prime)}; + + const auto public_point{edwards25519_decode_point( + public_key, parameters.prime, parameters.coefficient_d, + square_root_of_minus_one)}; + if (!public_point.has_value()) { + return false; + } + + // The signature is the encoded point R followed by the little-endian scalar + // S, which must lie below the group order + const auto encoded_r{signature.substr(0, 32)}; + const auto point_r{edwards25519_decode_point(encoded_r, parameters.prime, + parameters.coefficient_d, + square_root_of_minus_one)}; + if (!point_r.has_value()) { + return false; + } + + const auto scalar_s{bignum_from_bytes_little_endian(signature.substr(32))}; + if (bignum_compare(scalar_s, parameters.order) >= 0) { + return false; + } + + // k = SHA-512(R || A || M) reduced modulo the group order + std::string preimage; + preimage.reserve(encoded_r.size() + public_key.size() + message.size()); + preimage.append(encoded_r); + preimage.append(public_key); + preimage.append(message); + const auto digest{sha512_digest(preimage)}; + auto scalar_k{bignum_from_bytes_little_endian(std::string_view{ + reinterpret_cast(digest.data()), digest.size()})}; + bignum_reduce(scalar_k, parameters.order); + + // The signature holds when [S]B = R + [k]A + const auto left{ + edwards_point_scalar_multiply(scalar_s, parameters.base, parameters)}; + const auto right{edwards_point_add( + point_r.value(), + edwards_point_scalar_multiply(scalar_k, public_point.value(), parameters), + parameters)}; + return edwards_point_equal(left, right, parameters.prime); +} + +// 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, + const Bignum &prime, + const Bignum &coefficient_d) + -> std::optional { + if (encoding.size() != 57) { + return std::nullopt; + } + + // The final bit holds the sign of x, the remaining bits the little-endian y + std::string bytes{encoding}; + const auto sign_bit{ + static_cast(static_cast(bytes.back()) >> 7) & 1u}; + bytes.back() = + static_cast(static_cast(bytes.back()) & 0x7fu); + const auto y{bignum_from_bytes_little_endian(bytes)}; + + // A y coordinate at or beyond the field prime is not a canonical encoding + if (bignum_compare(y, prime) >= 0) { + return std::nullopt; + } + + const auto one{bignum_from_u64(1)}; + const auto y_squared{bignum_mod_multiply(y, y, prime)}; + + // Solve x^2 = (y^2 - 1) / (d * y^2 - 1) (mod p) + const auto numerator{bignum_mod_subtract(y_squared, one, prime)}; + const auto denominator{bignum_mod_subtract( + bignum_mod_multiply(coefficient_d, y_squared, prime), one, prime)}; + + // The candidate root is x = numerator^3 * denominator * + // (numerator^5 * denominator^3)^((p - 3) / 4) (mod p), the field having + // p congruent to 3 modulo 4 + const auto numerator_squared{ + bignum_mod_multiply(numerator, numerator, prime)}; + const auto numerator_cubed{ + bignum_mod_multiply(numerator_squared, numerator, prime)}; + const auto numerator_fifth{ + bignum_mod_multiply(numerator_squared, numerator_cubed, prime)}; + const auto denominator_squared{ + bignum_mod_multiply(denominator, denominator, prime)}; + const auto denominator_cubed{ + bignum_mod_multiply(denominator_squared, denominator, prime)}; + auto exponent{prime}; + bignum_subtract_in_place(exponent, bignum_from_u64(3)); + exponent = bignum_shift_right(exponent, 2); + const auto root{bignum_mod_exp( + bignum_mod_multiply(numerator_fifth, denominator_cubed, prime), exponent, + prime)}; + auto candidate{bignum_mod_multiply( + bignum_mod_multiply(numerator_cubed, denominator, prime), root, prime)}; + + // The candidate is correct when denominator * x^2 equals the numerator, and + // otherwise no root exists, as the field admits a single square root + const auto check{bignum_mod_multiply( + denominator, bignum_mod_multiply(candidate, candidate, prime), prime)}; + if (bignum_compare(check, numerator) != 0) { + return std::nullopt; + } + + // Reject the non-canonical zero root with a set sign bit, then select the + // root whose low bit matches the encoded sign + if (bignum_is_zero(candidate) && sign_bit == 1) { + return std::nullopt; + } + + if (static_cast(bignum_get_bit(candidate, 0)) != sign_bit) { + auto negated{prime}; + bignum_subtract_in_place(negated, candidate); + candidate = negated; + } + + return EdwardsPoint{.x = candidate, + .y = y, + .z = one, + .t = bignum_mod_multiply(candidate, y, prime)}; +} + +// The Edwards448 domain parameters (RFC 8032 Section 5.2) +inline auto edwards448() -> EdwardsParameters { + EdwardsParameters parameters; + // clang-format off + parameters.prime = bignum_from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + parameters.order = bignum_from_hex("3fffffffffffffffffffffffffffffffffffffffffffffffffffffff7cca23e9c44edb49aed63690216cc2728dc58f552378c292ab5844f3"); + // clang-format on + + // The curve coefficient a is 1, and d is -39081 (mod p) + parameters.coefficient_a = bignum_from_u64(1); + parameters.coefficient_d = parameters.prime; + bignum_subtract_in_place(parameters.coefficient_d, bignum_from_u64(39081)); + + // The base point is recovered from its canonical 57-octet encoding (RFC 8032 + // Section 5.2) + // clang-format off + const auto base_encoding{bignum_to_bytes(bignum_from_hex("14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f6900"), 57)}; + // clang-format on + parameters.base = edwards448_decode_point(base_encoding, parameters.prime, + parameters.coefficient_d) + .value(); + return parameters; +} + +// Verify an Ed448 signature over a message (RFC 8032 Section 5.2.7), given the +// 57-byte public key and the 114-byte signature +inline auto edwards448_verify(const std::string_view public_key, + const std::string_view message, + const std::string_view signature) -> bool { + if (public_key.size() != 57 || signature.size() != 114) { + return false; + } + + const auto parameters{edwards448()}; + const auto public_point{edwards448_decode_point(public_key, parameters.prime, + parameters.coefficient_d)}; + if (!public_point.has_value()) { + return false; + } + + // The signature is the encoded point R followed by the little-endian scalar + // S, which must lie below the group order + const auto encoded_r{signature.substr(0, 57)}; + const auto point_r{edwards448_decode_point(encoded_r, parameters.prime, + parameters.coefficient_d)}; + if (!point_r.has_value()) { + return false; + } + + const auto scalar_s{bignum_from_bytes_little_endian(signature.substr(57))}; + if (bignum_compare(scalar_s, parameters.order) >= 0) { + return false; + } + + // k = SHAKE256(dom4 || R || A || M) reduced modulo the group order, where + // dom4 is "SigEd448" followed by the zero pre-hash flag and an empty context + // (RFC 8032 Section 5.2.7 and Section 2) + std::string preimage{"SigEd448"}; + preimage.push_back('\x00'); + preimage.push_back('\x00'); + preimage.append(encoded_r); + preimage.append(public_key); + preimage.append(message); + const auto digest{shake256(preimage, 114)}; + auto scalar_k{bignum_from_bytes_little_endian(digest)}; + bignum_reduce(scalar_k, parameters.order); + + // The signature holds when [S]B = R + [k]A + const auto left{ + edwards_point_scalar_multiply(scalar_s, parameters.base, parameters)}; + const auto right{edwards_point_add( + point_r.value(), + edwards_point_scalar_multiply(scalar_k, public_point.value(), parameters), + parameters)}; + return edwards_point_equal(left, right, parameters.prime); +} + +} // 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 new file mode 100644 index 000000000..7426649a4 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_eddsa_apple.h @@ -0,0 +1,15 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_EDDSA_APPLE_H_ +#define SOURCEMETA_CORE_CRYPTO_EDDSA_APPLE_H_ + +#include // std::size_t + +// Verify an Ed25519 signature through CryptoKit, defined in the Objective-C++ +// bridge that consumes the Swift shim. The signature is invalid rather than an +// error for any malformed input, including a key or signature of the wrong +// length, since CryptoKit rejects those inputs +extern "C" auto sourcemeta_core_eddsa_ed25519_verify_cryptokit( + const unsigned char *public_key, std::size_t public_key_size, + const unsigned char *message, std::size_t message_size, + const unsigned char *signature, std::size_t signature_size) -> bool; + +#endif diff --git a/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.mm b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.mm new file mode 100644 index 000000000..814b6b505 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.mm @@ -0,0 +1,20 @@ +#include "crypto_eddsa_apple.h" + +#import // NSData + +// The Objective-C interface generated from the Swift shim +#import "sourcemeta_core_cryptokit-Swift.h" + +extern "C" auto sourcemeta_core_eddsa_ed25519_verify_cryptokit( + const unsigned char *public_key, std::size_t public_key_size, + const unsigned char *message, std::size_t message_size, + const unsigned char *signature, std::size_t signature_size) -> bool { + @autoreleasepool { + NSData *const key{[NSData dataWithBytes:public_key length:public_key_size]}; + NSData *const payload{[NSData dataWithBytes:message length:message_size]}; + NSData *const tag{[NSData dataWithBytes:signature length:signature_size]}; + return [SourcemetaCoreEd25519 verifyWithPublicKey:key + message:payload + signature:tag] == YES; + } +} diff --git a/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.swift b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.swift new file mode 100644 index 000000000..0ab74c742 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.swift @@ -0,0 +1,19 @@ +import CryptoKit +import Foundation + +// The Ed25519 verification primitive that the Apple Security framework does +// not expose through its C API. CryptoKit provides it since macOS 10.15, and +// this class surfaces it to the Objective-C++ bridge through the generated +// Objective-C interface header +@objc(SourcemetaCoreEd25519) +public final class SourcemetaCoreEd25519: NSObject { + @objc public static func verify(publicKey: Data, message: Data, + signature: Data) -> Bool { + guard let key = try? Curve25519.Signing.PublicKey( + rawRepresentation: publicKey) else { + return false + } + + return key.isValidSignature(signature, for: message) + } +} diff --git a/vendor/core/src/core/crypto/crypto_helpers.h b/vendor/core/src/core/crypto/crypto_helpers.h index 645b4d961..e1751d464 100644 --- a/vendor/core/src/core/crypto/crypto_helpers.h +++ b/vendor/core/src/core/crypto/crypto_helpers.h @@ -5,6 +5,7 @@ #include #include #include +#include #include // std::size_t #include // std::string @@ -17,6 +18,22 @@ namespace sourcemeta::core { // the range of valid key sizes inline constexpr std::size_t MAXIMUM_KEY_BYTES{512}; +// 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 +// without changing the modular exponentiation result, is rejected +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 value < bound; +} + inline auto curve_field_bytes(const EllipticCurve curve) noexcept -> std::size_t { switch (curve) { @@ -31,6 +48,32 @@ inline auto curve_field_bytes(const EllipticCurve curve) noexcept std::unreachable(); } +// 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 + -> std::size_t { + switch (curve) { + case EdwardsCurve::Ed25519: + return 32; + case EdwardsCurve::Ed448: + return 57; + } + + std::unreachable(); +} + +inline auto eddsa_signature_bytes(const EdwardsCurve curve) noexcept + -> std::size_t { + switch (curve) { + case EdwardsCurve::Ed25519: + return 64; + case EdwardsCurve::Ed448: + return 114; + } + + std::unreachable(); +} + inline auto digest_message(const SignatureHashFunction hash, const std::string_view message) -> std::string { switch (hash) { diff --git a/vendor/core/src/core/crypto/crypto_shake256.h b/vendor/core/src/core/crypto/crypto_shake256.h new file mode 100644 index 000000000..dd942542b --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_shake256.h @@ -0,0 +1,131 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_SHAKE256_H_ +#define SOURCEMETA_CORE_CRYPTO_SHAKE256_H_ + +// The SHAKE256 extendable-output function (FIPS 202), built on the Keccak-f +// [1600] permutation, for the reference Ed448 verification backend + +#include // std::array +#include // std::size_t +#include // std::uint8_t, std::uint64_t +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +inline constexpr std::array keccak_round_constants{ + {0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL, + 0x8000000080008000ULL, 0x000000000000808bULL, 0x0000000080000001ULL, + 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL, + 0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000aULL, + 0x000000008000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, + 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, + 0x000000000000800aULL, 0x800000008000000aULL, 0x8000000080008081ULL, + 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL}}; + +// The rotation offsets and destination lanes of the combined rho and pi steps, +// walking the lanes starting from lane 1 +inline constexpr std::array keccak_rho_offsets{ + {1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, + 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44}}; + +inline constexpr std::array keccak_pi_lanes{ + {10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, + 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1}}; + +inline constexpr auto keccak_rotate_left(const std::uint64_t value, + const unsigned offset) noexcept + -> std::uint64_t { + return (value << offset) | (value >> (64u - offset)); +} + +inline auto keccak_permute(std::array &state) noexcept + -> void { + for (std::size_t round = 0; round < 24; ++round) { + // Theta + std::array column_parity{}; + for (std::size_t column = 0; column < 5; ++column) { + column_parity[column] = state[column] ^ state[column + 5] ^ + state[column + 10] ^ state[column + 15] ^ + state[column + 20]; + } + + for (std::size_t column = 0; column < 5; ++column) { + const auto delta{column_parity[(column + 4) % 5] ^ + keccak_rotate_left(column_parity[(column + 1) % 5], 1)}; + for (std::size_t row = 0; row < 25; row += 5) { + state[row + column] ^= delta; + } + } + + // Rho and pi + auto current{state[1]}; + for (std::size_t index = 0; index < 24; ++index) { + const auto lane{keccak_pi_lanes[index]}; + const auto moved{state[lane]}; + state[lane] = keccak_rotate_left(current, keccak_rho_offsets[index]); + current = moved; + } + + // Chi + for (std::size_t row = 0; row < 25; row += 5) { + std::array plane{}; + for (std::size_t column = 0; column < 5; ++column) { + plane[column] = state[row + column]; + } + + for (std::size_t column = 0; column < 5; ++column) { + state[row + column] = plane[column] ^ (~plane[(column + 1) % 5] & + plane[(column + 2) % 5]); + } + } + + // Iota + state[0] ^= keccak_round_constants[round]; + } +} + +// Hash a string with SHAKE256, returning the requested number of output bytes +inline auto shake256(const std::string_view input, + const std::size_t output_length) -> std::string { + // The bitrate is 1600 - 2 * 256 = 1088 bits, that is 136 octets + constexpr std::size_t rate{136}; + std::array state{}; + + std::size_t pointer{0}; + for (const auto character : input) { + state[pointer / 8] ^= + static_cast(static_cast(character)) + << (8 * (pointer % 8)); + pointer += 1; + if (pointer == rate) { + keccak_permute(state); + pointer = 0; + } + } + + // The SHAKE domain separation suffix is the bits 1111, padded to the rate + // with the pad10*1 rule + state[pointer / 8] ^= static_cast(0x1f) << (8 * (pointer % 8)); + state[(rate - 1) / 8] ^= static_cast(0x80) + << (8 * ((rate - 1) % 8)); + keccak_permute(state); + + std::string output; + output.reserve(output_length); + std::size_t squeeze_pointer{0}; + while (output.size() < output_length) { + output.push_back(static_cast( + (state[squeeze_pointer / 8] >> (8 * (squeeze_pointer % 8))) & 0xffu)); + squeeze_pointer += 1; + if (squeeze_pointer == rate) { + keccak_permute(state); + squeeze_pointer = 0; + } + } + + return output; +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/crypto_verify_apple.cc b/vendor/core/src/core/crypto/crypto_verify_apple.cc new file mode 100644 index 000000000..7f116b0f4 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_apple.cc @@ -0,0 +1,368 @@ +#include +#include + +#include "crypto_eddsa.h" +#include "crypto_eddsa_apple.h" +#include "crypto_helpers.h" + +#include // CF*, kCF* +#include // Sec*, kSec* + +#include // std::array +#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 encoded +// point and verify through CryptoKit or the reference implementation +struct PublicKey::Internal { + PublicKey::Type kind; + SecKeyRef key; + std::string modulus; + std::size_t field_bytes; + std::string edwards_point; + EdwardsCurve edwards_curve; +}; + +} // 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_rsa_key(const std::string_view modulus, + const std::string_view exponent) -> SecKeyRef { + // The platform expects the PKCS#1 RSAPublicKey structure, a DER sequence of + // the modulus and exponent integers (RFC 8017 Appendix A.1.1) + std::string body; + sourcemeta::core::der_append_unsigned_integer(body, modulus); + sourcemeta::core::der_append_unsigned_integer(body, exponent); + std::string der; + der.push_back('\x30'); + sourcemeta::core::der_append_length(der, body.size()); + der.append(body); + + auto key_data{make_data(der)}; + if (key_data == nullptr) { + return nullptr; + } + + std::array attribute_keys{ + {kSecAttrKeyType, kSecAttrKeyClass}}; + std::array attribute_values{ + {kSecAttrKeyTypeRSA, kSecAttrKeyClassPublic}}; + 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{SecKeyCreateWithData(key_data, attributes, nullptr)}; + CFRelease(attributes); + CFRelease(key_data); + return key; +} + +auto native_ec_key(const sourcemeta::core::EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) -> SecKeyRef { + 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')}; + if (stripped_x.size() > width || stripped_y.size() > width) { + return nullptr; + } + + // The platform infers the curve from the X9.63 uncompressed point, the 0x04 + // lead byte followed by the two fixed-width coordinates + 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')); + + auto key_data{make_data(point)}; + if (key_data == nullptr) { + return nullptr; + } + + std::array attribute_keys{ + {kSecAttrKeyType, kSecAttrKeyClass}}; + std::array attribute_values{ + {kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeyClassPublic}}; + 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{SecKeyCreateWithData(key_data, attributes, nullptr)}; + CFRelease(attributes); + CFRelease(key_data); + return key; +} + +auto encode_ecdsa_signature(const std::string_view raw_signature) + -> std::string { + // The raw form is the two integers concatenated, while the platform expects + // the X9.62 DER sequence of those integers + const auto half{raw_signature.size() / 2}; + std::string body; + sourcemeta::core::der_append_unsigned_integer(body, + raw_signature.substr(0, half)); + sourcemeta::core::der_append_unsigned_integer(body, + raw_signature.substr(half)); + std::string der; + der.push_back('\x30'); + sourcemeta::core::der_append_length(der, body.size()); + der.append(body); + return der; +} + +auto verify_with_algorithm(SecKeyRef key, const SecKeyAlgorithm algorithm, + const std::string_view message, + const std::string_view signature) -> bool { + auto message_data{make_data(message)}; + auto signature_data{make_data(signature)}; + auto result{false}; + if (message_data != nullptr && signature_data != nullptr) { + result = SecKeyVerifySignature(key, algorithm, message_data, signature_data, + nullptr) == true; + } + + if (signature_data != nullptr) { + CFRelease(signature_data); + } + + if (message_data != nullptr) { + CFRelease(message_data); + } + + return result; +} + +} // namespace + +namespace sourcemeta::core { + +PublicKey::PublicKey(Internal *internal) noexcept : internal_{internal} {} + +PublicKey::~PublicKey() { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + CFRelease(internal_->key); + } + + delete internal_; + } +} + +PublicKey::PublicKey(PublicKey &&other) noexcept : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { + if (this != &other) { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + CFRelease(internal_->key); + } + + delete internal_; + } + + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PublicKey::type() const noexcept -> Type { return internal_->kind; } + +auto make_rsa_public_key(const std::string_view modulus, + const std::string_view exponent) + -> std::optional { + auto stripped_modulus{std::string{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) { + return std::nullopt; + } + + auto *key{native_rsa_key(stripped_modulus, stripped_exponent)}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::RSA, + .key = key, + .modulus = std::move(stripped_modulus), + .field_bytes = 0, + .edwards_point = {}, + .edwards_curve = {}}}; +} + +auto make_ec_public_key(const EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + auto *key{native_ec_key(curve, coordinate_x, coordinate_y)}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::EllipticCurve, + .key = key, + .modulus = {}, + .field_bytes = curve_field_bytes(curve), + .edwards_point = {}, + .edwards_curve = {}}}; +} + +auto make_eddsa_public_key(const EdwardsCurve curve, + const std::string_view public_key) + -> std::optional { + if (public_key.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::Edwards, + .key = nullptr, + .modulus = {}, + .field_bytes = 0, + .edwards_point = std::string{public_key}, + .edwards_curve = curve}}; +} + +auto rsassa_pkcs1_v15_verify(const PublicKey &key, + const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_with_algorithm( + internal->key, to_sec_key_pkcs1_v15_algorithm(hash), message, signature); +} + +auto rsassa_pss_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_with_algorithm(internal->key, to_sec_key_pss_algorithm(hash), + message, signature); +} + +auto ecdsa_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::EllipticCurve || + signature.size() != internal->field_bytes * 2) { + return false; + } + + return verify_with_algorithm(internal->key, to_sec_key_ecdsa_algorithm(hash), + message, encode_ecdsa_signature(signature)); +} + +auto eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::Edwards || + signature.size() != eddsa_signature_bytes(internal->edwards_curve)) { + return false; + } + + switch (internal->edwards_curve) { + case EdwardsCurve::Ed25519: + return sourcemeta_core_eddsa_ed25519_verify_cryptokit( + reinterpret_cast( + internal->edwards_point.data()), + internal->edwards_point.size(), + reinterpret_cast(message.data()), + message.size(), + reinterpret_cast(signature.data()), + signature.size()); + case EdwardsCurve::Ed448: + return edwards448_verify(internal->edwards_point, message, signature); + } + + std::unreachable(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_ecdsa_apple.cc b/vendor/core/src/core/crypto/crypto_verify_ecdsa_apple.cc deleted file mode 100644 index 5a23aba1a..000000000 --- a/vendor/core/src/core/crypto/crypto_verify_ecdsa_apple.cc +++ /dev/null @@ -1,135 +0,0 @@ -#include -#include - -#include "crypto_helpers.h" - -#include // CF*, kCF* -#include // Sec*, kSec* - -#include // std::array -#include // std::string -#include // std::string_view -#include // std::unreachable - -namespace { - -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 make_ec_public_key(const sourcemeta::core::EllipticCurve curve, - const std::string_view coordinate_x, - const std::string_view coordinate_y) -> SecKeyRef { - 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')}; - if (stripped_x.size() > width || stripped_y.size() > width) { - return nullptr; - } - - // The platform infers the curve from the X9.63 uncompressed point, the - // 0x04 lead byte followed by the two fixed-width coordinates - 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')); - - auto key_data{make_data(point)}; - if (key_data == nullptr) { - return nullptr; - } - - std::array attribute_keys{ - {kSecAttrKeyType, kSecAttrKeyClass}}; - std::array attribute_values{ - {kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeyClassPublic}}; - 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{SecKeyCreateWithData(key_data, attributes, nullptr)}; - CFRelease(attributes); - CFRelease(key_data); - return key; -} - -auto encode_ecdsa_signature(const std::string_view raw_signature) - -> std::string { - // The raw form is the two integers concatenated, while the platform - // expects the X9.62 DER sequence of those integers - const auto half{raw_signature.size() / 2}; - std::string body; - sourcemeta::core::der_append_unsigned_integer(body, - raw_signature.substr(0, half)); - sourcemeta::core::der_append_unsigned_integer(body, - raw_signature.substr(half)); - std::string der; - der.push_back('\x30'); - sourcemeta::core::der_append_length(der, body.size()); - der.append(body); - return der; -} - -} // namespace - -namespace sourcemeta::core { - -auto ecdsa_verify(const EllipticCurve curve, const SignatureHashFunction hash, - const std::string_view coordinate_x, - const std::string_view coordinate_y, - const std::string_view message, - const std::string_view signature) -> bool { - if (signature.size() != sourcemeta::core::curve_field_bytes(curve) * 2) { - return false; - } - - auto key{make_ec_public_key(curve, coordinate_x, coordinate_y)}; - if (key == nullptr) { - return false; - } - - const auto der_signature{encode_ecdsa_signature(signature)}; - auto message_data{make_data(message)}; - auto signature_data{make_data(der_signature)}; - auto result{false}; - if (message_data != nullptr && signature_data != nullptr) { - result = - SecKeyVerifySignature(key, to_sec_key_ecdsa_algorithm(hash), - message_data, signature_data, nullptr) == true; - } - - if (signature_data != nullptr) { - CFRelease(signature_data); - } - - if (message_data != nullptr) { - CFRelease(message_data); - } - - CFRelease(key); - return result; -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_ecdsa_openssl.cc b/vendor/core/src/core/crypto/crypto_verify_ecdsa_openssl.cc deleted file mode 100644 index f97417219..000000000 --- a/vendor/core/src/core/crypto/crypto_verify_ecdsa_openssl.cc +++ /dev/null @@ -1,161 +0,0 @@ -#include -#include - -#include "crypto_helpers.h" - -#include // BN_* -#include // OSSL_PKEY_PARAM_* -#include // ECDSA_SIG_*, i2d_ECDSA_SIG -#include // EVP_* -#include // OSSL_PARAM_* - -#include // std::size_t -#include // std::string -#include // std::string_view -#include // std::unreachable - -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(); -} - -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 make_ec_public_key(const sourcemeta::core::EllipticCurve curve, - 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')}; - if (stripped_x.size() > width || stripped_y.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')); - - EVP_PKEY *result{nullptr}; - auto *builder{OSSL_PARAM_BLD_new()}; - if (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) { - 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_PUBLIC_KEY, parameters); - } - - EVP_PKEY_CTX_free(context); - } - - OSSL_PARAM_free(parameters); - } - } - - OSSL_PARAM_BLD_free(builder); - return result; -} - -// Convert the raw fixed-width R || S concatenation into the DER signature -// that the verification interface expects -auto encode_ecdsa_signature(const std::string_view raw_signature, - unsigned char **output) -> int { - const auto half{raw_signature.size() / 2}; - auto *signature{ECDSA_SIG_new()}; - if (signature == nullptr) { - return -1; - } - - auto *r{ - BN_bin2bn(reinterpret_cast(raw_signature.data()), - static_cast(half), nullptr)}; - auto *s{BN_bin2bn( - reinterpret_cast(raw_signature.data() + half), - static_cast(half), nullptr)}; - if (r == nullptr || s == nullptr || ECDSA_SIG_set0(signature, r, s) != 1) { - BN_free(r); - BN_free(s); - ECDSA_SIG_free(signature); - return -1; - } - - const auto length{i2d_ECDSA_SIG(signature, output)}; - ECDSA_SIG_free(signature); - return length; -} - -} // namespace - -namespace sourcemeta::core { - -auto ecdsa_verify(const EllipticCurve curve, const SignatureHashFunction hash, - const std::string_view coordinate_x, - const std::string_view coordinate_y, - const std::string_view message, - const std::string_view signature) -> bool { - if (signature.size() != sourcemeta::core::curve_field_bytes(curve) * 2) { - return false; - } - - auto *key{make_ec_public_key(curve, coordinate_x, coordinate_y)}; - if (key == nullptr) { - return false; - } - - unsigned char *der_signature{nullptr}; - const auto der_length{encode_ecdsa_signature(signature, &der_signature)}; - auto result{false}; - if (der_length > 0) { - auto *context{EVP_MD_CTX_new()}; - if (context != nullptr) { - if (EVP_DigestVerifyInit(context, nullptr, to_message_digest(hash), - nullptr, key) == 1) { - result = - EVP_DigestVerify( - context, der_signature, static_cast(der_length), - reinterpret_cast(message.data()), - message.size()) == 1; - } - - EVP_MD_CTX_free(context); - } - } - - OPENSSL_free(der_signature); - EVP_PKEY_free(key); - return result; -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_ecdsa_other.cc b/vendor/core/src/core/crypto/crypto_verify_ecdsa_other.cc deleted file mode 100644 index df2a428a7..000000000 --- a/vendor/core/src/core/crypto/crypto_verify_ecdsa_other.cc +++ /dev/null @@ -1,119 +0,0 @@ -#include -#include - -#include "crypto_bignum.h" -#include "crypto_ecc.h" -#include "crypto_helpers.h" - -#include // std::size_t -#include // std::string -#include // std::string_view -#include // std::unreachable - -namespace { - -auto to_curve_parameters(const sourcemeta::core::EllipticCurve curve) - -> sourcemeta::core::EllipticCurveParameters { - switch (curve) { - case sourcemeta::core::EllipticCurve::P256: - return sourcemeta::core::curve_p256(); - case sourcemeta::core::EllipticCurve::P384: - return sourcemeta::core::curve_p384(); - case sourcemeta::core::EllipticCurve::P521: - return sourcemeta::core::curve_p521(); - } - - std::unreachable(); -} - -// FIPS 186-4 Section 6.4 step 2, deriving the integer e from the leftmost -// bits of the message digest, truncated to the bit length of the order -auto digest_to_integer(const sourcemeta::core::SignatureHashFunction hash, - const std::string_view message, - const std::size_t order_bits) - -> sourcemeta::core::Bignum { - const auto digest{sourcemeta::core::digest_message(hash, message)}; - auto value{sourcemeta::core::bignum_from_bytes(digest)}; - const auto digest_bits{digest.size() * 8}; - if (digest_bits > order_bits) { - value = - sourcemeta::core::bignum_shift_right(value, digest_bits - order_bits); - } - - return value; -} - -} // namespace - -namespace sourcemeta::core { - -auto ecdsa_verify(const EllipticCurve curve, const SignatureHashFunction hash, - const std::string_view coordinate_x, - const std::string_view coordinate_y, - const std::string_view message, - const std::string_view signature) -> bool { - const auto parameters{to_curve_parameters(curve)}; - const auto field_bytes{parameters.field_bytes}; - - // RFC 7518 Section 3.4: the signature is the fixed-width concatenation of - // the two integers, each as long as the curve field - if (signature.size() != field_bytes * 2) { - return false; - } - - const auto r{bignum_from_bytes(signature.substr(0, field_bytes))}; - const auto s{bignum_from_bytes(signature.substr(field_bytes))}; - - // FIPS 186-4 Section 6.4.2 step 1: both integers must lie in [1, n - 1] - if (bignum_is_zero(r) || bignum_compare(r, parameters.order) >= 0 || - bignum_is_zero(s) || bignum_compare(s, parameters.order) >= 0) { - return false; - } - - // Reject coordinates wider than the field, matching the platform backends - // and preventing an oversized input from being truncated into a valid key - const auto stripped_x{sourcemeta::core::strip_left(coordinate_x, '\x00')}; - const auto stripped_y{sourcemeta::core::strip_left(coordinate_y, '\x00')}; - if (stripped_x.size() > field_bytes || stripped_y.size() > field_bytes) { - return false; - } - - const auto public_x{bignum_from_bytes(stripped_x)}; - const auto public_y{bignum_from_bytes(stripped_y)}; - - // The public key must be a valid point: coordinates below the field prime - // and satisfying the curve equation - if (bignum_compare(public_x, parameters.prime) >= 0 || - bignum_compare(public_y, parameters.prime) >= 0 || - !point_on_curve(public_x, public_y, parameters)) { - return false; - } - - const auto order_bits{bignum_bit_length(parameters.order)}; - const auto digest_integer{digest_to_integer(hash, message, order_bits)}; - const auto s_inverse{bignum_mod_inverse(s, parameters.order)}; - const auto u1{ - bignum_mod_multiply(digest_integer, s_inverse, parameters.order)}; - const auto u2{bignum_mod_multiply(r, s_inverse, parameters.order)}; - - const JacobianPoint generator{parameters.generator_x, parameters.generator_y, - bignum_from_u64(1)}; - const JacobianPoint public_point{public_x, public_y, bignum_from_u64(1)}; - const auto point{point_add( - point_scalar_multiply(u1, generator, parameters), - point_scalar_multiply(u2, public_point, parameters), parameters)}; - - // FIPS 186-4 Section 6.4.2 step 6: reject when the combination is the - // point at infinity - if (point_is_infinity(point)) { - return false; - } - - // FIPS 186-4 Section 6.4.2 step 7: the signature is valid when the affine - // x coordinate, reduced modulo the order, equals r - auto candidate{point_affine_x(point, parameters)}; - bignum_reduce(candidate, parameters.order); - return bignum_compare(candidate, r) == 0; -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_ecdsa_windows.cc b/vendor/core/src/core/crypto/crypto_verify_ecdsa_windows.cc deleted file mode 100644 index 6654099dc..000000000 --- a/vendor/core/src/core/crypto/crypto_verify_ecdsa_windows.cc +++ /dev/null @@ -1,107 +0,0 @@ -#include -#include - -#include "crypto_helpers.h" - -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#include // ULONG, LPCWSTR - -#include // BCrypt*, BCRYPT_* - -#include // std::memcpy -#include // std::string -#include // std::string_view -#include // std::unreachable - -namespace { - -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_public_magic(const sourcemeta::core::EllipticCurve curve) noexcept - -> ULONG { - switch (curve) { - case sourcemeta::core::EllipticCurve::P256: - return BCRYPT_ECDSA_PUBLIC_P256_MAGIC; - case sourcemeta::core::EllipticCurve::P384: - return BCRYPT_ECDSA_PUBLIC_P384_MAGIC; - case sourcemeta::core::EllipticCurve::P521: - return BCRYPT_ECDSA_PUBLIC_P521_MAGIC; - } - - std::unreachable(); -} - -} // namespace - -namespace sourcemeta::core { - -auto ecdsa_verify(const EllipticCurve curve, const SignatureHashFunction hash, - const std::string_view coordinate_x, - const std::string_view coordinate_y, - const std::string_view message, - const std::string_view signature) -> bool { - 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')}; - if (signature.size() != width * 2 || stripped_x.size() > width || - stripped_y.size() > width) { - return false; - } - - // The public key blob is the header followed by the two fixed-width - // coordinates - BCRYPT_ECCKEY_BLOB header{}; - header.dwMagic = to_ecc_public_magic(curve); - header.cbKey = static_cast(width); - - std::string blob; - blob.resize(sizeof(header)); - std::memcpy(blob.data(), &header, sizeof(header)); - blob.append(sourcemeta::core::pad_left(stripped_x, width, '\x00')); - blob.append(sourcemeta::core::pad_left(stripped_y, width, '\x00')); - - BCRYPT_ALG_HANDLE algorithm{nullptr}; - if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( - &algorithm, to_ecdsa_algorithm(curve), nullptr, 0))) { - return false; - } - - BCRYPT_KEY_HANDLE key{nullptr}; - if (!BCRYPT_SUCCESS( - BCryptImportKeyPair(algorithm, nullptr, BCRYPT_ECCPUBLIC_BLOB, &key, - reinterpret_cast(blob.data()), - static_cast(blob.size()), 0))) { - BCryptCloseAlgorithmProvider(algorithm, 0); - return false; - } - - const auto digest{sourcemeta::core::digest_message(hash, message)}; - - // The CNG signature format is the raw fixed-width R || S concatenation, so - // the input passes through unchanged - const auto result{BCRYPT_SUCCESS(BCryptVerifySignature( - key, nullptr, - reinterpret_cast(const_cast(digest.data())), - static_cast(digest.size()), - reinterpret_cast(const_cast(signature.data())), - static_cast(signature.size()), 0))}; - - BCryptDestroyKey(key); - BCryptCloseAlgorithmProvider(algorithm, 0); - return result; -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_openssl.cc b/vendor/core/src/core/crypto/crypto_verify_openssl.cc new file mode 100644 index 000000000..428ba2f1d --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_openssl.cc @@ -0,0 +1,404 @@ +#include +#include + +#include "crypto_helpers.h" + +#include // BN_* +#include // OSSL_PKEY_PARAM_* +#include // ECDSA_SIG_*, i2d_ECDSA_SIG +#include // EVP_* +#include // OSSL_PARAM_* +#include // RSA_PKCS1_PSS_PADDING, RSA_PSS_SALTLEN_DIGEST + +#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 verify +// without rebuilding it +struct PublicKey::Internal { + PublicKey::Type kind; + EVP_PKEY *key; + // The stripped modulus, kept for the RSA signature range check + std::string modulus; + // The field width for the elliptic curve signature size check + std::size_t field_bytes; + // The expected signature length for the Edwards curve + std::size_t signature_bytes; +}; + +} // 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(); +} + +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(); +} + +auto native_rsa_key(const std::string_view modulus, + const std::string_view exponent) -> EVP_PKEY * { + EVP_PKEY *result{nullptr}; + auto *modulus_number{ + BN_bin2bn(reinterpret_cast(modulus.data()), + static_cast(modulus.size()), nullptr)}; + auto *exponent_number{ + BN_bin2bn(reinterpret_cast(exponent.data()), + static_cast(exponent.size()), nullptr)}; + auto *builder{OSSL_PARAM_BLD_new()}; + + if (modulus_number != nullptr && exponent_number != nullptr && + builder != nullptr && + OSSL_PARAM_BLD_push_BN(builder, OSSL_PKEY_PARAM_RSA_N, modulus_number) == + 1 && + OSSL_PARAM_BLD_push_BN(builder, OSSL_PKEY_PARAM_RSA_E, exponent_number) == + 1) { + auto *parameters{OSSL_PARAM_BLD_to_param(builder)}; + if (parameters != nullptr) { + auto *context{EVP_PKEY_CTX_new_from_name(nullptr, "RSA", nullptr)}; + if (context != nullptr) { + if (EVP_PKEY_fromdata_init(context) == 1) { + EVP_PKEY_fromdata(context, &result, EVP_PKEY_PUBLIC_KEY, parameters); + } + + EVP_PKEY_CTX_free(context); + } + + OSSL_PARAM_free(parameters); + } + } + + OSSL_PARAM_BLD_free(builder); + BN_free(exponent_number); + BN_free(modulus_number); + return result; +} + +auto native_ec_key(const sourcemeta::core::EllipticCurve curve, + 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')}; + if (stripped_x.size() > width || stripped_y.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')); + + EVP_PKEY *result{nullptr}; + auto *builder{OSSL_PARAM_BLD_new()}; + if (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) { + 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_PUBLIC_KEY, parameters); + } + + EVP_PKEY_CTX_free(context); + } + + OSSL_PARAM_free(parameters); + } + } + + OSSL_PARAM_BLD_free(builder); + return result; +} + +// Convert the raw fixed-width R || S concatenation into the DER signature that +// the verification interface expects +auto encode_ecdsa_signature(const std::string_view raw_signature, + unsigned char **output) -> int { + const auto half{raw_signature.size() / 2}; + auto *signature{ECDSA_SIG_new()}; + if (signature == nullptr) { + return -1; + } + + auto *r{ + BN_bin2bn(reinterpret_cast(raw_signature.data()), + static_cast(half), nullptr)}; + auto *s{BN_bin2bn( + reinterpret_cast(raw_signature.data() + half), + static_cast(half), nullptr)}; + if (r == nullptr || s == nullptr || ECDSA_SIG_set0(signature, r, s) != 1) { + BN_free(r); + BN_free(s); + ECDSA_SIG_free(signature); + return -1; + } + + const auto length{i2d_ECDSA_SIG(signature, output)}; + ECDSA_SIG_free(signature); + return length; +} + +auto verify_rsa(EVP_PKEY *key, + const sourcemeta::core::SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature, const bool probabilistic) + -> bool { + auto result{false}; + auto *context{EVP_MD_CTX_new()}; + if (context != nullptr) { + EVP_PKEY_CTX *key_context{nullptr}; + auto ready{EVP_DigestVerifyInit(context, &key_context, + to_message_digest(hash), nullptr, + key) == 1}; + 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; + } + + if (ready) { + result = EVP_DigestVerify( + context, + reinterpret_cast(signature.data()), + signature.size(), + reinterpret_cast(message.data()), + message.size()) == 1; + } + + EVP_MD_CTX_free(context); + } + + return result; +} + +} // namespace + +namespace sourcemeta::core { + +PublicKey::PublicKey(Internal *internal) noexcept : internal_{internal} {} + +PublicKey::~PublicKey() { + if (internal_ != nullptr) { + EVP_PKEY_free(internal_->key); + delete internal_; + } +} + +PublicKey::PublicKey(PublicKey &&other) noexcept : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { + if (this != &other) { + if (internal_ != nullptr) { + EVP_PKEY_free(internal_->key); + delete internal_; + } + + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PublicKey::type() const noexcept -> Type { return internal_->kind; } + +auto make_rsa_public_key(const std::string_view modulus, + const std::string_view exponent) + -> std::optional { + auto stripped_modulus{std::string{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) { + return std::nullopt; + } + + auto *key{native_rsa_key(stripped_modulus, stripped_exponent)}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::RSA, + .key = key, + .modulus = std::move(stripped_modulus), + .field_bytes = 0, + .signature_bytes = 0}}; +} + +auto make_ec_public_key(const EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + auto *key{native_ec_key(curve, coordinate_x, coordinate_y)}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::EllipticCurve, + .key = key, + .modulus = {}, + .field_bytes = curve_field_bytes(curve), + .signature_bytes = 0}}; +} + +auto make_eddsa_public_key(const EdwardsCurve curve, + const std::string_view public_key) + -> std::optional { + if (public_key.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + auto *key{EVP_PKEY_new_raw_public_key( + to_pkey_id(curve), nullptr, + reinterpret_cast(public_key.data()), + public_key.size())}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::Edwards, + .key = key, + .modulus = {}, + .field_bytes = 0, + .signature_bytes = eddsa_signature_bytes(curve)}}; +} + +auto rsassa_pkcs1_v15_verify(const PublicKey &key, + const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_rsa(internal->key, hash, message, signature, false); +} + +auto rsassa_pss_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_rsa(internal->key, hash, message, signature, true); +} + +auto ecdsa_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::EllipticCurve || + signature.size() != internal->field_bytes * 2) { + return false; + } + + unsigned char *der_signature{nullptr}; + const auto der_length{encode_ecdsa_signature(signature, &der_signature)}; + auto result{false}; + if (der_length > 0) { + auto *context{EVP_MD_CTX_new()}; + if (context != nullptr) { + if (EVP_DigestVerifyInit(context, nullptr, to_message_digest(hash), + nullptr, internal->key) == 1) { + result = + EVP_DigestVerify( + context, der_signature, static_cast(der_length), + reinterpret_cast(message.data()), + message.size()) == 1; + } + + EVP_MD_CTX_free(context); + } + } + + OPENSSL_free(der_signature); + return result; +} + +auto eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::Edwards || + signature.size() != internal->signature_bytes) { + return false; + } + + auto result{false}; + auto *context{EVP_MD_CTX_new()}; + if (context != nullptr) { + // EdDSA is a one-shot verification with a null digest, since the curve + // fixes the hash function internally + if (EVP_DigestVerifyInit(context, nullptr, nullptr, nullptr, + internal->key) == 1) { + result = EVP_DigestVerify( + context, + reinterpret_cast(signature.data()), + signature.size(), + reinterpret_cast(message.data()), + message.size()) == 1; + } + + EVP_MD_CTX_free(context); + } + + return result; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_other.cc b/vendor/core/src/core/crypto/crypto_verify_other.cc new file mode 100644 index 000000000..c441bab43 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_other.cc @@ -0,0 +1,478 @@ +#include +#include + +#include "crypto_bignum.h" +#include "crypto_ecc.h" +#include "crypto_eddsa.h" +#include "crypto_helpers.h" + +#include // std::array +#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::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()}; + + // RFC 8017 Section 9.2 step 3: "If emLen < tLen + 11, output 'intended + // encoded message length too short'" + 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 verification (RFC 8017 Section 9.1.2), with the salt length fixed to +// the hash function output as RFC 7518 Section 3.5 requires +auto emsa_pss_verify(const SignatureHashFunction hash, + const std::string_view message, + const std::string_view encoded_message, + const std::size_t encoded_bits) -> bool { + const auto digest{digest_message(hash, message)}; + const auto hash_length{digest.size()}; + const auto salt_length{hash_length}; + const auto encoded_length{encoded_message.size()}; + + // RFC 8017 Section 9.1.2 step 3: "If emLen < hLen + sLen + 2, output + // 'inconsistent'" + if (encoded_length < hash_length + salt_length + 2) { + return false; + } + + // RFC 8017 Section 9.1.2 step 4: "If the rightmost octet of EM does not have + // hexadecimal value 0xbc, output 'inconsistent'" + if (static_cast(encoded_message.back()) != 0xbc) { + return false; + } + + const auto database_length{encoded_length - hash_length - 1}; + const auto masked_database{encoded_message.substr(0, database_length)}; + const auto hash_value{encoded_message.substr(database_length, hash_length)}; + + // RFC 8017 Section 9.1.2 step 6: "If the leftmost 8emLen - emBits bits of the + // leftmost octet in maskedDB are not all equal to zero, output + // 'inconsistent'" + const auto unused_bits{(8 * encoded_length) - encoded_bits}; + const auto unused_mask{ + static_cast((0xff00u >> unused_bits) & 0xffu)}; + if ((static_cast(masked_database.front()) & unused_mask) != 0) { + return false; + } + + auto database{mask_generation(hash, hash_value, database_length)}; + for (std::size_t index = 0; index < database_length; ++index) { + database[index] = + static_cast(database[index] ^ masked_database[index]); + } + + database[0] = static_cast(static_cast(database[0]) & + static_cast(~unused_mask)); + + // RFC 8017 Section 9.1.2 step 10: "If the emLen - hLen - sLen - 2 leftmost + // octets of DB are not zero or if the octet at position emLen - hLen - sLen - + // 1 does not have hexadecimal value 0x01, output 'inconsistent'" + const auto padding_length{encoded_length - hash_length - salt_length - 2}; + for (std::size_t index = 0; index < padding_length; ++index) { + if (database[index] != '\x00') { + return false; + } + } + + if (static_cast(database[padding_length]) != 0x01) { + return false; + } + + // RFC 8017 Section 9.1.2 steps 12 and 13: hash the concatenation of eight + // zero octets, the message digest, and the recovered salt + std::string verification_input(8, '\x00'); + verification_input.append(digest); + verification_input.append(database.substr(database_length - salt_length)); + const auto expected{digest_message(hash, verification_input)}; + return expected == hash_value; +} + +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(); +} + +// FIPS 186-4 Section 6.4 step 2, deriving the integer e from the leftmost bits +// of the message digest, truncated to the bit length of the order +auto digest_to_integer(const SignatureHashFunction hash, + const std::string_view message, + const std::size_t order_bits) -> Bignum { + const auto digest{digest_message(hash, message)}; + auto value{bignum_from_bytes(digest)}; + const auto digest_bits{digest.size() * 8}; + if (digest_bits > order_bits) { + value = bignum_shift_right(value, digest_bits - order_bits); + } + + return value; +} + +// RSASSA-PKCS1-v1_5 verification (RFC 8017 Section 8.2.2) over raw key material +auto verify_pkcs1(const SignatureHashFunction hash, + const std::string_view modulus, + const std::string_view exponent, + const std::string_view message, + const std::string_view signature) -> bool { + // RFC 8017 Section 8.2.2 step 1: "If the length of S is not k octets, output + // 'invalid signature'" + const auto key_length{modulus.size()}; + if (signature.size() != key_length) { + return false; + } + + const auto modulus_number{bignum_from_bytes(modulus)}; + const auto signature_number{bignum_from_bytes(signature)}; + + // RFC 8017 Section 5.2.2: "If the signature representative s is not between 0 + // and n - 1, output 'signature representative out of range'" + if (bignum_compare(signature_number, modulus_number) >= 0) { + return false; + } + + const auto exponent_number{bignum_from_bytes(exponent)}; + const auto message_representative{ + bignum_mod_exp(signature_number, exponent_number, modulus_number)}; + const auto encoded_message{ + bignum_to_bytes(message_representative, key_length)}; + const auto expected{build_encoded_message(hash, message, key_length)}; + return expected.has_value() && encoded_message == expected.value(); +} + +// RSASSA-PSS verification (RFC 8017 Section 8.1.2) over raw key material +auto verify_pss(const SignatureHashFunction hash, + const std::string_view modulus, const std::string_view exponent, + const std::string_view message, + const std::string_view signature) -> bool { + // RFC 8017 Section 8.1.2 step 1: "If the length of the signature S is not k + // octets, output 'invalid signature'" + const auto key_length{modulus.size()}; + if (signature.size() != key_length) { + return false; + } + + const auto modulus_number{bignum_from_bytes(modulus)}; + const auto signature_number{bignum_from_bytes(signature)}; + + // RFC 8017 Section 5.2.2: "If the signature representative s is not between 0 + // and n - 1, output 'signature representative out of range'" + if (bignum_compare(signature_number, modulus_number) >= 0) { + return false; + } + + const auto exponent_number{bignum_from_bytes(exponent)}; + const auto message_representative{ + bignum_mod_exp(signature_number, exponent_number, modulus_number)}; + + // RFC 8017 Section 8.1.2 step 2c: the encoded message is emLen octets long, + // where emLen equals the byte length of emBits = modBits - 1 bits, which is + // one octet less than k when the modulus bit length is congruent to one + // modulo eight + const auto encoded_bits{bignum_bit_length(modulus_number) - 1}; + const auto encoded_length{(encoded_bits + 7) / 8}; + const auto full_representative{ + bignum_to_bytes(message_representative, key_length)}; + for (std::size_t index = 0; index < key_length - encoded_length; ++index) { + if (full_representative[index] != '\x00') { + return false; + } + } + + const auto encoded_message{std::string_view{full_representative}.substr( + key_length - encoded_length)}; + return emsa_pss_verify(hash, message, encoded_message, encoded_bits); +} + +// ECDSA verification (FIPS 186-4 Section 6.4) over the raw public point +auto verify_ecdsa(const EllipticCurve curve, const SignatureHashFunction hash, + const std::string_view coordinate_x, + const std::string_view coordinate_y, + const std::string_view message, + const std::string_view signature) -> bool { + const auto parameters{to_curve_parameters(curve)}; + const auto field_bytes{parameters.field_bytes}; + + // RFC 7518 Section 3.4: the signature is the fixed-width concatenation of the + // two integers, each as long as the curve field + if (signature.size() != field_bytes * 2) { + return false; + } + + const auto r{bignum_from_bytes(signature.substr(0, field_bytes))}; + const auto s{bignum_from_bytes(signature.substr(field_bytes))}; + + // FIPS 186-4 Section 6.4.2 step 1: both integers must lie in [1, n - 1] + if (bignum_is_zero(r) || bignum_compare(r, parameters.order) >= 0 || + bignum_is_zero(s) || bignum_compare(s, parameters.order) >= 0) { + return false; + } + + // Reject coordinates wider than the field, matching the platform backends and + // preventing an oversized input from being truncated into a valid key + const auto stripped_x{strip_left(coordinate_x, '\x00')}; + const auto stripped_y{strip_left(coordinate_y, '\x00')}; + if (stripped_x.size() > field_bytes || stripped_y.size() > field_bytes) { + return false; + } + + const auto public_x{bignum_from_bytes(stripped_x)}; + const auto public_y{bignum_from_bytes(stripped_y)}; + + // The public key must be a valid point: coordinates below the field prime and + // satisfying the curve equation + if (bignum_compare(public_x, parameters.prime) >= 0 || + bignum_compare(public_y, parameters.prime) >= 0 || + !point_on_curve(public_x, public_y, parameters)) { + return false; + } + + const auto order_bits{bignum_bit_length(parameters.order)}; + const auto digest_integer{digest_to_integer(hash, message, order_bits)}; + const auto s_inverse{bignum_mod_inverse(s, parameters.order)}; + const auto u1{ + bignum_mod_multiply(digest_integer, s_inverse, parameters.order)}; + const auto u2{bignum_mod_multiply(r, s_inverse, parameters.order)}; + + const JacobianPoint generator{.x = parameters.generator_x, + .y = parameters.generator_y, + .z = bignum_from_u64(1)}; + const JacobianPoint public_point{ + .x = public_x, .y = public_y, .z = bignum_from_u64(1)}; + const auto point{point_double_scalar_multiply(u1, generator, u2, public_point, + parameters)}; + + // FIPS 186-4 Section 6.4.2 step 6: reject when the combination is the point + // at infinity + if (point_is_infinity(point)) { + return false; + } + + // FIPS 186-4 Section 6.4.2 step 7: the signature is valid when the affine x + // coordinate, reduced modulo the order, equals r + auto candidate{point_affine_x(point, parameters)}; + bignum_reduce(candidate, parameters.order); + return bignum_compare(candidate, r) == 0; +} + +} // namespace + +// The reference backend parses the key material into big integers inside each +// verification, which is cheap next to the modular arithmetic, so the parsed +// key simply holds the raw material +struct PublicKey::Internal { + PublicKey::Type kind; + std::string modulus; + std::string exponent; + std::string coordinate_x; + std::string coordinate_y; + EllipticCurve elliptic_curve; + EdwardsCurve edwards_curve; +}; + +PublicKey::PublicKey(Internal *internal) noexcept : internal_{internal} {} + +PublicKey::~PublicKey() { delete internal_; } + +PublicKey::PublicKey(PublicKey &&other) noexcept : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { + if (this != &other) { + delete internal_; + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PublicKey::type() const noexcept -> Type { 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) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::RSA, + .modulus = std::string{stripped_modulus}, + .exponent = std::string{stripped_exponent}, + .coordinate_x = {}, + .coordinate_y = {}, + .elliptic_curve = {}, + .edwards_curve = {}}}; +} + +auto make_ec_public_key(const EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + const auto width{curve_field_bytes(curve)}; + const auto stripped_x{strip_left(coordinate_x, '\x00')}; + const auto stripped_y{strip_left(coordinate_y, '\x00')}; + if (stripped_x.size() > width || stripped_y.size() > width) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::EllipticCurve, + .modulus = {}, + .exponent = {}, + .coordinate_x = std::string{stripped_x}, + .coordinate_y = std::string{stripped_y}, + .elliptic_curve = curve, + .edwards_curve = {}}}; +} + +auto make_eddsa_public_key(const EdwardsCurve curve, + const std::string_view public_key) + -> std::optional { + if (public_key.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::Edwards, + .modulus = {}, + .exponent = {}, + .coordinate_x = std::string{public_key}, + .coordinate_y = {}, + .elliptic_curve = {}, + .edwards_curve = curve}}; +} + +auto rsassa_pkcs1_v15_verify(const PublicKey &key, + const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + return internal != nullptr && internal->kind == PublicKey::Type::RSA && + verify_pkcs1(hash, internal->modulus, internal->exponent, message, + signature); +} + +auto rsassa_pss_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + return internal != nullptr && internal->kind == PublicKey::Type::RSA && + verify_pss(hash, internal->modulus, internal->exponent, message, + signature); +} + +auto ecdsa_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + return internal != nullptr && + internal->kind == PublicKey::Type::EllipticCurve && + verify_ecdsa(internal->elliptic_curve, hash, internal->coordinate_x, + internal->coordinate_y, message, signature); +} + +auto eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::Edwards) { + return false; + } + + switch (internal->edwards_curve) { + case EdwardsCurve::Ed25519: + return edwards25519_verify(internal->coordinate_x, message, signature); + case EdwardsCurve::Ed448: + return edwards448_verify(internal->coordinate_x, message, signature); + } + + std::unreachable(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_rsa_apple.cc b/vendor/core/src/core/crypto/crypto_verify_rsa_apple.cc deleted file mode 100644 index 6b58acffd..000000000 --- a/vendor/core/src/core/crypto/crypto_verify_rsa_apple.cc +++ /dev/null @@ -1,150 +0,0 @@ -#include -#include - -#include "crypto_helpers.h" - -#include // CF*, kCF* -#include // Sec*, kSec* - -#include // std::array -#include // std::string -#include // std::string_view -#include // std::unreachable - -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 make_data(const std::string_view value) -> CFDataRef { - return CFDataCreate(kCFAllocatorDefault, - reinterpret_cast(value.data()), - static_cast(value.size())); -} - -auto make_rsa_public_key(const std::string_view modulus, - const std::string_view exponent) -> SecKeyRef { - // The platform expects the PKCS#1 RSAPublicKey structure, a DER sequence - // of the modulus and exponent integers (RFC 8017 Appendix A.1.1) - std::string body; - sourcemeta::core::der_append_unsigned_integer(body, modulus); - sourcemeta::core::der_append_unsigned_integer(body, exponent); - std::string der; - der.push_back('\x30'); - sourcemeta::core::der_append_length(der, body.size()); - der.append(body); - - auto key_data{make_data(der)}; - if (key_data == nullptr) { - return nullptr; - } - - std::array attribute_keys{ - {kSecAttrKeyType, kSecAttrKeyClass}}; - std::array attribute_values{ - {kSecAttrKeyTypeRSA, kSecAttrKeyClassPublic}}; - 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{SecKeyCreateWithData(key_data, attributes, nullptr)}; - CFRelease(attributes); - CFRelease(key_data); - return key; -} - -auto verify_rsa_signature(const SecKeyAlgorithm algorithm, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - const auto stripped_modulus{sourcemeta::core::strip_left(modulus, '\x00')}; - const auto stripped_exponent{sourcemeta::core::strip_left(exponent, '\x00')}; - if (stripped_modulus.empty() || stripped_exponent.empty() || - stripped_modulus.size() > sourcemeta::core::MAXIMUM_KEY_BYTES || - stripped_exponent.size() > sourcemeta::core::MAXIMUM_KEY_BYTES) { - return false; - } - - auto key{make_rsa_public_key(stripped_modulus, stripped_exponent)}; - if (key == nullptr) { - return false; - } - - auto message_data{make_data(message)}; - auto signature_data{make_data(signature)}; - auto result{false}; - if (message_data != nullptr && signature_data != nullptr) { - result = SecKeyVerifySignature(key, algorithm, message_data, signature_data, - nullptr) == true; - } - - if (signature_data != nullptr) { - CFRelease(signature_data); - } - - if (message_data != nullptr) { - CFRelease(message_data); - } - - CFRelease(key); - return result; -} - -} // namespace - -namespace sourcemeta::core { - -auto rsassa_pkcs1_v15_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - return verify_rsa_signature(to_sec_key_pkcs1_v15_algorithm(hash), modulus, - exponent, message, signature); -} - -auto rsassa_pss_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - return verify_rsa_signature(to_sec_key_pss_algorithm(hash), modulus, exponent, - message, signature); -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_rsa_openssl.cc b/vendor/core/src/core/crypto/crypto_verify_rsa_openssl.cc deleted file mode 100644 index 52e555113..000000000 --- a/vendor/core/src/core/crypto/crypto_verify_rsa_openssl.cc +++ /dev/null @@ -1,144 +0,0 @@ -#include -#include - -#include "crypto_helpers.h" - -#include // BN_* -#include // OSSL_PKEY_PARAM_* -#include // EVP_* -#include // OSSL_PARAM_* -#include // RSA_PKCS1_PSS_PADDING, RSA_PSS_SALTLEN_DIGEST - -#include // std::string_view -#include // std::unreachable - -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(); -} - -auto make_rsa_public_key(const std::string_view modulus, - const std::string_view exponent) -> EVP_PKEY * { - EVP_PKEY *result{nullptr}; - auto *modulus_number{ - BN_bin2bn(reinterpret_cast(modulus.data()), - static_cast(modulus.size()), nullptr)}; - auto *exponent_number{ - BN_bin2bn(reinterpret_cast(exponent.data()), - static_cast(exponent.size()), nullptr)}; - auto *builder{OSSL_PARAM_BLD_new()}; - - if (modulus_number != nullptr && exponent_number != nullptr && - builder != nullptr && - OSSL_PARAM_BLD_push_BN(builder, OSSL_PKEY_PARAM_RSA_N, modulus_number) == - 1 && - OSSL_PARAM_BLD_push_BN(builder, OSSL_PKEY_PARAM_RSA_E, exponent_number) == - 1) { - auto *parameters{OSSL_PARAM_BLD_to_param(builder)}; - if (parameters != nullptr) { - auto *context{EVP_PKEY_CTX_new_from_name(nullptr, "RSA", nullptr)}; - if (context != nullptr) { - if (EVP_PKEY_fromdata_init(context) == 1) { - EVP_PKEY_fromdata(context, &result, EVP_PKEY_PUBLIC_KEY, parameters); - } - - EVP_PKEY_CTX_free(context); - } - - OSSL_PARAM_free(parameters); - } - } - - OSSL_PARAM_BLD_free(builder); - BN_free(exponent_number); - BN_free(modulus_number); - return result; -} - -auto verify_rsa_signature(const sourcemeta::core::SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature, - const bool probabilistic) -> bool { - const auto stripped_modulus{sourcemeta::core::strip_left(modulus, '\x00')}; - const auto stripped_exponent{sourcemeta::core::strip_left(exponent, '\x00')}; - if (stripped_modulus.empty() || stripped_exponent.empty() || - stripped_modulus.size() > sourcemeta::core::MAXIMUM_KEY_BYTES || - stripped_exponent.size() > sourcemeta::core::MAXIMUM_KEY_BYTES) { - return false; - } - - auto *key{make_rsa_public_key(stripped_modulus, stripped_exponent)}; - if (key == nullptr) { - return false; - } - - auto result{false}; - auto *context{EVP_MD_CTX_new()}; - if (context != nullptr) { - EVP_PKEY_CTX *key_context{nullptr}; - auto ready{EVP_DigestVerifyInit(context, &key_context, - to_message_digest(hash), nullptr, - key) == 1}; - if (ready && probabilistic) { - // Requesting the digest-length salt that RFC 7518 Section 3.5 - // requires makes verification reject signatures carrying any other - // salt length - 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; - } - - if (ready) { - result = EVP_DigestVerify( - context, - reinterpret_cast(signature.data()), - signature.size(), - reinterpret_cast(message.data()), - message.size()) == 1; - } - - EVP_MD_CTX_free(context); - } - - EVP_PKEY_free(key); - return result; -} - -} // namespace - -namespace sourcemeta::core { - -auto rsassa_pkcs1_v15_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - return verify_rsa_signature(hash, modulus, exponent, message, signature, - false); -} - -auto rsassa_pss_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - return verify_rsa_signature(hash, modulus, exponent, message, signature, - true); -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_rsa_other.cc b/vendor/core/src/core/crypto/crypto_verify_rsa_other.cc deleted file mode 100644 index e70682ec9..000000000 --- a/vendor/core/src/core/crypto/crypto_verify_rsa_other.cc +++ /dev/null @@ -1,258 +0,0 @@ -#include -#include - -#include "crypto_bignum.h" -#include "crypto_helpers.h" - -#include // std::array -#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::unreachable - -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 sourcemeta::core::SignatureHashFunction hash) - -> std::string_view { - switch (hash) { - case sourcemeta::core::SignatureHashFunction::SHA256: - return {reinterpret_cast(DIGEST_INFO_SHA256.data()), - DIGEST_INFO_SHA256.size()}; - case sourcemeta::core::SignatureHashFunction::SHA384: - return {reinterpret_cast(DIGEST_INFO_SHA384.data()), - DIGEST_INFO_SHA384.size()}; - case sourcemeta::core::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 sourcemeta::core::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{sourcemeta::core::digest_message(hash, message)}; - const auto encoded_length{prefix.size() + digest.size()}; - - // RFC 8017 Section 9.2 step 3: "If emLen < tLen + 11, output 'intended - // encoded message length too short'" - 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 sourcemeta::core::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(sourcemeta::core::digest_message(hash, block)); - counter += 1; - } - - result.resize(length); - return result; -} - -// EMSA-PSS verification (RFC 8017 Section 9.1.2), with the salt length -// fixed to the hash function output as RFC 7518 Section 3.5 requires -auto emsa_pss_verify(const sourcemeta::core::SignatureHashFunction hash, - const std::string_view message, - const std::string_view encoded_message, - const std::size_t encoded_bits) -> bool { - const auto digest{sourcemeta::core::digest_message(hash, message)}; - const auto hash_length{digest.size()}; - const auto salt_length{hash_length}; - const auto encoded_length{encoded_message.size()}; - - // RFC 8017 Section 9.1.2 step 3: "If emLen < hLen + sLen + 2, output - // 'inconsistent'" - if (encoded_length < hash_length + salt_length + 2) { - return false; - } - - // RFC 8017 Section 9.1.2 step 4: "If the rightmost octet of EM does not - // have hexadecimal value 0xbc, output 'inconsistent'" - if (static_cast(encoded_message.back()) != 0xbc) { - return false; - } - - const auto database_length{encoded_length - hash_length - 1}; - const auto masked_database{encoded_message.substr(0, database_length)}; - const auto hash_value{encoded_message.substr(database_length, hash_length)}; - - // RFC 8017 Section 9.1.2 step 6: "If the leftmost 8emLen - emBits bits of - // the leftmost octet in maskedDB are not all equal to zero, output - // 'inconsistent'" - const auto unused_bits{(8 * encoded_length) - encoded_bits}; - const auto unused_mask{ - static_cast((0xff00u >> unused_bits) & 0xffu)}; - if ((static_cast(masked_database.front()) & unused_mask) != 0) { - return false; - } - - auto database{mask_generation(hash, hash_value, database_length)}; - for (std::size_t index = 0; index < database_length; ++index) { - database[index] = - static_cast(database[index] ^ masked_database[index]); - } - - database[0] = static_cast(static_cast(database[0]) & - static_cast(~unused_mask)); - - // RFC 8017 Section 9.1.2 step 10: "If the emLen - hLen - sLen - 2 - // leftmost octets of DB are not zero or if the octet at position - // emLen - hLen - sLen - 1 does not have hexadecimal value 0x01, output - // 'inconsistent'" - const auto padding_length{encoded_length - hash_length - salt_length - 2}; - for (std::size_t index = 0; index < padding_length; ++index) { - if (database[index] != '\x00') { - return false; - } - } - - if (static_cast(database[padding_length]) != 0x01) { - return false; - } - - // RFC 8017 Section 9.1.2 steps 12 and 13: hash the concatenation of eight - // zero octets, the message digest, and the recovered salt - std::string verification_input(8, '\x00'); - verification_input.append(digest); - verification_input.append(database.substr(database_length - salt_length)); - const auto expected{ - sourcemeta::core::digest_message(hash, verification_input)}; - return expected == hash_value; -} - -} // namespace - -namespace sourcemeta::core { - -auto rsassa_pkcs1_v15_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - const auto stripped_modulus{sourcemeta::core::strip_left(modulus, '\x00')}; - const auto stripped_exponent{sourcemeta::core::strip_left(exponent, '\x00')}; - if (stripped_modulus.empty() || stripped_exponent.empty() || - stripped_modulus.size() > sourcemeta::core::MAXIMUM_KEY_BYTES || - stripped_exponent.size() > sourcemeta::core::MAXIMUM_KEY_BYTES) { - return false; - } - - // RFC 8017 Section 8.2.2 step 1: "If the length of S is not k octets, - // output 'invalid signature'" - const auto key_length{stripped_modulus.size()}; - if (signature.size() != key_length) { - return false; - } - - const auto modulus_number{bignum_from_bytes(stripped_modulus)}; - const auto signature_number{bignum_from_bytes(signature)}; - - // RFC 8017 Section 5.2.2: "If the signature representative s is not - // between 0 and n - 1, output 'signature representative out of range'" - if (bignum_compare(signature_number, modulus_number) >= 0) { - return false; - } - - const auto exponent_number{bignum_from_bytes(stripped_exponent)}; - const auto message_representative{ - bignum_mod_exp(signature_number, exponent_number, modulus_number)}; - const auto encoded_message{ - bignum_to_bytes(message_representative, key_length)}; - const auto expected{build_encoded_message(hash, message, key_length)}; - return expected.has_value() && encoded_message == expected.value(); -} - -auto rsassa_pss_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - const auto stripped_modulus{sourcemeta::core::strip_left(modulus, '\x00')}; - const auto stripped_exponent{sourcemeta::core::strip_left(exponent, '\x00')}; - if (stripped_modulus.empty() || stripped_exponent.empty() || - stripped_modulus.size() > sourcemeta::core::MAXIMUM_KEY_BYTES || - stripped_exponent.size() > sourcemeta::core::MAXIMUM_KEY_BYTES) { - return false; - } - - // RFC 8017 Section 8.1.2 step 1: "If the length of the signature S is not - // k octets, output 'invalid signature'" - const auto key_length{stripped_modulus.size()}; - if (signature.size() != key_length) { - return false; - } - - const auto modulus_number{bignum_from_bytes(stripped_modulus)}; - const auto signature_number{bignum_from_bytes(signature)}; - - // RFC 8017 Section 5.2.2: "If the signature representative s is not - // between 0 and n - 1, output 'signature representative out of range'" - if (bignum_compare(signature_number, modulus_number) >= 0) { - return false; - } - - const auto exponent_number{bignum_from_bytes(stripped_exponent)}; - const auto message_representative{ - bignum_mod_exp(signature_number, exponent_number, modulus_number)}; - - // RFC 8017 Section 8.1.2 step 2c: the encoded message is emLen octets - // long, where emLen equals the byte length of emBits = modBits - 1 bits, - // which is one octet less than k when the modulus bit length is congruent - // to one modulo eight - const auto encoded_bits{bignum_bit_length(modulus_number) - 1}; - const auto encoded_length{(encoded_bits + 7) / 8}; - const auto full_representative{ - bignum_to_bytes(message_representative, key_length)}; - for (std::size_t index = 0; index < key_length - encoded_length; ++index) { - if (full_representative[index] != '\x00') { - return false; - } - } - - const auto encoded_message{std::string_view{full_representative}.substr( - key_length - encoded_length)}; - return emsa_pss_verify(hash, message, encoded_message, encoded_bits); -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_rsa_windows.cc b/vendor/core/src/core/crypto/crypto_verify_rsa_windows.cc deleted file mode 100644 index 8b6379649..000000000 --- a/vendor/core/src/core/crypto/crypto_verify_rsa_windows.cc +++ /dev/null @@ -1,137 +0,0 @@ -#include -#include - -#include "crypto_helpers.h" - -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#include // ULONG, LPCWSTR - -#include // BCrypt*, BCRYPT_* - -#include // std::countl_zero -#include // std::size_t -#include // std::uint8_t -#include // std::memcpy -#include // std::string -#include // std::string_view -#include // std::unreachable - -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 verify_rsa_signature(const sourcemeta::core::SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature, - const bool probabilistic) -> bool { - const auto stripped_modulus{sourcemeta::core::strip_left(modulus, '\x00')}; - const auto stripped_exponent{sourcemeta::core::strip_left(exponent, '\x00')}; - if (stripped_modulus.empty() || stripped_exponent.empty() || - stripped_modulus.size() > sourcemeta::core::MAXIMUM_KEY_BYTES || - stripped_exponent.size() > sourcemeta::core::MAXIMUM_KEY_BYTES) { - return false; - } - - const auto modulus_bit_length{ - (stripped_modulus.size() * 8u) - - static_cast(std::countl_zero( - static_cast(stripped_modulus.front())))}; - - BCRYPT_RSAKEY_BLOB header{}; - header.Magic = BCRYPT_RSAPUBLIC_MAGIC; - header.BitLength = static_cast(modulus_bit_length); - header.cbPublicExp = static_cast(stripped_exponent.size()); - header.cbModulus = static_cast(stripped_modulus.size()); - header.cbPrime1 = 0; - header.cbPrime2 = 0; - - std::string blob; - blob.resize(sizeof(header)); - std::memcpy(blob.data(), &header, sizeof(header)); - blob.append(stripped_exponent); - blob.append(stripped_modulus); - - BCRYPT_ALG_HANDLE algorithm{nullptr}; - if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( - &algorithm, BCRYPT_RSA_ALGORITHM, nullptr, 0))) { - return false; - } - - BCRYPT_KEY_HANDLE key{nullptr}; - if (!BCRYPT_SUCCESS( - BCryptImportKeyPair(algorithm, nullptr, BCRYPT_RSAPUBLIC_BLOB, &key, - reinterpret_cast(blob.data()), - static_cast(blob.size()), 0))) { - BCryptCloseAlgorithmProvider(algorithm, 0); - return false; - } - - const auto digest{sourcemeta::core::digest_message(hash, message)}; - - // The signature parameter is not const-qualified but is input only - auto result{false}; - if (probabilistic) { - // The digest-length salt is what RFC 7518 Section 3.5 requires - BCRYPT_PSS_PADDING_INFO padding{}; - padding.pszAlgId = to_cng_algorithm(hash); - padding.cbSalt = static_cast(digest.size()); - result = BCRYPT_SUCCESS(BCryptVerifySignature( - key, &padding, - reinterpret_cast(const_cast(digest.data())), - static_cast(digest.size()), - reinterpret_cast(const_cast(signature.data())), - static_cast(signature.size()), BCRYPT_PAD_PSS)); - } else { - BCRYPT_PKCS1_PADDING_INFO padding{}; - padding.pszAlgId = to_cng_algorithm(hash); - result = BCRYPT_SUCCESS(BCryptVerifySignature( - key, &padding, - reinterpret_cast(const_cast(digest.data())), - static_cast(digest.size()), - reinterpret_cast(const_cast(signature.data())), - static_cast(signature.size()), BCRYPT_PAD_PKCS1)); - } - - BCryptDestroyKey(key); - BCryptCloseAlgorithmProvider(algorithm, 0); - return result; -} - -} // namespace - -namespace sourcemeta::core { - -auto rsassa_pkcs1_v15_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - return verify_rsa_signature(hash, modulus, exponent, message, signature, - false); -} - -auto rsassa_pss_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - return verify_rsa_signature(hash, modulus, exponent, message, signature, - true); -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_windows.cc b/vendor/core/src/core/crypto/crypto_verify_windows.cc new file mode 100644 index 000000000..0e41c9e1b --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_windows.cc @@ -0,0 +1,367 @@ +#include +#include + +#include "crypto_eddsa.h" +#include "crypto_helpers.h" + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG, LPCWSTR + +#include // BCrypt*, BCRYPT_* + +#include // std::countl_zero +#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 encoded point and verify through the reference implementation +struct PublicKey::Internal { + PublicKey::Type kind; + BCRYPT_ALG_HANDLE algorithm; + BCRYPT_KEY_HANDLE key; + std::size_t field_bytes; + std::string modulus; + std::string edwards_point; + EdwardsCurve edwards_curve; +}; + +} // 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_public_magic(const sourcemeta::core::EllipticCurve curve) noexcept + -> ULONG { + switch (curve) { + case sourcemeta::core::EllipticCurve::P256: + return BCRYPT_ECDSA_PUBLIC_P256_MAGIC; + case sourcemeta::core::EllipticCurve::P384: + return BCRYPT_ECDSA_PUBLIC_P384_MAGIC; + case sourcemeta::core::EllipticCurve::P521: + return BCRYPT_ECDSA_PUBLIC_P521_MAGIC; + } + + std::unreachable(); +} + +struct KeyPair { + BCRYPT_ALG_HANDLE algorithm; + BCRYPT_KEY_HANDLE key; +}; + +auto native_rsa_key(const std::string_view modulus, + const std::string_view exponent) -> KeyPair { + const auto modulus_bit_length{ + (modulus.size() * 8u) - static_cast(std::countl_zero( + static_cast(modulus.front())))}; + + BCRYPT_RSAKEY_BLOB header{}; + header.Magic = BCRYPT_RSAPUBLIC_MAGIC; + header.BitLength = static_cast(modulus_bit_length); + header.cbPublicExp = static_cast(exponent.size()); + header.cbModulus = static_cast(modulus.size()); + header.cbPrime1 = 0; + header.cbPrime2 = 0; + + std::string blob; + blob.resize(sizeof(header)); + std::memcpy(blob.data(), &header, sizeof(header)); + blob.append(exponent); + blob.append(modulus); + + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( + &algorithm, BCRYPT_RSA_ALGORITHM, nullptr, 0))) { + return {.algorithm = nullptr, .key = nullptr}; + } + + BCRYPT_KEY_HANDLE key{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptImportKeyPair(algorithm, nullptr, BCRYPT_RSAPUBLIC_BLOB, &key, + reinterpret_cast(blob.data()), + static_cast(blob.size()), 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + return {.algorithm = nullptr, .key = nullptr}; + } + + return {.algorithm = algorithm, .key = key}; +} + +auto native_ec_key(const sourcemeta::core::EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y, const std::size_t width) + -> KeyPair { + BCRYPT_ECCKEY_BLOB header{}; + header.dwMagic = to_ecc_public_magic(curve); + header.cbKey = static_cast(width); + + std::string blob; + blob.resize(sizeof(header)); + std::memcpy(blob.data(), &header, sizeof(header)); + blob.append(sourcemeta::core::pad_left(coordinate_x, width, '\x00')); + blob.append(sourcemeta::core::pad_left(coordinate_y, width, '\x00')); + + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( + &algorithm, to_ecdsa_algorithm(curve), nullptr, 0))) { + return {.algorithm = nullptr, .key = nullptr}; + } + + BCRYPT_KEY_HANDLE key{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptImportKeyPair(algorithm, nullptr, BCRYPT_ECCPUBLIC_BLOB, &key, + reinterpret_cast(blob.data()), + static_cast(blob.size()), 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + return {.algorithm = nullptr, .key = nullptr}; + } + + return {.algorithm = algorithm, .key = key}; +} + +auto verify_rsa(BCRYPT_KEY_HANDLE key, + const sourcemeta::core::SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature, const bool probabilistic) + -> bool { + const auto digest{sourcemeta::core::digest_message(hash, message)}; + + if (probabilistic) { + // The digest-length salt is what RFC 7518 Section 3.5 requires + BCRYPT_PSS_PADDING_INFO padding{}; + padding.pszAlgId = to_cng_algorithm(hash); + padding.cbSalt = static_cast(digest.size()); + return BCRYPT_SUCCESS(BCryptVerifySignature( + key, &padding, + reinterpret_cast(const_cast(digest.data())), + static_cast(digest.size()), + reinterpret_cast(const_cast(signature.data())), + static_cast(signature.size()), BCRYPT_PAD_PSS)); + } + + BCRYPT_PKCS1_PADDING_INFO padding{}; + padding.pszAlgId = to_cng_algorithm(hash); + return BCRYPT_SUCCESS(BCryptVerifySignature( + key, &padding, + reinterpret_cast(const_cast(digest.data())), + static_cast(digest.size()), + reinterpret_cast(const_cast(signature.data())), + static_cast(signature.size()), BCRYPT_PAD_PKCS1)); +} + +} // namespace + +namespace sourcemeta::core { + +PublicKey::PublicKey(Internal *internal) noexcept : internal_{internal} {} + +PublicKey::~PublicKey() { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + BCryptDestroyKey(internal_->key); + } + + if (internal_->algorithm != nullptr) { + BCryptCloseAlgorithmProvider(internal_->algorithm, 0); + } + + delete internal_; + } +} + +PublicKey::PublicKey(PublicKey &&other) noexcept : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { + if (this != &other) { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + BCryptDestroyKey(internal_->key); + } + + if (internal_->algorithm != nullptr) { + BCryptCloseAlgorithmProvider(internal_->algorithm, 0); + } + + delete internal_; + } + + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PublicKey::type() const noexcept -> Type { return internal_->kind; } + +auto make_rsa_public_key(const std::string_view modulus, + const std::string_view exponent) + -> std::optional { + auto stripped_modulus{std::string{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) { + return std::nullopt; + } + + const auto pair{native_rsa_key(stripped_modulus, stripped_exponent)}; + if (pair.key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::RSA, + .algorithm = pair.algorithm, + .key = pair.key, + .field_bytes = 0, + .modulus = std::move(stripped_modulus), + .edwards_point = {}, + .edwards_curve = {}}}; +} + +auto make_ec_public_key(const EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + const auto width{curve_field_bytes(curve)}; + const auto stripped_x{strip_left(coordinate_x, '\x00')}; + const auto stripped_y{strip_left(coordinate_y, '\x00')}; + if (stripped_x.size() > width || stripped_y.size() > width) { + return std::nullopt; + } + + const auto pair{native_ec_key(curve, stripped_x, stripped_y, width)}; + if (pair.key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::EllipticCurve, + .algorithm = pair.algorithm, + .key = pair.key, + .field_bytes = width, + .modulus = {}, + .edwards_point = {}, + .edwards_curve = {}}}; +} + +auto make_eddsa_public_key(const EdwardsCurve curve, + const std::string_view public_key) + -> std::optional { + if (public_key.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::Edwards, + .algorithm = nullptr, + .key = nullptr, + .field_bytes = 0, + .modulus = {}, + .edwards_point = std::string{public_key}, + .edwards_curve = curve}}; +} + +auto rsassa_pkcs1_v15_verify(const PublicKey &key, + const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_rsa(internal->key, hash, message, signature, false); +} + +auto rsassa_pss_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_rsa(internal->key, hash, message, signature, true); +} + +auto ecdsa_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::EllipticCurve || + signature.size() != internal->field_bytes * 2) { + return false; + } + + const auto digest{digest_message(hash, message)}; + + // The CNG signature format is the raw fixed-width R || S concatenation, so + // the input passes through unchanged + return BCRYPT_SUCCESS(BCryptVerifySignature( + internal->key, nullptr, + reinterpret_cast(const_cast(digest.data())), + static_cast(digest.size()), + reinterpret_cast(const_cast(signature.data())), + static_cast(signature.size()), 0)); +} + +auto eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::Edwards) { + return false; + } + + switch (internal->edwards_curve) { + case EdwardsCurve::Ed25519: + return edwards25519_verify(internal->edwards_point, message, signature); + case EdwardsCurve::Ed448: + return edwards448_verify(internal->edwards_point, message, signature); + } + + std::unreachable(); +} + +} // namespace sourcemeta::core 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 6a033bef3..38cc7ae35 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 @@ -6,6 +6,7 @@ #endif #include // std::uint8_t +#include // std::optional #include // std::string_view namespace sourcemeta::core { @@ -18,67 +19,156 @@ enum class SignatureHashFunction : std::uint8_t { SHA256, SHA384, SHA512 }; /// The NIST elliptic curves supported by signature verification. enum class EllipticCurve : std::uint8_t { P256, P384, P521 }; +/// @ingroup crypto +/// The Edwards curves supported by signature verification. +enum class EdwardsCurve : std::uint8_t { Ed25519, Ed448 }; + +/// @ingroup crypto +/// A parsed public key that holds the native key, so that the same key can +/// verify many signatures without paying the key construction cost on every +/// call. Build it once with one of the factory functions and pass it to the +/// matching verification function. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::make_rsa_public_key(modulus, exponent)}; +/// assert(key.has_value()); +/// assert(sourcemeta::core::rsassa_pkcs1_v15_verify( +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, message, +/// signature)); +/// ``` +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 }; + + ~PublicKey(); + PublicKey(PublicKey &&other) noexcept; + auto operator=(PublicKey &&other) noexcept -> PublicKey &; + PublicKey(const PublicKey &) = delete; + auto operator=(const PublicKey &) -> PublicKey & = 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 functions below + explicit PublicKey(Internal *internal) noexcept; + /// Access the parsed key, which the verification 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 an RSA public key from its raw big-endian modulus and exponent bytes, +/// returning no value when the material is malformed or beyond 4096 bits. +auto SOURCEMETA_CORE_CRYPTO_EXPORT make_rsa_public_key( + const std::string_view modulus, const std::string_view exponent) + -> std::optional; + +/// @ingroup crypto +/// Parse an elliptic curve public key from its raw big-endian point +/// coordinates, returning no value when the point is malformed. +auto SOURCEMETA_CORE_CRYPTO_EXPORT make_ec_public_key( + const EllipticCurve curve, const std::string_view coordinate_x, + const std::string_view coordinate_y) -> std::optional; + +/// @ingroup crypto +/// Parse an Edwards-curve public key from its raw encoded point, returning no +/// value when the key is malformed or the wrong length for the curve. +auto SOURCEMETA_CORE_CRYPTO_EXPORT make_eddsa_public_key( + const EdwardsCurve curve, const std::string_view public_key) + -> std::optional; + /// @ingroup crypto /// Verify an RSASSA-PKCS1-v1_5 signature (RFC 8017 Section 8.2.2) over a -/// message, given the public key as raw big-endian modulus and exponent -/// bytes. The signature is invalid rather than an error if any input is -/// malformed, including keys beyond 4096 bits. For example: +/// message with the given RSA key. The signature is invalid rather than an +/// error if it is malformed or the key is not an RSA key. For example: /// /// ```cpp /// #include /// #include /// +/// const auto key{sourcemeta::core::make_rsa_public_key(modulus, exponent)}; +/// assert(key.has_value()); /// assert(!sourcemeta::core::rsassa_pkcs1_v15_verify( -/// sourcemeta::core::SignatureHashFunction::SHA256, -/// "", "", "message", "signature")); +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, "message", +/// "signature")); /// ``` auto SOURCEMETA_CORE_CRYPTO_EXPORT rsassa_pkcs1_v15_verify( - const SignatureHashFunction hash, const std::string_view modulus, - const std::string_view exponent, const std::string_view message, - const std::string_view signature) -> bool; + const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, const std::string_view signature) -> bool; /// @ingroup crypto -/// Verify an RSASSA-PSS signature (RFC 8017 Section 8.1.2) over a message, -/// given the public key as raw big-endian modulus and exponent bytes. The -/// salt is expected to be as long as the hash function output, as RFC 7518 -/// requires, and signatures carrying any other salt length are invalid, as -/// are keys beyond 4096 bits. For example: +/// Verify an RSASSA-PSS signature (RFC 8017 Section 8.1.2) over a message with +/// the given RSA key. The salt is expected to be as long as the hash function +/// output, as RFC 7518 requires, and signatures carrying any other salt length +/// are invalid, as are signatures verified against a non-RSA key. For example: /// /// ```cpp /// #include /// #include /// +/// const auto key{sourcemeta::core::make_rsa_public_key(modulus, exponent)}; +/// assert(key.has_value()); /// assert(!sourcemeta::core::rsassa_pss_verify( -/// sourcemeta::core::SignatureHashFunction::SHA256, -/// "", "", "message", "signature")); +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, "message", +/// "signature")); /// ``` auto SOURCEMETA_CORE_CRYPTO_EXPORT rsassa_pss_verify( - const SignatureHashFunction hash, const std::string_view modulus, - const std::string_view exponent, const std::string_view message, - const std::string_view signature) -> bool; + const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, const std::string_view signature) -> bool; /// @ingroup crypto -/// Verify an ECDSA signature (FIPS 186-4 Section 6.4) over a message, given -/// the public key as raw big-endian point coordinates. 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). The signature is invalid -/// rather than an error if any input is malformed or the point is not on the -/// curve. For example: +/// Verify an ECDSA signature (FIPS 186-4 Section 6.4) over a message with the +/// given elliptic curve key. 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). The signature is invalid rather than an error if it is +/// malformed or the key is not an elliptic curve key. For example: /// /// ```cpp /// #include /// #include /// +/// const auto key{sourcemeta::core::make_ec_public_key( +/// sourcemeta::core::EllipticCurve::P256, x, y)}; +/// assert(key.has_value()); /// assert(!sourcemeta::core::ecdsa_verify( -/// sourcemeta::core::EllipticCurve::P256, -/// sourcemeta::core::SignatureHashFunction::SHA256, -/// "", "", "message", "signature")); +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, "message", +/// "signature")); /// ``` auto SOURCEMETA_CORE_CRYPTO_EXPORT ecdsa_verify( - const EllipticCurve curve, const SignatureHashFunction hash, - const std::string_view coordinate_x, const std::string_view coordinate_y, + const PublicKey &key, const SignatureHashFunction hash, const std::string_view message, const std::string_view signature) -> bool; +/// @ingroup crypto +/// Verify an EdDSA signature (RFC 8032) over a message with the given Edwards +/// curve key. There is no separate hash function, as the curve fixes it. The +/// signature is invalid rather than an error if it is malformed or the key is +/// not an Edwards curve key. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::make_eddsa_public_key( +/// sourcemeta::core::EdwardsCurve::Ed25519, public_key)}; +/// assert(key.has_value()); +/// assert(!sourcemeta::core::eddsa_verify(key.value(), "message", +/// "signature")); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT +eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool; + } // namespace sourcemeta::core #endif diff --git a/vendor/core/src/core/http/CMakeLists.txt b/vendor/core/src/core/http/CMakeLists.txt index ddc5d7d11..e81a3b9c8 100644 --- a/vendor/core/src/core/http/CMakeLists.txt +++ b/vendor/core/src/core/http/CMakeLists.txt @@ -1,8 +1,19 @@ +if(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) + set(SOURCEMETA_CORE_HTTP_CLIENT_SOURCE client_curl.cc) +elseif(APPLE) + set(SOURCEMETA_CORE_HTTP_CLIENT_SOURCE client_darwin.mm) +elseif(WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "MSYS") + set(SOURCEMETA_CORE_HTTP_CLIENT_SOURCE client_windows.cc) +else() + set(SOURCEMETA_CORE_HTTP_CLIENT_SOURCE client_curl.cc) +endif() + sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME http - PRIVATE_HEADERS problem.h status.h method.h message.h error.h + PRIVATE_HEADERS problem.h status.h method.h message.h error.h system.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) + accept_includes_all.cc content_type_matches.cc parse_bearer.cc + ${SOURCEMETA_CORE_HTTP_CLIENT_SOURCE}) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME http) @@ -11,3 +22,17 @@ 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) + +if(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) + find_package(CURL REQUIRED) + target_compile_definitions(sourcemeta_core_http + PRIVATE SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) + target_link_libraries(sourcemeta_core_http PRIVATE CURL::libcurl) +elseif(APPLE) + target_link_libraries(sourcemeta_core_http PRIVATE "-framework Foundation") +elseif(WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "MSYS") + target_link_libraries(sourcemeta_core_http PRIVATE winhttp) + target_link_libraries(sourcemeta_core_http PRIVATE sourcemeta::core::unicode) +else() + target_link_libraries(sourcemeta_core_http PRIVATE ${CMAKE_DL_LIBS}) +endif() diff --git a/vendor/core/src/core/http/client_curl.cc b/vendor/core/src/core/http/client_curl.cc new file mode 100644 index 000000000..0b7c89d94 --- /dev/null +++ b/vendor/core/src/core/http/client_curl.cc @@ -0,0 +1,410 @@ +#include + +#ifdef SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL +#include // curl_easy_*, curl_slist_*, curl_global_init, CURLOPT_* +#endif + +#include // std::size_t +#include // std::uint16_t +#include // std::optional +#include // std::string +#include // std::string_view + +#ifndef SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL +#include // dlopen, dlsym, dlerror, RTLD_NOW + +#include // std::array +#include // std::getenv +#include // std::memcpy +#include // std::vector + +// When cURL is not linked at build time, we load it at runtime via dlopen +// and therefore need no curl headers. The following reproduces the small +// subset of libcurl's C API this backend uses. Every type, prototype, and +// option id is part of libcurl's frozen ABI (SONAME libcurl.so.4, stable +// since 2006), so these values never change. The prototypes are never +// called directly (we invoke them through dlsym'd pointers) nor linked; +// they exist only so the shared CurlApi table can derive their types +extern "C" { + +using CURL = void; +using CURLcode = int; +using CURLoption = int; +using CURLINFO = int; +using curl_off_t = long long; + +struct curl_slist; + +auto curl_global_init(long flags) -> CURLcode; +auto curl_easy_init() -> CURL *; +auto curl_easy_cleanup(CURL *handle) -> void; +auto curl_easy_setopt(CURL *handle, CURLoption option, ...) -> CURLcode; +auto curl_easy_perform(CURL *handle) -> CURLcode; +auto curl_easy_getinfo(CURL *handle, CURLINFO info, ...) -> CURLcode; +auto curl_easy_strerror(CURLcode code) -> const char *; +auto curl_slist_append(curl_slist *list, const char *value) -> curl_slist *; +auto curl_slist_free_all(curl_slist *list) -> void; + +} // extern "C" + +// Option ids are a type-class base plus an index in libcurl's headers; the +// resolved values are reproduced here (see the trailing comments) +constexpr CURLcode CURLE_OK{0}; +constexpr long CURL_GLOBAL_ALL{3}; // SSL(1<<0) | WIN32(1<<1) +constexpr CURLoption CURLOPT_URL{10002}; // STRINGPOINT + 2 +constexpr CURLoption CURLOPT_FOLLOWLOCATION{52}; // LONG + 52 +constexpr CURLoption CURLOPT_MAXREDIRS{68}; // LONG + 68 +constexpr CURLoption CURLOPT_NOSIGNAL{99}; // LONG + 99 +constexpr CURLoption CURLOPT_ACCEPT_ENCODING{10102}; // STRINGPOINT + 102 +constexpr CURLoption CURLOPT_TIMEOUT_MS{155}; // LONG + 155 +constexpr CURLoption CURLOPT_CONNECTTIMEOUT_MS{156}; // LONG + 156 +constexpr CURLoption CURLOPT_WRITEFUNCTION{20011}; // FUNCTIONPOINT + 11 +constexpr CURLoption CURLOPT_WRITEDATA{10001}; // CBPOINT + 1 +constexpr CURLoption CURLOPT_HEADERFUNCTION{20079}; // FUNCTIONPOINT + 79 +constexpr CURLoption CURLOPT_HEADERDATA{10029}; // CBPOINT + 29 +constexpr CURLoption CURLOPT_POSTFIELDSIZE_LARGE{30120}; // OFF_T + 120 +constexpr CURLoption CURLOPT_POSTFIELDS{10015}; // OBJECTPOINT + 15 +constexpr CURLoption CURLOPT_HTTPHEADER{10023}; // SLISTPOINT + 23 +constexpr CURLoption CURLOPT_NOBODY{44}; // LONG + 44 +constexpr CURLoption CURLOPT_CUSTOMREQUEST{10036}; // STRINGPOINT + 36 +constexpr CURLINFO CURLINFO_RESPONSE_CODE{2097154}; // CURLINFO_LONG(0x200000)+2 +constexpr CURLINFO CURLINFO_EFFECTIVE_URL{ + 1048577}; // CURLINFO_STRING(0x100000)+1 +#endif + +namespace { + +constexpr std::string_view HTTP_RESPONSE_TOO_LARGE_MESSAGE{ + "The response exceeds the maximum allowed size"}; + +// The subset of the libcurl C API this backend relies on, captured as +// function pointers so the request logic is shared between the link-time +// backend (SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) and the default +// runtime-loaded (dlopen) backend. Member types are derived from the curl +// headers, so they stay in sync with the real prototypes +struct CurlApi { + decltype(&curl_global_init) global_init; + decltype(&curl_easy_init) easy_init; + decltype(&curl_easy_cleanup) easy_cleanup; + decltype(&curl_easy_setopt) easy_setopt; + decltype(&curl_easy_perform) easy_perform; + decltype(&curl_easy_getinfo) easy_getinfo; + decltype(&curl_easy_strerror) easy_strerror; + decltype(&curl_slist_append) slist_append; + decltype(&curl_slist_free_all) slist_free_all; +}; + +class CurlHandle { +public: + explicit CurlHandle(const CurlApi &api) + : api_{api}, handle_{api.easy_init()} {} + ~CurlHandle() { + if (this->handle_) { + this->api_.easy_cleanup(this->handle_); + } + } + + CurlHandle(const CurlHandle &) = delete; + auto operator=(const CurlHandle &) -> CurlHandle & = delete; + CurlHandle(CurlHandle &&) = delete; + auto operator=(CurlHandle &&) -> CurlHandle & = delete; + + [[nodiscard]] auto get() const -> CURL * { return this->handle_; } + explicit operator bool() const { return this->handle_ != nullptr; } + +private: + const CurlApi &api_; + CURL *handle_; +}; + +class CurlHeaderList { +public: + explicit CurlHeaderList(const CurlApi &api) : api_{api} {} + ~CurlHeaderList() { + if (this->list_) { + this->api_.slist_free_all(this->list_); + } + } + + CurlHeaderList(const CurlHeaderList &) = delete; + auto operator=(const CurlHeaderList &) -> CurlHeaderList & = delete; + CurlHeaderList(CurlHeaderList &&) = delete; + auto operator=(CurlHeaderList &&) -> CurlHeaderList & = delete; + + auto append(const std::string &line) -> void { + auto *result{this->api_.slist_append(this->list_, line.c_str())}; + if (result) { + this->list_ = result; + } + } + + [[nodiscard]] auto get() const -> curl_slist * { return this->list_; } + +private: + const CurlApi &api_; + curl_slist *list_{nullptr}; +}; + +struct BodyContext { + std::string *output; + std::optional maximum_size; + bool maximum_size_exceeded{false}; +}; + +auto body_callback(char *data, std::size_t size, std::size_t count, + void *user_data) -> std::size_t { + auto *context{static_cast(user_data)}; + const std::size_t chunk{size * count}; + if (context->maximum_size.has_value() && + (context->output->size() > context->maximum_size.value() || + chunk > context->maximum_size.value() - context->output->size())) { + context->maximum_size_exceeded = true; + // Returning a smaller count than given aborts the transfer + return 0; + } + + context->output->append(data, chunk); + return chunk; +} + +auto header_callback(char *data, std::size_t size, std::size_t count, + void *output) -> std::size_t { + sourcemeta::core::http_accumulate_header_line( + *static_cast(output), + std::string_view{data, size * count}); + return size * count; +} + +#ifndef SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL + +using sourcemeta::core::HTTPSystemBackendError; + +constexpr std::string_view CURL_LIBRARY_ENV{"SOURCEMETA_CORE_CURL_SO"}; + +// Tried in order. Every entry carries the `.so.4` SONAME so we only ever +// bind an ABI-compatible cURL (never the unversioned `libcurl.so` dev +// symlink, which could point at a different major version). The bare +// soname is first because it resolves through the dynamic linker (ld.so +// cache on glibc, default /lib:/usr/lib on musl) and is present on every +// mainstream distribution. The absolute entries are fallbacks for +// environments where the cache is absent (custom prefixes, ldconfig not +// run). The trailing GnuTLS entry is an ABI-compatible last resort for +// minimal Debian and Ubuntu systems that ship only that build +constexpr std::array CURL_CANDIDATE_PATHS{ + {"libcurl.so.4", "/usr/lib/x86_64-linux-gnu/libcurl.so.4", + "/usr/lib/aarch64-linux-gnu/libcurl.so.4", + "/usr/lib/arm-linux-gnueabihf/libcurl.so.4", + "/usr/lib/i386-linux-gnu/libcurl.so.4", "/usr/lib64/libcurl.so.4", + "/lib64/libcurl.so.4", "/usr/lib/libcurl.so.4", + "/usr/local/lib/libcurl.so.4", "libcurl-gnutls.so.4"}}; + +struct ResolvedLibrary { + void *handle; + std::string path; +}; + +template +auto resolve_symbol(const ResolvedLibrary &library, const char *name) + -> Signature { + // Clear any stale error, then distinguish a null symbol from a null value + dlerror(); + void *symbol{dlsym(library.handle, name)}; + if (dlerror() != nullptr) { + throw HTTPSystemBackendError{ + "The cURL library was loaded but does not provide the expected API", + std::string{CURL_LIBRARY_ENV}, + {library.path}}; + } + + // Copy the pointer representation instead of reinterpret_cast, which the + // standard only conditionally supports for object-to-function conversions. + // POSIX guarantees dlsym results are convertible to function pointers + Signature function{}; + std::memcpy(&function, &symbol, sizeof(function)); + return function; +} + +auto open_library() -> ResolvedLibrary { + if (const auto *configured_path{std::getenv(CURL_LIBRARY_ENV.data())}; + configured_path != nullptr && configured_path[0] != '\0') { + if (auto *handle{dlopen(configured_path, RTLD_NOW)}; handle != nullptr) { + return {handle, configured_path}; + } + + throw HTTPSystemBackendError{ + "Could not load the cURL library from the configured path", + std::string{CURL_LIBRARY_ENV}, + {std::string{configured_path}}}; + } + + for (const auto candidate : CURL_CANDIDATE_PATHS) { + if (auto *handle{dlopen(candidate.data(), RTLD_NOW)}; handle != nullptr) { + return {handle, std::string{candidate}}; + } + } + + std::vector searched; + searched.reserve(CURL_CANDIDATE_PATHS.size()); + for (const auto candidate : CURL_CANDIDATE_PATHS) { + searched.emplace_back(candidate); + } + + throw HTTPSystemBackendError{ + "Could not find the system cURL library (libcurl)", + std::string{CURL_LIBRARY_ENV}, std::move(searched)}; +} + +auto load_curl() -> const CurlApi & { + // The handle is intentionally never dlclose()d: the function pointers + // must remain valid for the lifetime of the process + static const ResolvedLibrary library{open_library()}; + static const CurlApi api{ + .global_init = resolve_symbol( + library, "curl_global_init"), + .easy_init = resolve_symbol( + library, "curl_easy_init"), + .easy_cleanup = resolve_symbol( + library, "curl_easy_cleanup"), + .easy_setopt = resolve_symbol( + library, "curl_easy_setopt"), + .easy_perform = resolve_symbol( + library, "curl_easy_perform"), + .easy_getinfo = resolve_symbol( + library, "curl_easy_getinfo"), + .easy_strerror = resolve_symbol( + library, "curl_easy_strerror"), + .slist_append = resolve_symbol( + library, "curl_slist_append"), + .slist_free_all = resolve_symbol( + library, "curl_slist_free_all")}; + return api; +} + +#endif + +auto acquire_api() -> const CurlApi & { +#ifdef SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL + static const CurlApi api{.global_init = &curl_global_init, + .easy_init = &curl_easy_init, + .easy_cleanup = &curl_easy_cleanup, + .easy_setopt = &curl_easy_setopt, + .easy_perform = &curl_easy_perform, + .easy_getinfo = &curl_easy_getinfo, + .easy_strerror = &curl_easy_strerror, + .slist_append = &curl_slist_append, + .slist_free_all = &curl_slist_free_all}; + return api; +#else + return load_curl(); +#endif +} + +} // namespace + +namespace sourcemeta::core { + +auto HTTPSystemRequest::send() const -> HTTPResponse { + const CurlApi &api{acquire_api()}; + + static const CURLcode global_initialization{api.global_init(CURL_GLOBAL_ALL)}; + if (global_initialization != CURLE_OK) { + throw HTTPError{this->method_, this->url_, + api.easy_strerror(global_initialization)}; + } + + const CurlHandle handle{api}; + if (!handle) { + throw HTTPError{this->method_, this->url_, + "Failed to initialise the HTTP client"}; + } + + HTTPResponse response; + api.easy_setopt(handle.get(), CURLOPT_URL, this->url_.c_str()); + api.easy_setopt(handle.get(), CURLOPT_FOLLOWLOCATION, + this->follow_redirects_ ? 1L : 0L); + if (this->follow_redirects_) { + api.easy_setopt(handle.get(), CURLOPT_MAXREDIRS, + static_cast(this->maximum_redirects_)); + } + + api.easy_setopt(handle.get(), CURLOPT_NOSIGNAL, 1L); + api.easy_setopt(handle.get(), CURLOPT_TIMEOUT_MS, + static_cast(this->timeout_.count())); + if (this->connect_timeout_.has_value()) { + api.easy_setopt(handle.get(), CURLOPT_CONNECTTIMEOUT_MS, + static_cast(this->connect_timeout_.value().count())); + } + + // Advertise and transparently decode all supported content encodings, + // matching what the NSURLSession and WinHTTP backends do + api.easy_setopt(handle.get(), CURLOPT_ACCEPT_ENCODING, ""); + + std::string raw_headers; + BodyContext body_context{.output = &response.body, + .maximum_size = this->maximum_response_size_}; + api.easy_setopt(handle.get(), CURLOPT_WRITEFUNCTION, body_callback); + api.easy_setopt(handle.get(), CURLOPT_WRITEDATA, &body_context); + api.easy_setopt(handle.get(), CURLOPT_HEADERFUNCTION, header_callback); + api.easy_setopt(handle.get(), CURLOPT_HEADERDATA, &raw_headers); + + CurlHeaderList header_list{api}; + for (const auto &[name, value] : this->headers_) { + std::string line{name}; + // The semicolon form is how cURL distinguishes a header with an + // empty value from a header to suppress + if (value.empty()) { + line += ";"; + } else { + line += ": "; + line += value; + } + + header_list.append(line); + } + + if (this->body_.has_value()) { + std::string content_type_line{"Content-Type: "}; + content_type_line += this->body_.value().content_type; + header_list.append(content_type_line); + api.easy_setopt(handle.get(), CURLOPT_POSTFIELDSIZE_LARGE, + static_cast(this->body_.value().data.size())); + api.easy_setopt(handle.get(), CURLOPT_POSTFIELDS, + this->body_.value().data.data()); + } + + if (header_list.get()) { + api.easy_setopt(handle.get(), CURLOPT_HTTPHEADER, header_list.get()); + } + + const std::string method{http_method_string(this->method_)}; + if (this->method_ == HTTPMethod::HEAD) { + api.easy_setopt(handle.get(), CURLOPT_NOBODY, 1L); + } else if (this->method_ != HTTPMethod::GET || this->body_.has_value()) { + api.easy_setopt(handle.get(), CURLOPT_CUSTOMREQUEST, method.c_str()); + } + + const auto code{api.easy_perform(handle.get())}; + if (code != CURLE_OK) { + if (body_context.maximum_size_exceeded) { + throw HTTPError{this->method_, this->url_, + std::string{HTTP_RESPONSE_TOO_LARGE_MESSAGE}}; + } + + throw HTTPError{this->method_, this->url_, api.easy_strerror(code)}; + } + + long status_code{0}; + api.easy_getinfo(handle.get(), CURLINFO_RESPONSE_CODE, &status_code); + char *effective_url{nullptr}; + api.easy_getinfo(handle.get(), CURLINFO_EFFECTIVE_URL, &effective_url); + if (effective_url != nullptr) { + response.url.assign(effective_url); + } + + http_parse_headers(raw_headers, response.headers); + response.status = + http_status_from_code(static_cast(status_code)); + return response; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/client_darwin.mm b/vendor/core/src/core/http/client_darwin.mm new file mode 100644 index 000000000..84d4b731c --- /dev/null +++ b/vendor/core/src/core/http/client_darwin.mm @@ -0,0 +1,194 @@ +#include +#include + +// NSURL, NSMutableURLRequest, NSURLSession, NSHTTPURLResponse, dispatch_* +#import + +#include // std::size_t +#include // std::uint16_t +#include // std::string +#include // std::string_view +#include // std::move + +namespace { + +constexpr std::string_view HTTP_RESPONSE_TOO_LARGE_MESSAGE{ + "The response exceeds the maximum allowed size"}; + +auto to_nsstring(const std::string_view input) -> NSString * { + return [[NSString alloc] initWithBytes:input.data() + length:input.size() + encoding:NSUTF8StringEncoding]; +} + +} // namespace + +// The delegate-based API streams the response body in chunks, allowing +// the maximum response size to be enforced without first buffering the +// entire response in memory +@interface SourcemetaCoreHTTPDelegate : NSObject +@property(nonatomic, assign) sourcemeta::core::HTTPResponse *response; +@property(nonatomic, assign) std::string *failure; +@property(nonatomic, strong) dispatch_semaphore_t semaphore; +@property(nonatomic, assign) BOOL hasMaximumResponseSize; +@property(nonatomic, assign) std::size_t maximumResponseSize; +@property(nonatomic, assign) BOOL followRedirects; +@property(nonatomic, assign) std::size_t maximumRedirects; +@property(nonatomic, assign) std::size_t redirectCount; +@end + +@implementation SourcemetaCoreHTTPDelegate + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + willPerformHTTPRedirection:(NSHTTPURLResponse *)response + newRequest:(NSURLRequest *)request + completionHandler: + (void (^)(NSURLRequest *))completionHandler { + // Passing a nil request stops the redirection and delivers the redirect + // response itself as the final response + if (!self.followRedirects) { + completionHandler(nil); + return; + } + + self.redirectCount += 1; + if (self.redirectCount > self.maximumRedirects) { + self.failure->assign("The maximum number of redirects was exceeded"); + [task cancel]; + completionHandler(nil); + return; + } + + completionHandler(request); +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + didReceiveData:(NSData *)data { + auto *body{&self.response->body}; + if (self.hasMaximumResponseSize && + (body->size() > self.maximumResponseSize || + static_cast(data.length) > + self.maximumResponseSize - body->size())) { + self.failure->assign(HTTP_RESPONSE_TOO_LARGE_MESSAGE); + [dataTask cancel]; + return; + } + + [data enumerateByteRangesUsingBlock:^(const void *bytes, NSRange range, + BOOL *) { + body->append(static_cast(bytes), range.length); + }]; +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didCompleteWithError:(NSError *)error { + // A failure recorded while streaming, such as exceeding the maximum + // response size, takes precedence over the resulting cancellation error + if (self.failure->empty()) { + if (error != nil) { + self.failure->assign([error.localizedDescription UTF8String]); + } else if (![task.response isKindOfClass:[NSHTTPURLResponse class]]) { + self.failure->assign("The response is not an HTTP response"); + } else { + const auto *http_response{(NSHTTPURLResponse *)task.response}; + self.response->status = sourcemeta::core::http_status_from_code( + static_cast(http_response.statusCode)); + if (http_response.URL != nil) { + self.response->url.assign([http_response.URL.absoluteString UTF8String]); + } + + auto *headers{&self.response->headers}; + [http_response.allHeaderFields + enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSString *value, + BOOL *) { + std::string header_name{[name UTF8String]}; + sourcemeta::core::to_lowercase(header_name); + headers->emplace_back(std::move(header_name), [value UTF8String]); + }]; + } + } + + dispatch_semaphore_signal(self.semaphore); +} + +@end + +namespace sourcemeta::core { + +auto HTTPSystemRequest::send() const -> HTTPResponse { + HTTPResponse response; + // The delegate runs on a background queue, where throwing would + // terminate the process, so failures are recorded here and thrown + // from the calling thread once the request settles + std::string failure; + + @autoreleasepool { + NSURL *target{[NSURL URLWithString:to_nsstring(this->url_)]}; + if (target == nil) { + failure = "Invalid URL"; + } else { + NSMutableURLRequest *url_request{ + [NSMutableURLRequest requestWithURL:target]}; + url_request.HTTPMethod = to_nsstring(http_method_string(this->method_)); + for (const auto &[name, value] : this->headers_) { + // Repeated headers are folded into a single comma-separated field + // line, which is semantically equivalent per RFC 9110 + [url_request addValue:to_nsstring(value) + forHTTPHeaderField:to_nsstring(name)]; + } + + if (this->body_.has_value()) { + [url_request setValue:to_nsstring(this->body_.value().content_type) + forHTTPHeaderField:@"Content-Type"]; + url_request.HTTPBody = + [NSData dataWithBytes:this->body_.value().data.data() + length:this->body_.value().data.size()]; + } + + NSURLSessionConfiguration *configuration{ + [NSURLSessionConfiguration ephemeralSessionConfiguration]}; + configuration.timeoutIntervalForResource = + static_cast(this->timeout_.count()) / 1000.0; + if (this->connect_timeout_.has_value()) { + configuration.timeoutIntervalForRequest = + static_cast(this->connect_timeout_.value().count()) / + 1000.0; + } + + // The delegate completes before the semaphore is signalled, so + // pointing to the stack-allocated locals from it is safe + SourcemetaCoreHTTPDelegate *delegate{ + [[SourcemetaCoreHTTPDelegate alloc] init]}; + delegate.response = &response; + delegate.failure = &failure; + delegate.semaphore = dispatch_semaphore_create(0); + delegate.hasMaximumResponseSize = + this->maximum_response_size_.has_value() ? YES : NO; + delegate.maximumResponseSize = + this->maximum_response_size_.value_or(0); + delegate.followRedirects = this->follow_redirects_ ? YES : NO; + delegate.maximumRedirects = this->maximum_redirects_; + delegate.redirectCount = 0; + + NSURLSession *session{ + [NSURLSession sessionWithConfiguration:configuration + delegate:delegate + delegateQueue:nil]}; + NSURLSessionDataTask *task{[session dataTaskWithRequest:url_request]}; + [task resume]; + dispatch_semaphore_wait(delegate.semaphore, DISPATCH_TIME_FOREVER); + [session finishTasksAndInvalidate]; + } + } + + if (!failure.empty()) { + throw HTTPError{this->method_, this->url_, failure}; + } + + return response; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/client_windows.cc b/vendor/core/src/core/http/client_windows.cc new file mode 100644 index 000000000..609115db2 --- /dev/null +++ b/vendor/core/src/core/http/client_windows.cc @@ -0,0 +1,277 @@ +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#include // DWORD, GetLastError, LPVOID +#include // WinHttp* + +// `windows.h` defines a `DELETE` macro that conflicts with +// `sourcemeta::core::HTTPMethod::DELETE` +#ifdef DELETE +#undef DELETE +#endif + +#include + +#include // std::chrono::milliseconds +#include // std::size_t +#include // std::uint16_t +#include // std::numeric_limits +#include // std::string, std::wstring +#include // std::wstring_view +#include // std::pair +#include // std::vector + +namespace { + +constexpr std::string_view HTTP_RESPONSE_TOO_LARGE_MESSAGE{ + "The response exceeds the maximum allowed size"}; + +// WinHttpSetTimeouts takes signed millisecond counts where zero requests no +// timeout. Floor non-positive durations to the smallest bound so a misused +// value cannot become an unbounded wait, and saturate large ones to avoid a +// narrowing wrap +auto to_winhttp_timeout(const std::chrono::milliseconds value) -> int { + if (value.count() <= 0) { + return 1; + } + + if (value.count() > std::numeric_limits::max()) { + return std::numeric_limits::max(); + } + + return static_cast(value.count()); +} + +class WinHTTPHandle { +public: + WinHTTPHandle(const HINTERNET handle) : handle_{handle} {} + ~WinHTTPHandle() { + if (this->handle_) { + WinHttpCloseHandle(this->handle_); + } + } + + WinHTTPHandle(const WinHTTPHandle &) = delete; + auto operator=(const WinHTTPHandle &) -> WinHTTPHandle & = delete; + WinHTTPHandle(WinHTTPHandle &&) = delete; + auto operator=(WinHTTPHandle &&) -> WinHTTPHandle & = delete; + + auto get() const -> HINTERNET { return this->handle_; } + explicit operator bool() const { return this->handle_ != nullptr; } + +private: + HINTERNET handle_; +}; + +auto parse_response_headers( + const HINTERNET request, + std::vector> &headers) -> void { + DWORD size{0}; + WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, + WINHTTP_HEADER_NAME_BY_INDEX, WINHTTP_NO_OUTPUT_BUFFER, + &size, WINHTTP_NO_HEADER_INDEX); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + return; + } + + std::wstring buffer(size / sizeof(wchar_t), L'\0'); + if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, + WINHTTP_HEADER_NAME_BY_INDEX, buffer.data(), &size, + WINHTTP_NO_HEADER_INDEX)) { + return; + } + + sourcemeta::core::http_parse_headers(sourcemeta::core::wide_to_utf8(buffer), + headers); +} + +auto query_effective_url(const HINTERNET request) -> std::string { + DWORD size{0}; + WinHttpQueryOption(request, WINHTTP_OPTION_URL, nullptr, &size); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || size == 0) { + return {}; + } + + std::wstring buffer(size / sizeof(wchar_t), L'\0'); + if (!WinHttpQueryOption(request, WINHTTP_OPTION_URL, buffer.data(), &size)) { + return {}; + } + + buffer.resize(size / sizeof(wchar_t)); + if (!buffer.empty() && buffer.back() == L'\0') { + buffer.pop_back(); + } + + return sourcemeta::core::wide_to_utf8(buffer); +} + +} // namespace + +namespace sourcemeta::core { + +auto HTTPSystemRequest::send() const -> HTTPResponse { + HTTPResponse response; + + const auto wide_url{sourcemeta::core::utf8_to_wide(this->url_)}; + URL_COMPONENTS components{}; + components.dwStructSize = sizeof(components); + components.dwHostNameLength = static_cast(-1); + components.dwUrlPathLength = static_cast(-1); + components.dwExtraInfoLength = static_cast(-1); + if (!WinHttpCrackUrl(wide_url.c_str(), 0, 0, &components)) { + throw HTTPError{this->method_, this->url_, "Invalid URL"}; + } + + const std::wstring host{components.lpszHostName, components.dwHostNameLength}; + std::wstring path{components.lpszUrlPath, components.dwUrlPathLength}; + if (components.lpszExtraInfo) { + // The fragment, if any, must never be sent to the server + const std::wstring_view extra_information{components.lpszExtraInfo, + components.dwExtraInfoLength}; + path.append(extra_information.substr(0, extra_information.find(L'#'))); + } + + const WinHTTPHandle session{ + WinHttpOpen(nullptr, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, + WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0)}; + if (!session) { + throw HTTPError{this->method_, this->url_, + "Failed to initialise the HTTP client"}; + } + + const WinHTTPHandle connection{ + WinHttpConnect(session.get(), host.c_str(), components.nPort, 0)}; + if (!connection) { + throw HTTPError{this->method_, this->url_, "Failed to connect to the host"}; + } + + const auto secure{components.nScheme == INTERNET_SCHEME_HTTPS}; + const auto method{ + sourcemeta::core::utf8_to_wide(http_method_string(this->method_))}; + const WinHTTPHandle request_handle{WinHttpOpenRequest( + connection.get(), method.c_str(), path.c_str(), nullptr, + WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, + secure ? WINHTTP_FLAG_SECURE : 0)}; + if (!request_handle) { + throw HTTPError{this->method_, this->url_, + "Failed to create the HTTP request"}; + } + + if (this->follow_redirects_) { + DWORD maximum_redirects{static_cast(this->maximum_redirects_)}; + WinHttpSetOption(request_handle.get(), + WINHTTP_OPTION_MAX_HTTP_AUTOMATIC_REDIRECTS, + &maximum_redirects, sizeof(maximum_redirects)); + } else { + DWORD policy{WINHTTP_OPTION_REDIRECT_POLICY_NEVER}; + WinHttpSetOption(request_handle.get(), WINHTTP_OPTION_REDIRECT_POLICY, + &policy, sizeof(policy)); + } + + // The total timeout bounds sending the request and receiving the response, + // and also caps the resolution and connection phases unless a narrower + // connect timeout is given, so it acts as an overall ceiling + const auto total_timeout{to_winhttp_timeout(this->timeout_)}; + const auto connect_timeout{ + this->connect_timeout_.has_value() + ? to_winhttp_timeout(this->connect_timeout_.value()) + : total_timeout}; + WinHttpSetTimeouts(request_handle.get(), connect_timeout, connect_timeout, + total_timeout, total_timeout); + + DWORD decompression{WINHTTP_DECOMPRESSION_FLAG_ALL}; + WinHttpSetOption(request_handle.get(), WINHTTP_OPTION_DECOMPRESSION, + &decompression, sizeof(decompression)); + + auto serialized_headers{http_serialize_headers(this->headers_)}; + LPVOID body_data{WINHTTP_NO_REQUEST_DATA}; + DWORD body_size{0}; + if (this->body_.has_value()) { + if (this->body_.value().data.size() > std::numeric_limits::max()) { + throw HTTPError{this->method_, this->url_, + "The request body is too large"}; + } + + serialized_headers += "Content-Type: "; + serialized_headers += this->body_.value().content_type; + serialized_headers += "\r\n"; + body_data = const_cast(this->body_.value().data.data()); + body_size = static_cast(this->body_.value().data.size()); + } + + const auto request_headers{ + sourcemeta::core::utf8_to_wide(serialized_headers)}; + + if (!WinHttpSendRequest( + request_handle.get(), + request_headers.empty() ? WINHTTP_NO_ADDITIONAL_HEADERS + : request_headers.c_str(), + request_headers.empty() ? 0 + : static_cast(request_headers.size()), + body_data, body_size, body_size, 0)) { + throw HTTPError{this->method_, this->url_, + "Failed to send the HTTP request"}; + } + + if (!WinHttpReceiveResponse(request_handle.get(), nullptr)) { + throw HTTPError{this->method_, this->url_, + "Failed to receive the HTTP response"}; + } + + DWORD status_code{0}; + DWORD status_code_size{sizeof(status_code)}; + if (!WinHttpQueryHeaders(request_handle.get(), + WINHTTP_QUERY_STATUS_CODE | + WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, &status_code, + &status_code_size, WINHTTP_NO_HEADER_INDEX)) { + throw HTTPError{this->method_, this->url_, + "Failed to read the HTTP response status"}; + } + + parse_response_headers(request_handle.get(), response.headers); + response.url = query_effective_url(request_handle.get()); + + while (true) { + DWORD available{0}; + if (!WinHttpQueryDataAvailable(request_handle.get(), &available)) { + throw HTTPError{this->method_, this->url_, + "Failed to read the HTTP response body"}; + } + + if (available == 0) { + break; + } + + if (this->maximum_response_size_.has_value() && + (response.body.size() > this->maximum_response_size_.value() || + available > + this->maximum_response_size_.value() - response.body.size())) { + throw HTTPError{this->method_, this->url_, + std::string{HTTP_RESPONSE_TOO_LARGE_MESSAGE}}; + } + + const auto offset{response.body.size()}; + response.body.resize(offset + available); + DWORD read{0}; + if (!WinHttpReadData(request_handle.get(), response.body.data() + offset, + available, &read)) { + throw HTTPError{this->method_, this->url_, + "Failed to read the HTTP response body"}; + } + + response.body.resize(offset + read); + } + + response.status = + http_status_from_code(static_cast(status_code)); + return response; +} + +} // namespace sourcemeta::core 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 59028c611..f572a6e32 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http.h @@ -11,6 +11,7 @@ #include #include #include +#include // NOLINTEND(misc-include-cleaner) #include // std::chrono::system_clock diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_system.h b/vendor/core/src/core/http/include/sourcemeta/core/http_system.h new file mode 100644 index 000000000..2d85dcbff --- /dev/null +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_system.h @@ -0,0 +1,192 @@ +#ifndef SOURCEMETA_CORE_HTTP_SYSTEM_H_ +#define SOURCEMETA_CORE_HTTP_SYSTEM_H_ + +#ifndef SOURCEMETA_CORE_HTTP_EXPORT +#include +#endif + +#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 + +namespace sourcemeta::core { + +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup http +/// The result of performing a request against a system HTTP backend. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// sourcemeta::core::HTTPSystemRequest request{"https://example.com"}; +/// const auto response{request.send()}; +/// assert(response.status == sourcemeta::core::HTTP_STATUS_OK); +/// ``` +struct HTTPResponse { + /// The response status code + HTTPStatus status{}; + /// The response headers, with names normalised to lowercase. Repeated + /// headers are preserved as separate entries, except on backends that fold + /// them into a single comma-separated entry, which is semantically + /// equivalent per RFC 9110 + std::vector> headers; + /// The response body, owned by this result + std::string body; + /// The effective URL after any followed redirects + std::string url; +}; + +/// @ingroup http +/// An error that prevented loading the underlying system HTTP backend, such +/// as a missing dynamically loaded library. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const sourcemeta::core::HTTPSystemBackendError error{ +/// "Could not find the system cURL library", "SOURCEMETA_CORE_CURL_SO", +/// {"libcurl.so.4"}}; +/// assert(error.variable() == "SOURCEMETA_CORE_CURL_SO"); +/// ``` +class SOURCEMETA_CORE_HTTP_EXPORT HTTPSystemBackendError + : public std::runtime_error { +public: + HTTPSystemBackendError(const std::string &message, std::string variable, + std::vector paths) + : std::runtime_error{message}, variable_{std::move(variable)}, + paths_{std::move(paths)} {} + + /// Get the name of the environment variable that overrides the backend path + [[nodiscard]] auto variable() const noexcept -> const std::string & { + return this->variable_; + } + + /// Get the paths that were searched while looking for the backend + [[nodiscard]] auto paths() const noexcept + -> const std::vector & { + return this->paths_; + } + +private: + std::string variable_; + std::vector paths_; +}; + +/// @ingroup http +/// A simple cross-platform HTTP request that delegates to the system HTTP +/// stack, NSURLSession on Apple platforms, WinHTTP on Windows, and cURL +/// everywhere else. The request owns its data, configure it with the builder +/// methods and perform it with `send`. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// sourcemeta::core::HTTPSystemRequest request{ +/// "https://example.com", sourcemeta::core::HTTPMethod::POST}; +/// request.header("Accept", "application/json"); +/// request.body("{}", "application/json"); +/// const auto response{request.send()}; +/// assert(response.status == sourcemeta::core::HTTP_STATUS_OK); +/// ``` +class SOURCEMETA_CORE_HTTP_EXPORT HTTPSystemRequest { +public: + explicit HTTPSystemRequest(std::string url, + const HTTPMethod method = HTTPMethod::GET) + : url_{std::move(url)}, method_{method} {} + + /// Set the request method + auto method(const HTTPMethod method) -> HTTPSystemRequest & { + this->method_ = method; + return *this; + } + + /// Add a request header. Repeated names are permitted + auto header(std::string name, std::string value) -> HTTPSystemRequest & { + this->headers_.emplace_back(std::move(name), std::move(value)); + return *this; + } + + /// Set the request body, sent along with the given `Content-Type` header + auto body(std::string data, std::string content_type) -> HTTPSystemRequest & { + this->body_ = + Body{.data = std::move(data), .content_type = std::move(content_type)}; + return *this; + } + + /// Set whether to follow redirects, on by default + auto follow_redirects(const bool value) -> HTTPSystemRequest & { + this->follow_redirects_ = value; + return *this; + } + + /// Set the maximum number of redirects to follow, 20 by default + auto maximum_redirects(const std::size_t value) -> HTTPSystemRequest & { + this->maximum_redirects_ = value; + return *this; + } + + /// Set the total request timeout, 30 seconds by default + auto timeout(const std::chrono::milliseconds value) -> HTTPSystemRequest & { + this->timeout_ = value; + return *this; + } + + /// Set a best-effort timeout for establishing the connection, applied as + /// each backend allows and falling back to the backend default when unset + auto connect_timeout(const std::chrono::milliseconds value) + -> HTTPSystemRequest & { + this->connect_timeout_ = value; + return *this; + } + + /// Abort with an error if the response body exceeds this number of bytes + auto maximum_response_size(const std::size_t value) -> HTTPSystemRequest & { + this->maximum_response_size_ = value; + return *this; + } + + /// 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; + +private: + struct Body { + std::string data; + std::string content_type; + }; + + std::string url_; + HTTPMethod method_; + std::vector> headers_; + std::optional body_; + bool follow_redirects_{true}; + std::size_t maximum_redirects_{20}; + std::chrono::milliseconds timeout_{std::chrono::seconds{30}}; + std::optional connect_timeout_; + std::optional maximum_response_size_; +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/jose/CMakeLists.txt b/vendor/core/src/core/jose/CMakeLists.txt new file mode 100644 index 000000000..3923db25b --- /dev/null +++ b/vendor/core/src/core/jose/CMakeLists.txt @@ -0,0 +1,18 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME jose + PRIVATE_HEADERS algorithm.h error.h jwk.h jwks.h jwt.h verify.h + SOURCES jose_algorithm.cc jose_jwk.cc jose_jwks.cc jose_jwt.cc + jose_jwt_check_claims.cc jose_jws_verify_signature.cc + jose_jwt_verify_signature.cc jose_jwt_verify.cc) + +target_link_libraries(sourcemeta_core_jose + PUBLIC sourcemeta::core::json) +target_link_libraries(sourcemeta_core_jose + PUBLIC sourcemeta::core::crypto) +target_link_libraries(sourcemeta_core_jose + PRIVATE sourcemeta::core::text) +target_link_libraries(sourcemeta_core_jose + PRIVATE sourcemeta::core::time) + +if(SOURCEMETA_CORE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME jose) +endif() diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose.h new file mode 100644 index 000000000..47193a2ae --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose.h @@ -0,0 +1,26 @@ +#ifndef SOURCEMETA_CORE_JOSE_H_ +#define SOURCEMETA_CORE_JOSE_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +#include +#include +#include +#include +// NOLINTEND(misc-include-cleaner) + +/// @defgroup jose JOSE +/// @brief Standards-driven primitives for validating JSON Web Tokens. +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +#endif 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 new file mode 100644 index 000000000..3f938c103 --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_algorithm.h @@ -0,0 +1,49 @@ +#ifndef SOURCEMETA_CORE_JOSE_ALGORITHM_H_ +#define SOURCEMETA_CORE_JOSE_ALGORITHM_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +#include // std::uint8_t +#include // std::optional +#include // std::string_view + +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. +enum class JWSAlgorithm : std::uint8_t { + RS256, + RS384, + RS512, + PS256, + PS384, + PS512, + ES256, + ES384, + ES512, + EdDSA +}; + +/// @ingroup jose +/// Map a JSON Web Signature `alg` value to its algorithm, returning no value +/// for any unrecognized name. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::to_jws_algorithm("RS256").has_value()); +/// assert(!sourcemeta::core::to_jws_algorithm("none").has_value()); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto to_jws_algorithm(const std::string_view value) noexcept + -> std::optional; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_error.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_error.h new file mode 100644 index 000000000..9cd17fc58 --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_error.h @@ -0,0 +1,49 @@ +#ifndef SOURCEMETA_CORE_JOSE_ERROR_H_ +#define SOURCEMETA_CORE_JOSE_ERROR_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +#include // std::exception + +namespace sourcemeta::core { + +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup jose +/// An error that occurs when parsing an invalid JSON Web Key. +class SOURCEMETA_CORE_JOSE_EXPORT JWKParseError : public std::exception { +public: + [[nodiscard]] auto what() const noexcept -> const char * override { + return "The input is not a valid JSON Web Key"; + } +}; + +/// @ingroup jose +/// An error that occurs when parsing an invalid JSON Web Key Set. +class SOURCEMETA_CORE_JOSE_EXPORT JWKSParseError : public std::exception { +public: + [[nodiscard]] auto what() const noexcept -> const char * override { + return "The input is not a valid JSON Web Key Set"; + } +}; + +/// @ingroup jose +/// An error that occurs when parsing an invalid JSON Web Token. +class SOURCEMETA_CORE_JOSE_EXPORT JWTParseError : public std::exception { +public: + [[nodiscard]] auto what() const noexcept -> const char * override { + return "The input is not a valid JSON Web Token"; + } +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::core + +#endif 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 new file mode 100644 index 000000000..879873b9d --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk.h @@ -0,0 +1,111 @@ +#ifndef SOURCEMETA_CORE_JOSE_JWK_H_ +#define SOURCEMETA_CORE_JOSE_JWK_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +// NOLINTEND(misc-include-cleaner) + +#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 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: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto document{sourcemeta::core::parse_json( +/// R"({ "kty": "RSA", "n": "0vx7ag", "e": "AQAB" })")}; +/// const auto key{sourcemeta::core::JWK::from(document)}; +/// assert(key.has_value()); +/// assert(key.value().type() == sourcemeta::core::JWK::Type::RSA); +/// ``` +class SOURCEMETA_CORE_JOSE_EXPORT JWK { +public: + enum class Type : std::uint8_t { RSA, EllipticCurve, OctetKeyPair }; + + /// Parse a JSON Web Key from a JSON value, throwing on invalid input. + explicit JWK(const JSON &value); + + /// Parse a JSON Web Key from a JSON value, throwing on invalid input. + explicit JWK(JSON &&value); + + /// A key exclusively owns its parsed public key, so it is move-only. + JWK(JWK &&other) noexcept = default; + auto operator=(JWK &&other) noexcept -> JWK & = default; + JWK(const JWK &) = delete; + auto operator=(const JWK &) -> JWK & = delete; + + /// Parse a JSON Web Key from a JSON value, returning no value on invalid + /// input. + [[nodiscard]] static auto from(const JSON &value) -> std::optional; + + /// Parse a JSON Web Key from a JSON value, returning no value on invalid + /// input. + [[nodiscard]] static auto from(JSON &&value) -> std::optional; + + [[nodiscard]] auto type() const noexcept -> Type { return this->type_; } + + [[nodiscard]] auto key_id() const noexcept + -> std::optional { + if (this->key_id_.has_value()) { + return std::string_view{this->key_id_.value()}; + } + + return std::nullopt; + } + + [[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 + [[nodiscard]] auto curve() const noexcept -> std::string_view { + return this->curve_; + } + + // 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 + [[nodiscard]] auto public_key() const noexcept -> const PublicKey * { + return this->public_key_.has_value() ? &*this->public_key_ : nullptr; + } + +private: + JWK() = default; + static auto parse(const JSON &value, JWK &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 public_key_; +#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 new file mode 100644 index 000000000..9880b6833 --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks.h @@ -0,0 +1,89 @@ +#ifndef SOURCEMETA_CORE_JOSE_JWKS_H_ +#define SOURCEMETA_CORE_JOSE_JWKS_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +// NOLINTEND(misc-include-cleaner) + +#include +#include + +#include // std::size_t +#include // std::optional +#include // std::string_view +#include // std::vector + +namespace sourcemeta::core { + +/// @ingroup jose +/// A parsed JSON Web Key Set (RFC 7517 Section 5). Keys that individually fail +/// to parse, such as those of an unsupported type, are skipped rather than +/// failing the whole set, so one exotic key cannot break verification of +/// tokens signed by the others. The set owns its keys. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto document{sourcemeta::core::parse_json( +/// R"({ "keys": [ { "kty": "RSA", "n": "0vx7ag", "e": "AQAB", +/// "kid": "2024" } ] })")}; +/// const auto keys{sourcemeta::core::JWKS::from(document)}; +/// assert(keys.has_value()); +/// assert(keys.value().find("2024") != nullptr); +/// ``` +class SOURCEMETA_CORE_JOSE_EXPORT JWKS { +public: + /// Parse a JSON Web Key Set from a JSON value, throwing a `JWKSParseError` + /// on invalid input. + explicit JWKS(const JSON &value); + explicit JWKS(JSON &&value); + + /// A key set exclusively owns its keys, so it is move-only. + JWKS(JWKS &&other) noexcept = default; + auto operator=(JWKS &&other) noexcept -> JWKS & = default; + JWKS(const JWKS &) = delete; + auto operator=(const JWKS &) -> JWKS & = delete; + + /// 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; + [[nodiscard]] static auto from(JSON &&value) -> std::optional; + + /// Look up a key by its identifier (RFC 7515 Section 4.1.4), returning no + /// pointer when no key in the set carries it. + [[nodiscard]] auto find(const std::string_view key_id) const noexcept + -> const JWK *; + + [[nodiscard]] auto size() const noexcept -> std::size_t { + return this->keys_.size(); + } + + [[nodiscard]] auto empty() const noexcept -> bool { + return this->keys_.empty(); + } + + [[nodiscard]] auto begin() const noexcept { return this->keys_.begin(); } + [[nodiscard]] auto end() const noexcept { return this->keys_.end(); } + +private: + JWKS() = default; + static auto parse(const JSON &value, JWKS &result) -> bool; + +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + std::vector keys_; +#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_jwt.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwt.h new file mode 100644 index 000000000..a4b51abc3 --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwt.h @@ -0,0 +1,118 @@ +#ifndef SOURCEMETA_CORE_JOSE_JWT_H_ +#define SOURCEMETA_CORE_JOSE_JWT_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +// NOLINTEND(misc-include-cleaner) + +#include + +#include // std::chrono::system_clock::time_point +#include // std::optional +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup jose +/// A parsed JSON Web Token in compact serialization (RFC 7519, RFC 7515). The +/// token does not own its input, so the string it was parsed from must outlive +/// it. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const std::string input{ +/// "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY21lIn0.c2ln"}; +/// const auto token{sourcemeta::core::JWT::from(input)}; +/// assert(token.has_value()); +/// assert(token.value().algorithm() == sourcemeta::core::JWSAlgorithm::RS256); +/// ``` +class SOURCEMETA_CORE_JOSE_EXPORT JWT { +public: + /// Parse a JSON Web Token from its compact serialization, throwing a + /// `JWTParseError` on invalid input. + explicit JWT(const std::string_view input); + + /// Parse a JSON Web Token from its compact serialization, returning no value + /// on invalid input. + [[nodiscard]] static auto from(const std::string_view input) + -> std::optional; + + // Header (RFC 7515 Section 4) + + [[nodiscard]] auto algorithm() const noexcept -> std::optional { + return this->algorithm_; + } + + [[nodiscard]] auto key_id() const noexcept -> std::optional; + + [[nodiscard]] auto type() const noexcept -> std::optional; + + [[nodiscard]] auto header() const noexcept -> const JSON & { + return this->header_; + } + + // Registered claims (RFC 7519 Section 4.1) + + [[nodiscard]] auto issuer() const noexcept -> std::optional; + + [[nodiscard]] auto subject() const noexcept + -> std::optional; + + [[nodiscard]] auto + has_audience(const std::string_view audience) const noexcept -> bool; + + [[nodiscard]] auto expires_at() const + -> std::optional; + + [[nodiscard]] auto not_before() const + -> std::optional; + + [[nodiscard]] auto issued_at() const + -> std::optional; + + [[nodiscard]] auto token_id() const noexcept + -> std::optional; + + [[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) + [[nodiscard]] auto signing_input() const noexcept -> std::string_view { + return this->signing_input_; + } + + [[nodiscard]] auto signature() const noexcept -> std::string_view { + return this->signature_; + } + +private: + JWT() = default; + static auto parse(const std::string_view input, JWT &result) -> bool; + +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + std::string_view signing_input_; + std::string signature_; + JSON header_{nullptr}; + JSON payload_{nullptr}; + std::optional algorithm_; +#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_verify.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_verify.h new file mode 100644 index 000000000..30b9d37df --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_verify.h @@ -0,0 +1,178 @@ +#ifndef SOURCEMETA_CORE_JOSE_VERIFY_H_ +#define SOURCEMETA_CORE_JOSE_VERIFY_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +#include +#include +// NOLINTEND(misc-include-cleaner) + +#include // std::chrono::seconds, std::chrono::system_clock +#include // std::uint8_t +#include // std::optional +#include // std::span +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup jose +/// 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 { + Issuer, + Subject, + Audience, + Expiration, + NotBefore, + IssuedAt +}; + +/// @ingroup jose +/// Validate the registered claims of a JSON Web Token against the expected +/// issuer and audience at a given time, returning the first failing check or no +/// value when every check passes. The expiration claim is required (RFC 9068 +/// Section 2.2), and the subject is checked only when an expected value is +/// supplied. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// +/// const std::string input{ +/// "eyJhbGciOiJSUzI1NiJ9." +/// "eyJpc3MiOiJhY21lIiwiYXVkIjoiY2xpZW50IiwiZXhwIjoyMDAwMDAwMDAwfQ.c2ln"}; +/// const auto token{sourcemeta::core::JWT::from(input)}; +/// assert(token.has_value()); +/// const auto error{sourcemeta::core::jwt_check_claims( +/// token.value(), "acme", "client", +/// std::chrono::system_clock::from_time_t(1500000000))}; +/// assert(!error.has_value()); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto jwt_check_claims( + const JWT &token, const std::string_view expected_issuer, + const std::string_view expected_audience, + const std::chrono::system_clock::time_point now, + const std::chrono::seconds clock_skew = std::chrono::seconds{0}, + const std::optional expected_subject = std::nullopt) + -> std::optional; + +/// @ingroup jose +/// Verify a JSON Web Signature given its algorithm, its signing input, and its +/// decoded signature against a JSON Web Key, returning false rather than +/// throwing for an unrecognized algorithm, a key whose type or curve cannot +/// serve the algorithm, a key declaring a contradicting algorithm, or a +/// signature that does not verify. The signing input is the exact bytes the +/// signature was computed over, which carry no constraint on their content. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::JWK::from(sourcemeta::core::parse_json( +/// R"JSON({ "kty": "RSA", "n": "", "e": "" })JSON"))}; +/// assert(!key.has_value() || +/// !sourcemeta::core::jws_verify_signature( +/// sourcemeta::core::JWSAlgorithm::RS256, "header.payload", +/// "signature", key.value())); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto jws_verify_signature(const std::optional algorithm, + const std::string_view signing_input, + const std::string_view signature, const JWK &key) + -> bool; + +/// @ingroup jose +/// Verify the signature of a JSON Web Token against a JSON Web Key, returning +/// false rather than throwing whenever the token does not carry a confirmed +/// valid signature for the key. This includes an unrecognized algorithm, a key +/// whose type or curve cannot serve the algorithm, a key declaring a +/// contradicting algorithm, and a signature that does not verify. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const std::string input{ +/// "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY21lIn0.c2ln"}; +/// const auto token{sourcemeta::core::JWT::from(input)}; +/// assert(token.has_value()); +/// const auto key{sourcemeta::core::JWK::from( +/// sourcemeta::core::parse_json(R"JSON({ +/// "kty": "RSA", "n": "", "e": "" +/// })JSON"))}; +/// assert(!key.has_value() || +/// !sourcemeta::core::jwt_verify_signature(token.value(), key.value())); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto jwt_verify_signature(const JWT &token, const JWK &key) -> bool; + +/// @ingroup jose +/// The steps of full token verification that can fail, in the order they are +/// evaluated. +enum class JWTVerificationError : std::uint8_t { + AlgorithmNotAllowed, + UnknownKey, + Signature, + Type, + Issuer, + Subject, + Audience, + Expiration, + NotBefore, + IssuedAt +}; + +/// @ingroup jose +/// Verify a JSON Web Token end to end against a key set, in the mandated order: +/// the algorithm must be in the allow-list, a key is selected by its identifier +/// or, when absent, tried against every compatible key, the signature must +/// verify, and the claims must pass. Returns no value when the token is fully +/// valid, or the first failing step. The type check enforces the access token +/// profile (RFC 9068 Section 2.1) only when an expected type is supplied. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// #include +/// +/// const std::string input{ +/// "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY21lIn0.c2ln"}; +/// const auto token{sourcemeta::core::JWT::from(input)}; +/// assert(token.has_value()); +/// const auto keys{sourcemeta::core::JWKS::from( +/// sourcemeta::core::parse_json(R"JSON({ "keys": [] })JSON"))}; +/// assert(keys.has_value()); +/// const std::array allowed{sourcemeta::core::JWSAlgorithm::RS256}; +/// const auto error{sourcemeta::core::jwt_verify( +/// token.value(), keys.value(), allowed, "acme", "client", +/// std::chrono::system_clock::from_time_t(1500000000))}; +/// assert(error.has_value()); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto jwt_verify( + const JWT &token, const JWKS &keys, + const std::span allowed_algorithms, + const std::string_view expected_issuer, + const std::string_view expected_audience, + const std::chrono::system_clock::time_point now, + const std::chrono::seconds clock_skew = std::chrono::seconds{0}, + const std::optional expected_subject = std::nullopt, + const std::optional expected_type = std::nullopt) + -> std::optional; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/jose/jose_algorithm.cc b/vendor/core/src/core/jose/jose_algorithm.cc new file mode 100644 index 000000000..185877eb3 --- /dev/null +++ b/vendor/core/src/core/jose/jose_algorithm.cc @@ -0,0 +1,35 @@ +#include + +#include // std::optional, std::nullopt +#include // std::string_view + +namespace sourcemeta::core { + +auto to_jws_algorithm(const std::string_view value) noexcept + -> std::optional { + if (value == "RS256") { + return JWSAlgorithm::RS256; + } else if (value == "RS384") { + return JWSAlgorithm::RS384; + } else if (value == "RS512") { + return JWSAlgorithm::RS512; + } else if (value == "PS256") { + return JWSAlgorithm::PS256; + } else if (value == "PS384") { + return JWSAlgorithm::PS384; + } else if (value == "PS512") { + return JWSAlgorithm::PS512; + } else if (value == "ES256") { + return JWSAlgorithm::ES256; + } else if (value == "ES384") { + return JWSAlgorithm::ES384; + } else if (value == "ES512") { + return JWSAlgorithm::ES512; + } else if (value == "EdDSA") { + return JWSAlgorithm::EdDSA; + } else { + return std::nullopt; + } +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwk.cc b/vendor/core/src/core/jose/jose_jwk.cc new file mode 100644 index 000000000..5e2496034 --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwk.cc @@ -0,0 +1,271 @@ +#include + +#include + +#include // std::size_t +#include // std::optional, std::nullopt +#include // std::string_view +#include // std::move, std::unreachable + +namespace { + +const auto HASH_KTY{sourcemeta::core::JSON::Object::hash("kty")}; +const auto HASH_N{sourcemeta::core::JSON::Object::hash("n")}; +const auto HASH_E{sourcemeta::core::JSON::Object::hash("e")}; +const auto HASH_CRV{sourcemeta::core::JSON::Object::hash("crv")}; +const auto HASH_X{sourcemeta::core::JSON::Object::hash("x")}; +const auto HASH_Y{sourcemeta::core::JSON::Object::hash("y")}; +const auto HASH_KID{sourcemeta::core::JSON::Object::hash("kid")}; +const auto HASH_ALG{sourcemeta::core::JSON::Object::hash("alg")}; +const auto HASH_D{sourcemeta::core::JSON::Object::hash("d")}; +const auto HASH_P{sourcemeta::core::JSON::Object::hash("p")}; +const auto HASH_Q{sourcemeta::core::JSON::Object::hash("q")}; +const auto HASH_DP{sourcemeta::core::JSON::Object::hash("dp")}; +const auto HASH_DQ{sourcemeta::core::JSON::Object::hash("dq")}; +const auto HASH_QI{sourcemeta::core::JSON::Object::hash("qi")}; +const auto HASH_OTH{sourcemeta::core::JSON::Object::hash("oth")}; + +// 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; + } + + 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 { + +auto JWK::parse(const JSON &value, JWK &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") { + // A public key must not carry the private parameters (RFC 7518 Section + // 6.3.2), and rejecting them early surfaces dangerous misconfigurations + if (value.try_at("d", HASH_D) != nullptr || + value.try_at("p", HASH_P) != nullptr || + value.try_at("q", HASH_Q) != nullptr || + value.try_at("dp", HASH_DP) != nullptr || + value.try_at("dq", HASH_DQ) != nullptr || + value.try_at("qi", HASH_QI) != nullptr || + value.try_at("oth", HASH_OTH) != nullptr) { + return false; + } + + const auto *modulus{value.try_at("n", HASH_N)}; + const auto *exponent{value.try_at("e", HASH_E)}; + if (modulus == nullptr || !modulus->is_string() || exponent == nullptr || + !exponent->is_string()) { + return false; + } + + auto decoded_modulus{base64url_decode(modulus->to_string())}; + auto decoded_exponent{base64url_decode(exponent->to_string())}; + if (!decoded_modulus.has_value() || decoded_modulus.value().empty() || + !decoded_exponent.has_value() || decoded_exponent.value().empty()) { + return false; + } + + result.type_ = Type::RSA; + parsed_key = + make_rsa_public_key(decoded_modulus.value(), decoded_exponent.value()); + } else if (key_type_value == "EC") { + // A public key must not carry the private parameter (RFC 7518 Section + // 6.2.2) + if (value.try_at("d", HASH_D) != nullptr) { + return false; + } + + const auto *curve{value.try_at("crv", HASH_CRV)}; + const auto *coordinate_x{value.try_at("x", HASH_X)}; + const auto *coordinate_y{value.try_at("y", HASH_Y)}; + if (curve == nullptr || !curve->is_string() || coordinate_x == nullptr || + !coordinate_x->is_string() || coordinate_y == nullptr || + !coordinate_y->is_string()) { + return false; + } + + const auto coordinate_bytes{ec_coordinate_bytes(curve->to_string())}; + if (!coordinate_bytes.has_value()) { + return false; + } + + auto decoded_x{base64url_decode(coordinate_x->to_string())}; + auto decoded_y{base64url_decode(coordinate_y->to_string())}; + if (!decoded_x.has_value() || + decoded_x.value().size() != coordinate_bytes.value() || + !decoded_y.has_value() || + decoded_y.value().size() != coordinate_bytes.value()) { + return false; + } + + result.type_ = Type::EllipticCurve; + result.curve_ = curve->to_string(); + parsed_key = make_ec_public_key(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) + if (value.try_at("d", HASH_D) != nullptr) { + return false; + } + + const auto *curve{value.try_at("crv", HASH_CRV)}; + const auto *public_key{value.try_at("x", HASH_X)}; + if (curve == nullptr || !curve->is_string() || public_key == nullptr || + !public_key->is_string()) { + return false; + } + + const auto key_bytes{okp_key_bytes(curve->to_string())}; + if (!key_bytes.has_value()) { + return false; + } + + auto decoded_public_key{base64url_decode(public_key->to_string())}; + if (!decoded_public_key.has_value() || + decoded_public_key.value().size() != key_bytes.value()) { + return false; + } + + result.type_ = Type::OctetKeyPair; + result.curve_ = curve->to_string(); + parsed_key = make_eddsa_public_key(to_edwards_curve(result.curve_), + decoded_public_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() && + algorithm_matches_key(parsed.value(), result.type_, result.curve_)) { + result.algorithm_ = parsed; + } + } + + // The platform key is built once when the material is decoded, so + // verification reuses it. A key that cannot be turned into one stays null and + // simply fails to verify + result.public_key_ = std::move(parsed_key); + return true; +} + +JWK::JWK(const JSON &value) { + if (!parse(value, *this)) { + throw JWKParseError{}; + } +} + +// The key material is base64url-decoded into fresh storage, so there is nothing +// to move out of the source value. The rvalue overloads exist for call-site +// symmetry and delegate to the lvalue path +JWK::JWK(JSON &&value) : JWK{value} {} + +auto JWK::from(const JSON &value) -> std::optional { + JWK result; + if (parse(value, result)) { + return result; + } + + return std::nullopt; +} + +auto JWK::from(JSON &&value) -> std::optional { return from(value); } + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwks.cc b/vendor/core/src/core/jose/jose_jwks.cc new file mode 100644 index 000000000..17afbe79b --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwks.cc @@ -0,0 +1,72 @@ +#include + +#include // std::optional, std::nullopt +#include // std::string_view +#include // std::move + +namespace { + +const auto HASH_KEYS{sourcemeta::core::JSON::Object::hash("keys")}; + +} // namespace + +namespace sourcemeta::core { + +auto JWKS::parse(const JSON &value, JWKS &result) -> bool { + if (!value.is_object()) { + return false; + } + + const auto *keys{value.try_at("keys", HASH_KEYS)}; + if (keys == nullptr || !keys->is_array()) { + return false; + } + + // Individual keys that fail to parse are skipped so that one exotic key + // cannot break verification of tokens signed by the others (RFC 7517 + // Section 5) + for (const auto &entry : keys->as_array()) { + auto key{JWK::from(entry)}; + if (key.has_value()) { + result.keys_.push_back(std::move(key).value()); + } + } + + // An empty set, or one whose keys all failed to parse, is not usable + return !result.keys_.empty(); +} + +JWKS::JWKS(const JSON &value) { + if (!parse(value, *this)) { + throw JWKSParseError{}; + } +} + +// The keys are decoded into fresh storage, so there is nothing to move out of +// the source value. The rvalue overloads exist for call-site symmetry and +// delegate to the lvalue path +JWKS::JWKS(JSON &&value) : JWKS{value} {} + +auto JWKS::from(const JSON &value) -> std::optional { + JWKS result; + if (parse(value, result)) { + return result; + } + + return std::nullopt; +} + +auto JWKS::from(JSON &&value) -> std::optional { return from(value); } + +auto JWKS::find(const std::string_view key_id) const noexcept -> const JWK * { + for (const auto &key : this->keys_) { + const auto candidate{key.key_id()}; + if (candidate.has_value() && candidate.value() == key_id) { + return &key; + } + } + + return nullptr; +} + +} // 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 new file mode 100644 index 000000000..41555a61e --- /dev/null +++ b/vendor/core/src/core/jose/jose_jws_verify_signature.cc @@ -0,0 +1,101 @@ +#include + +#include + +#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, + const std::string_view signing_input, + const std::string_view signature, const JWK &key) + -> bool { + if (!algorithm.has_value()) { + return false; + } + + // 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.value()) { + 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()) { + 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()), + 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()), + 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" && + ecdsa_verify(*public_key, SignatureHashFunction::SHA256, + signing_input, signature); + case JWSAlgorithm::ES384: + return 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" && + 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 && + eddsa_verify(*public_key, signing_input, signature); + } + + std::unreachable(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwt.cc b/vendor/core/src/core/jose/jose_jwt.cc new file mode 100644 index 000000000..20bd6e1d0 --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwt.cc @@ -0,0 +1,197 @@ +#include + +#include +#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 + +namespace { + +const auto HASH_ALG{sourcemeta::core::JSON::Object::hash("alg")}; +const auto HASH_CRIT{sourcemeta::core::JSON::Object::hash("crit")}; +const auto HASH_KID{sourcemeta::core::JSON::Object::hash("kid")}; +const auto HASH_TYP{sourcemeta::core::JSON::Object::hash("typ")}; +const auto HASH_ISS{sourcemeta::core::JSON::Object::hash("iss")}; +const auto HASH_SUB{sourcemeta::core::JSON::Object::hash("sub")}; +const auto HASH_AUD{sourcemeta::core::JSON::Object::hash("aud")}; +const auto HASH_EXP{sourcemeta::core::JSON::Object::hash("exp")}; +const auto HASH_NBF{sourcemeta::core::JSON::Object::hash("nbf")}; +const auto HASH_IAT{sourcemeta::core::JSON::Object::hash("iat")}; +const auto HASH_JTI{sourcemeta::core::JSON::Object::hash("jti")}; + +auto string_claim(const sourcemeta::core::JSON &object, + const sourcemeta::core::JSON::StringView name, + const sourcemeta::core::JSON::Object::hash_type hash) + -> std::optional { + const auto *member{object.try_at(name, hash)}; + if (member == nullptr || !member->is_string()) { + return std::nullopt; + } + + return std::string_view{member->to_string()}; +} + +auto date_claim(const sourcemeta::core::JSON &object, + const sourcemeta::core::JSON::StringView name, + const sourcemeta::core::JSON::Object::hash_type hash) + -> std::optional { + const auto *member{object.try_at(name, hash)}; + if (member == nullptr || !member->is_number()) { + return std::nullopt; + } + + // A NumericDate is the number of seconds since the Unix epoch, possibly + // non-integer (RFC 7519 Section 2). A decimal-backed number (such as the + // exponent form "1e9") whose magnitude exceeds the range of a double cannot + // stand for a usable timestamp, and untrusted input must not abort + double seconds{0}; + try { + seconds = member->as_real(); + } catch (const std::out_of_range &) { + return std::nullopt; + } + + return sourcemeta::core::from_unix_timestamp( + std::chrono::duration{seconds}); +} + +} // namespace + +namespace sourcemeta::core { + +auto JWT::parse(const std::string_view input, JWT &result) -> bool { + // The compact serialization is exactly three base64url segments joined by + // dots (RFC 7515 Section 7.1) + const auto first{split_once(input, '.')}; + if (!first.has_value()) { + return false; + } + + const auto second{split_once(first->second, '.')}; + if (!second.has_value()) { + return false; + } + + const auto header_segment{first->first}; + const auto payload_segment{second->first}; + const auto signature_segment{second->second}; + if (signature_segment.find('.') != std::string_view::npos) { + return false; + } + + auto header_bytes{base64url_decode(header_segment)}; + auto payload_bytes{base64url_decode(payload_segment)}; + auto signature_bytes{base64url_decode(signature_segment)}; + if (!header_bytes.has_value() || !payload_bytes.has_value() || + !signature_bytes.has_value()) { + return false; + } + + auto header_json{try_parse_json(header_bytes.value())}; + auto payload_json{try_parse_json(payload_bytes.value())}; + if (!header_json.has_value() || !header_json.value().is_object() || + !payload_json.has_value() || !payload_json.value().is_object()) { + 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)}; + if (algorithm == nullptr || !algorithm->is_string()) { + return false; + } + + // Critical header extensions are not understood and must be rejected (RFC + // 7515 Section 4.1.11) + if (header_json.value().try_at("crit", HASH_CRIT) != nullptr) { + return false; + } + + result.algorithm_ = to_jws_algorithm(algorithm->to_string()); + result.signing_input_ = + input.substr(0, header_segment.size() + payload_segment.size() + 1); + result.signature_ = std::move(signature_bytes).value(); + result.header_ = std::move(header_json).value(); + result.payload_ = std::move(payload_json).value(); + return true; +} + +JWT::JWT(const std::string_view input) { + if (!parse(input, *this)) { + throw JWTParseError{}; + } +} + +auto JWT::from(const std::string_view input) -> std::optional { + JWT result; + if (parse(input, result)) { + return result; + } + + return std::nullopt; +} + +auto JWT::key_id() const noexcept -> std::optional { + return string_claim(this->header_, "kid", HASH_KID); +} + +auto JWT::type() const noexcept -> std::optional { + return string_claim(this->header_, "typ", HASH_TYP); +} + +auto JWT::issuer() const noexcept -> std::optional { + return string_claim(this->payload_, "iss", HASH_ISS); +} + +auto JWT::subject() const noexcept -> std::optional { + return string_claim(this->payload_, "sub", HASH_SUB); +} + +auto JWT::token_id() const noexcept -> std::optional { + return string_claim(this->payload_, "jti", HASH_JTI); +} + +auto JWT::has_audience(const std::string_view audience) const noexcept -> bool { + const auto *member{this->payload_.try_at("aud", HASH_AUD)}; + if (member == nullptr) { + return false; + } + + // The audience claim is either a single string or an array of strings (RFC + // 7519 Section 4.1.3) + if (member->is_string()) { + return member->to_string() == audience; + } + + if (member->is_array()) { + for (const auto &element : member->as_array()) { + if (element.is_string() && element.to_string() == audience) { + return true; + } + } + } + + return false; +} + +auto JWT::expires_at() const + -> std::optional { + return date_claim(this->payload_, "exp", HASH_EXP); +} + +auto JWT::not_before() const + -> std::optional { + return date_claim(this->payload_, "nbf", HASH_NBF); +} + +auto JWT::issued_at() const + -> std::optional { + return date_claim(this->payload_, "iat", HASH_IAT); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwt_check_claims.cc b/vendor/core/src/core/jose/jose_jwt_check_claims.cc new file mode 100644 index 000000000..20ba883e5 --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwt_check_claims.cc @@ -0,0 +1,69 @@ +#include + +#include // std::chrono::seconds, std::chrono::system_clock +#include // std::optional, std::nullopt +#include // std::string_view + +namespace sourcemeta::core { + +auto jwt_check_claims(const JWT &token, const std::string_view expected_issuer, + const std::string_view expected_audience, + const std::chrono::system_clock::time_point now, + const std::chrono::seconds clock_skew, + const std::optional expected_subject) + -> std::optional { + // The issuer must be present and match the expected value (RFC 7519 Section + // 4.1.1) + const auto issuer{token.issuer()}; + if (!issuer.has_value() || issuer.value() != expected_issuer) { + return JWTClaimError::Issuer; + } + + // The subject is checked only when the caller pins one, since it is the + // authenticated principal that many flows accept as any valid identity (RFC + // 7519 Section 4.1.2) + if (expected_subject.has_value()) { + const auto subject{token.subject()}; + if (!subject.has_value() || subject.value() != expected_subject.value()) { + return JWTClaimError::Subject; + } + } + + // The audience must be present and contain the expected value (RFC 7519 + // Section 4.1.3) + if (!token.has_audience(expected_audience)) { + return JWTClaimError::Audience; + } + + // 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) { + return JWTClaimError::Expiration; + } + + // The not-before time, when present, must be a usable NumericDate that is not + // in the future. A claim that is present but malformed fails closed rather + // than being ignored (RFC 7519 Section 4.1.5) + 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) { + return JWTClaimError::NotBefore; + } + } + + // The issued-at time, when present, must be a usable NumericDate that is not + // 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) { + return JWTClaimError::IssuedAt; + } + } + + return std::nullopt; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwt_verify.cc b/vendor/core/src/core/jose/jose_jwt_verify.cc new file mode 100644 index 000000000..1590999bd --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwt_verify.cc @@ -0,0 +1,126 @@ +#include + +#include + +#include // std::ranges::find +#include // std::chrono::seconds, std::chrono::system_clock +#include // std::optional, std::nullopt +#include // std::span +#include // std::string_view +#include // std::unreachable + +namespace { + +// RFC 7519 Section 5.1: "a recipient using the media type value MUST treat it +// as if `application/` were prepended to any `typ` value not containing a `/`". +// Removing an explicit `application/` prefix, when nothing else in the value +// contains a slash, lets the compact `at+jwt` form and the full +// `application/at+jwt` form compare equal +auto strip_application_prefix(const std::string_view value) + -> std::string_view { + constexpr std::string_view prefix{"application/"}; + if (value.size() > prefix.size() && + sourcemeta::core::equals_ignore_case(value.substr(0, prefix.size()), + prefix) && + value.find('/', prefix.size()) == std::string_view::npos) { + return value.substr(prefix.size()); + } + + return value; +} + +auto to_verification_error(const sourcemeta::core::JWTClaimError error) + -> sourcemeta::core::JWTVerificationError { + using sourcemeta::core::JWTClaimError; + using sourcemeta::core::JWTVerificationError; + switch (error) { + case JWTClaimError::Issuer: + return JWTVerificationError::Issuer; + case JWTClaimError::Subject: + return JWTVerificationError::Subject; + case JWTClaimError::Audience: + return JWTVerificationError::Audience; + case JWTClaimError::Expiration: + return JWTVerificationError::Expiration; + case JWTClaimError::NotBefore: + return JWTVerificationError::NotBefore; + case JWTClaimError::IssuedAt: + return JWTVerificationError::IssuedAt; + } + + std::unreachable(); +} + +} // namespace + +namespace sourcemeta::core { + +auto jwt_verify(const JWT &token, const JWKS &keys, + const std::span allowed_algorithms, + const std::string_view expected_issuer, + const std::string_view expected_audience, + const std::chrono::system_clock::time_point now, + const std::chrono::seconds clock_skew, + const std::optional expected_subject, + const std::optional expected_type) + -> std::optional { + // The algorithm allow-list is enforced before any key is touched, per step 3 + // of the Sourcemeta One validation algorithm + const auto algorithm{token.algorithm()}; + if (!algorithm.has_value() || + std::ranges::find(allowed_algorithms, algorithm.value()) == + allowed_algorithms.end()) { + return JWTVerificationError::AlgorithmNotAllowed; + } + + // A token names its key through `kid` (RFC 7515 Section 4.1.4). When it does + // not, every key in the set is tried, since some providers omit it when they + // publish a single key. A missing or non-verifying key is reported as unknown + // rather than as a signature failure so that downstream can refetch the set, + // except when the named key is present but its signature does not verify + const auto key_id{token.key_id()}; + if (key_id.has_value()) { + const auto *key{keys.find(key_id.value())}; + if (key == nullptr) { + return JWTVerificationError::UnknownKey; + } + + if (!jwt_verify_signature(token, *key)) { + return JWTVerificationError::Signature; + } + } else { + bool verified{false}; + for (const auto &key : keys) { + if (jwt_verify_signature(token, key)) { + verified = true; + break; + } + } + + if (!verified) { + return JWTVerificationError::UnknownKey; + } + } + + // The type is a header concern checked only on an authenticated token, which + // is how the access token profile is enforced (RFC 9068 Section 2.1) + if (expected_type.has_value()) { + const auto type{token.type()}; + if (!type.has_value() || + !equals_ignore_case(strip_application_prefix(type.value()), + strip_application_prefix(expected_type.value()))) { + return JWTVerificationError::Type; + } + } + + const auto claim_error{jwt_check_claims(token, expected_issuer, + expected_audience, now, clock_skew, + expected_subject)}; + if (claim_error.has_value()) { + return to_verification_error(claim_error.value()); + } + + return std::nullopt; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwt_verify_signature.cc b/vendor/core/src/core/jose/jose_jwt_verify_signature.cc new file mode 100644 index 000000000..a0995879d --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwt_verify_signature.cc @@ -0,0 +1,10 @@ +#include + +namespace sourcemeta::core { + +auto jwt_verify_signature(const JWT &token, const JWK &key) -> bool { + return jws_verify_signature(token.algorithm(), token.signing_input(), + token.signature(), key); +} + +} // namespace sourcemeta::core 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 eec6bb84f..f294776f4 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json.h @@ -19,6 +19,7 @@ #include // std::basic_ifstream #include // std::initializer_list #include // std::basic_istream +#include // std::optional #include // std::basic_ostream #include // std::ostringstream #include // std::basic_string @@ -75,6 +76,25 @@ SOURCEMETA_CORE_JSON_EXPORT auto parse_json( const std::basic_string_view input) -> JSON; +/// @ingroup json +/// +/// Create a JSON document from a JSON string, returning no value instead of +/// throwing when the input is not valid JSON. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto document{sourcemeta::core::try_parse_json("[ 1, 2, 3 ]")}; +/// assert(document.has_value()); +/// assert(document.value().is_array()); +/// assert(!sourcemeta::core::try_parse_json("[ 1, 2,").has_value()); +/// ``` +SOURCEMETA_CORE_JSON_EXPORT +auto try_parse_json( + const std::basic_string_view input) + -> std::optional; + /// @ingroup json /// /// Create a JSON document from a C++ standard input stream, passing your own 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 e862de2a7..392bae36b 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 @@ -21,9 +21,11 @@ #include // std::int64_t, std::uint8_t #include // std::less, std::reference_wrapper, std::function #include // std::initializer_list +#include // std::numeric_limits #include // std::allocator #include // std::set #include // std::basic_istringstream +#include // std::out_of_range #include // std::basic_string, std::char_traits #include // std::basic_string_view #include // std::is_same_v, std::remove_cvref_t @@ -774,7 +776,9 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { } /// Get the JSON numeric document as a real number if it is not one already. - /// For example: + /// A decimal whose magnitude does not fit in a double throws + /// `std::out_of_range`, matching the behaviour of converting that decimal + /// directly. For example: /// /// ```cpp /// #include @@ -783,16 +787,21 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { /// const sourcemeta::core::JSON document{5}; /// assert(document.as_real() == 5.0); /// ``` - [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto as_real() const noexcept - -> Real { + [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto as_real() const -> Real { assert(this->is_number()); - return this->is_real() ? this->to_real() - : static_cast(this->to_integer()); + if (this->is_real()) { + return this->to_real(); + } else if (this->is_integer()) { + return static_cast(this->to_integer()); + } else { + return this->to_decimal().to_double(); + } } /// Get the JSON numeric document as an integer number if it is not one - /// already. If the number is a real number, truncation will take place. For - /// example: + /// already. If the number is a real number, truncation will take place. A + /// value whose magnitude does not fit in a 64-bit integer throws + /// `std::out_of_range`. For example: /// /// ```cpp /// #include @@ -801,13 +810,28 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { /// const sourcemeta::core::JSON document{5.3}; /// assert(document.as_integer() == 5); /// ``` - [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto as_integer() const noexcept + [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto as_integer() const -> Integer { assert(this->is_number()); if (this->is_integer()) { return this->to_integer(); + } else if (this->is_real()) { + const auto truncated{std::trunc(this->to_real())}; + if (truncated < static_cast(std::numeric_limits::min()) || + truncated >= static_cast(std::numeric_limits::max())) { + throw std::out_of_range{ + "The real number does not fit in a 64-bit integer"}; + } + + return static_cast(truncated); } else { - return static_cast(std::trunc(this->to_real())); + const auto integral{this->to_decimal().to_integral()}; + if (!integral.is_int64()) { + throw std::out_of_range{ + "The decimal number does not fit in a 64-bit integer"}; + } + + return integral.to_int64(); } } diff --git a/vendor/core/src/core/json/json.cc b/vendor/core/src/core/json/json.cc index d4229a7f3..27193d0c7 100644 --- a/vendor/core/src/core/json/json.cc +++ b/vendor/core/src/core/json/json.cc @@ -8,54 +8,79 @@ #include "parser.h" #include "stringify.h" -#include // assert -#include // std::uint64_t -#include // std::filesystem -#include // std::basic_istream -#include // std::numeric_limits -#include // std::basic_ostream -#include // std::cmp_greater -#include // std::vector +#include // assert +#include // std::uint64_t +#include // std::filesystem +#include // std::basic_istream +#include // std::numeric_limits +#include // std::optional, std::nullopt +#include // std::basic_ostream +#include // std::conditional_t +#include // std::cmp_greater +#include // std::vector namespace sourcemeta::core { +template static auto internal_parse_json(const char *&cursor, const char *end, std::uint64_t &line, std::uint64_t &column, const JSON::ParseCallback &callback, const bool track_positions, JSON &output) - -> void { + -> std::conditional_t { const char *buffer_start{cursor}; // Tape entries address the input with 32-bit offsets and lengths, so a larger // input cannot be represented without truncation if (std::cmp_greater(end - cursor, std::numeric_limits::max())) { - throw JSONParseError(line, column); + if constexpr (should_throw) { + throw JSONParseError(line, column); + } else { + return false; + } } std::vector tape; tape.reserve(static_cast(end - cursor) / 8); - if (callback || track_positions) { - scan_json(cursor, end, buffer_start, line, column, tape); + + if constexpr (should_throw) { + if (callback || track_positions) { + scan_json(cursor, end, buffer_start, line, column, tape); + } else { + // Re-scan with position tracking on failure for a precise error message + try { + scan_json(cursor, end, buffer_start, line, column, tape); + } catch (const JSONParseError &) { + cursor = buffer_start; + tape.clear(); + line = 1; + column = 0; + scan_json(cursor, end, buffer_start, line, column, tape); + } + } + construct_json(buffer_start, tape, callback, output); } else { + // Both the scanning and the construction phases signal failure by throwing, + // so a single boundary around them reports either as no value try { - scan_json(cursor, end, buffer_start, line, column, tape); + if (callback || track_positions) { + scan_json(cursor, end, buffer_start, line, column, tape); + } else { + scan_json(cursor, end, buffer_start, line, column, tape); + } + construct_json(buffer_start, tape, callback, output); } catch (const JSONParseError &) { - cursor = buffer_start; - tape.clear(); - line = 1; - column = 0; - scan_json(cursor, end, buffer_start, line, column, tape); + return false; } + return true; } - construct_json(buffer_start, tape, callback, output); } static auto internal_parse_json(const char *&cursor, const char *end, std::uint64_t &line, std::uint64_t &column, const bool track_positions) -> JSON { JSON output{nullptr}; - internal_parse_json(cursor, end, line, column, nullptr, track_positions, - output); + internal_parse_json(cursor, end, line, column, nullptr, track_positions, + output); return output; } @@ -110,6 +135,21 @@ auto parse_json( false); } +auto try_parse_json( + const std::basic_string_view input) + -> std::optional { + std::uint64_t line{1}; + std::uint64_t column{0}; + const char *cursor{input.empty() ? "" : input.data()}; + JSON output{nullptr}; + if (internal_parse_json(cursor, cursor + input.size(), line, column, + nullptr, false, output)) { + return output; + } + + return std::nullopt; +} + auto read_json(const std::filesystem::path &path) -> JSON { try { return parse_json(read_file_to_string(path)); @@ -127,7 +167,7 @@ auto parse_json(std::basic_istream &stream, const auto input{read_to_string(stream)}; const char *cursor{input.data()}; const char *end{input.data() + input.size()}; - internal_parse_json(cursor, end, line, column, callback, true, output); + internal_parse_json(cursor, end, line, column, callback, true, output); if (start_position != static_cast(-1)) { const auto consumed{static_cast(cursor - input.data())}; stream.clear(); @@ -140,8 +180,8 @@ auto parse_json( std::uint64_t &line, std::uint64_t &column, JSON &output, const JSON::ParseCallback &callback) -> void { const char *cursor{input.empty() ? "" : input.data()}; - internal_parse_json(cursor, cursor + input.size(), line, column, callback, - true, output); + internal_parse_json(cursor, cursor + input.size(), line, column, + callback, true, output); } // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) @@ -153,7 +193,7 @@ auto parse_json(std::basic_istream &stream, const char *end{input.data() + input.size()}; std::uint64_t line{1}; std::uint64_t column{0}; - internal_parse_json(cursor, end, line, column, callback, false, output); + internal_parse_json(cursor, end, line, column, callback, false, output); if (start_position != static_cast(-1)) { const auto consumed{static_cast(cursor - input.data())}; stream.clear(); @@ -167,8 +207,8 @@ auto parse_json( 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); + internal_parse_json(cursor, cursor + input.size(), line, column, + callback, false, output); } 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 5231525f8..26f9dfa02 100644 --- a/vendor/core/src/core/json/json_value.cc +++ b/vendor/core/src/core/json/json_value.cc @@ -432,6 +432,9 @@ 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)) { @@ -489,6 +492,9 @@ auto JSON::operator>=(const JSON &other) const noexcept -> 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)) { diff --git a/vendor/core/src/core/time/CMakeLists.txt b/vendor/core/src/core/time/CMakeLists.txt index bfd35d613..0de159ada 100644 --- a/vendor/core/src/core/time/CMakeLists.txt +++ b/vendor/core/src/core/time/CMakeLists.txt @@ -1,7 +1,8 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME time SOURCES helpers.h imf_fixdate.cc rfc850_date.cc asctime.cc - rfc3339_datetime.cc rfc3339_fulldate.cc rfc3339_fulltime.cc - rfc3339_partialtime_no_secfrac.cc rfc3339_duration.cc) + unix_timestamp.cc rfc3339_datetime.cc rfc3339_fulldate.cc + rfc3339_fulltime.cc rfc3339_partialtime_no_secfrac.cc + rfc3339_duration.cc) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME time) diff --git a/vendor/core/src/core/time/include/sourcemeta/core/time.h b/vendor/core/src/core/time/include/sourcemeta/core/time.h index 26acc75a0..7751f70f1 100644 --- a/vendor/core/src/core/time/include/sourcemeta/core/time.h +++ b/vendor/core/src/core/time/include/sourcemeta/core/time.h @@ -118,6 +118,45 @@ SOURCEMETA_CORE_TIME_EXPORT auto from_asctime(const std::string_view value) noexcept -> std::optional; +/// @ingroup time +/// Convert a POSIX timestamp, the number of seconds since the Unix epoch +/// ignoring leap seconds and possibly fractional, into a time point, returning +/// no value when the timestamp is not representable. Fractional seconds finer +/// than the time point's tick resolution are truncated towards zero. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto point{ +/// sourcemeta::core::from_unix_timestamp(std::chrono::duration{0})}; +/// assert(point.has_value()); +/// assert(point.value() == std::chrono::system_clock::from_time_t(0)); +/// ``` +SOURCEMETA_CORE_TIME_EXPORT +auto from_unix_timestamp(const std::chrono::duration seconds) noexcept + -> std::optional; + +/// @ingroup time +/// Convert a time point into a POSIX timestamp, the number of seconds since +/// the Unix epoch ignoring leap seconds. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto point{std::chrono::system_clock::from_time_t(0)}; +/// assert(sourcemeta::core::to_unix_timestamp(point) == +/// std::chrono::duration{0}); +/// ``` +SOURCEMETA_CORE_TIME_EXPORT +auto to_unix_timestamp( + const std::chrono::system_clock::time_point time) noexcept + -> std::chrono::duration; + /// @ingroup time /// Check whether the given string is a valid date-time value per RFC 3339 /// Section 5.6 (Internet Date/Time Format). This implements the full diff --git a/vendor/core/src/core/time/unix_timestamp.cc b/vendor/core/src/core/time/unix_timestamp.cc new file mode 100644 index 000000000..e7daeb84f --- /dev/null +++ b/vendor/core/src/core/time/unix_timestamp.cc @@ -0,0 +1,41 @@ +#include + +#include // std::chrono::duration, std::chrono::system_clock +#include // std::isfinite +#include // std::optional, std::nullopt + +namespace sourcemeta::core { + +auto from_unix_timestamp(const std::chrono::duration seconds) noexcept + -> std::optional { + if (!std::isfinite(seconds.count())) { + return std::nullopt; + } + + // Reject timestamps outside the clock's representable window, leaving a one + // second guard so that the conversion to the clock's native tick cannot + // overflow at the boundary + constexpr auto maximum{ + std::chrono::duration_cast>( + std::chrono::system_clock::duration::max()) - + std::chrono::duration{1}}; + constexpr auto minimum{ + std::chrono::duration_cast>( + std::chrono::system_clock::duration::min()) + + std::chrono::duration{1}}; + if (seconds < minimum || seconds > maximum) { + return std::nullopt; + } + + return std::chrono::system_clock::time_point{ + std::chrono::duration_cast(seconds)}; +} + +auto to_unix_timestamp( + const std::chrono::system_clock::time_point time) noexcept + -> std::chrono::duration { + return std::chrono::duration_cast>( + time.time_since_epoch()); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/lang/numeric/include/sourcemeta/core/numeric_uint128.h b/vendor/core/src/lang/numeric/include/sourcemeta/core/numeric_uint128.h index df07a78d3..aea02f473 100644 --- a/vendor/core/src/lang/numeric/include/sourcemeta/core/numeric_uint128.h +++ b/vendor/core/src/lang/numeric/include/sourcemeta/core/numeric_uint128.h @@ -55,17 +55,35 @@ struct uint128_t { return *this; } + auto operator-=(const uint128_t &other) noexcept -> uint128_t & { + const auto old_low = this->low; + this->low -= other.low; + this->high -= other.high + (old_low < other.low ? 1 : 0); + return *this; + } + auto operator*=(const uint128_t &other) noexcept -> uint128_t & { *this = *this * other; return *this; } + auto operator%=(const uint128_t &other) noexcept -> uint128_t & { + *this = *this % other; + return *this; + } + friend auto operator+(uint128_t left, const uint128_t &right) noexcept -> uint128_t { left += right; return left; } + friend auto operator-(uint128_t left, const uint128_t &right) noexcept + -> uint128_t { + left -= right; + return left; + } + friend auto operator*(const uint128_t &left, const uint128_t &right) noexcept -> uint128_t { std::uint64_t result_high; diff --git a/vendor/core/src/lang/text/include/sourcemeta/core/text.h b/vendor/core/src/lang/text/include/sourcemeta/core/text.h index 1f6e37492..a5dab4ff8 100644 --- a/vendor/core/src/lang/text/include/sourcemeta/core/text.h +++ b/vendor/core/src/lang/text/include/sourcemeta/core/text.h @@ -398,6 +398,22 @@ auto hex_to_bytes(const std::string_view input, SOURCEMETA_CORE_TEXT_EXPORT auto bytes_to_hex(const std::string_view input) -> std::string; +/// @ingroup text +/// +/// Return whether two strings are equal under ASCII case-insensitive +/// comparison. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::equals_ignore_case("Hello", "hELLO")); +/// assert(!sourcemeta::core::equals_ignore_case("foo", "bar")); +/// ``` +SOURCEMETA_CORE_TEXT_EXPORT +auto equals_ignore_case(const std::string_view left, + const std::string_view right) noexcept -> bool; + /// @ingroup text /// /// Return `input` with `suffix` removed from the end under ASCII diff --git a/vendor/core/src/lang/text/text.cc b/vendor/core/src/lang/text/text.cc index 52df89d06..6c2c6efa3 100644 --- a/vendor/core/src/lang/text/text.cc +++ b/vendor/core/src/lang/text/text.cc @@ -174,6 +174,21 @@ auto split_once(const std::string_view input, return std::pair{before, after}; } +auto equals_ignore_case(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 (to_lowercase(left[index]) != to_lowercase(right[index])) { + return false; + } + } + + return true; +} + auto remove_suffix_ignore_case(const std::string_view input, const std::string_view suffix) noexcept -> std::string_view { diff --git a/vendor/jsonbinpack/CMakeLists.txt b/vendor/jsonbinpack/CMakeLists.txt new file mode 100644 index 000000000..1a5b53ab9 --- /dev/null +++ b/vendor/jsonbinpack/CMakeLists.txt @@ -0,0 +1,87 @@ +cmake_minimum_required(VERSION 3.16) +project(jsonbinpack VERSION 0.0.1 LANGUAGES CXX + DESCRIPTION "\ +A space-efficient open-source binary JSON serialization \ +format based on JSON Schema with \ +both schema-driven and schema-less support." + HOMEPAGE_URL "https://jsonbinpack.sourcemeta.com") +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") + +# Options +option(JSONBINPACK_RUNTIME "Build the JSON BinPack runtime" ON) +option(JSONBINPACK_COMPILER "Build the JSON BinPack compiler" ON) +option(JSONBINPACK_TESTS "Build the JSON BinPack tests" OFF) +option(JSONBINPACK_INSTALL "Install the JSON BinPack library" ON) +option(JSONBINPACK_DOCS "Build the JSON BinPack documentation" OFF) +option(JSONBINPACK_ADDRESS_SANITIZER "Build JSON BinPack with an address sanitizer" OFF) +option(JSONBINPACK_UNDEFINED_SANITIZER "Build JSON BinPack with an undefined behavior sanitizer" OFF) + +find_package(Core REQUIRED) +find_package(Blaze REQUIRED) + +if(JSONBINPACK_INSTALL) + include(GNUInstallDirs) + include(CMakePackageConfigHelpers) + configure_package_config_file( + config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config.cmake" + INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") + write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config-version.cmake" + COMPATIBILITY SameMajorVersion) + install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config-version.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config.cmake" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}" + COMPONENT sourcemeta_jsonbinpack_dev) +endif() + +# Runtime +if(JSONBINPACK_RUNTIME) + add_subdirectory(src/runtime) +endif() + +# Compiler +if(JSONBINPACK_COMPILER) + add_subdirectory(src/compiler) +endif() + +if(JSONBINPACK_ADDRESS_SANITIZER) + sourcemeta_sanitizer(TYPE address) +elseif(JSONBINPACK_UNDEFINED_SANITIZER) + sourcemeta_sanitizer(TYPE undefined) +endif() + +if(JSONBINPACK_DOCS) + sourcemeta_target_doxygen(CONFIG "${PROJECT_SOURCE_DIR}/doxygen/Doxyfile.in" + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/www") +endif() + +if(PROJECT_IS_TOP_LEVEL) + sourcemeta_target_clang_format(SOURCES + src/*.h src/*.cc + test/*.h test/*.cc) +endif() + +# Testing +if(JSONBINPACK_TESTS) + enable_testing() + + if(JSONBINPACK_RUNTIME) + add_subdirectory(test/runtime) + endif() + + if(JSONBINPACK_COMPILER) + add_subdirectory(test/compiler) + endif() + + add_subdirectory(test/e2e) + + if(PROJECT_IS_TOP_LEVEL) + # Otherwise we need the child project to link + # against the sanitizers too. + if(NOT JSONBINPACK_ADDRESS_SANITIZER AND NOT JSONBINPACK_UNDEFINED_SANITIZER) + add_subdirectory(test/packaging) + endif() + endif() +endif() diff --git a/vendor/jsonbinpack/DEPENDENCIES b/vendor/jsonbinpack/DEPENDENCIES new file mode 100644 index 000000000..7947a8722 --- /dev/null +++ b/vendor/jsonbinpack/DEPENDENCIES @@ -0,0 +1,4 @@ +vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 +core https://github.com/sourcemeta/core bb1c78e8fa148a2ece951bb776798a43fe328821 +blaze https://github.com/sourcemeta/blaze 04832d45bf4327d4ec874fa67f339797cd49b375 +bootstrap https://github.com/twbs/bootstrap 1a6fdfae6be09b09eaced8f0e442ca6f7680a61e diff --git a/vendor/jsonbinpack/LICENSE b/vendor/jsonbinpack/LICENSE new file mode 100644 index 000000000..9348973bd --- /dev/null +++ b/vendor/jsonbinpack/LICENSE @@ -0,0 +1,12 @@ +This software is dual-licensed: you can redistribute it and/or modify it under +the terms of the GNU Affero General Public License as published by the Free +Software Foundation, either version 3 of the License, or (at your option) any +later version. For the terms of this license, see +. + +You are free to use this software under the terms of the GNU Affero General +Public License WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +Alternatively, you can use this software under a commercial license, as set out +in . diff --git a/vendor/jsonbinpack/cmake/FindBlaze.cmake b/vendor/jsonbinpack/cmake/FindBlaze.cmake new file mode 100644 index 000000000..53fb3fcc3 --- /dev/null +++ b/vendor/jsonbinpack/cmake/FindBlaze.cmake @@ -0,0 +1,17 @@ +if(NOT Blaze_FOUND) + if(JSONBINPACK_INSTALL) + set(SOURCEMETA_BLAZE_INSTALL ON CACHE BOOL "enable installation") + else() + set(SOURCEMETA_BLAZE_INSTALL OFF CACHE BOOL "disable installation") + endif() + + set(BLAZE_TEST OFF CACHE BOOL "disable") + set(BLAZE_CONFIGURATION OFF CACHE BOOL "disable") + set(BLAZE_CODEGEN OFF CACHE BOOL "disable") + set(BLAZE_DOCUMENTATION OFF CACHE BOOL "disable") + set(BLAZE_EDITOR OFF CACHE BOOL "disable") + set(BLAZE_CONTRIB OFF CACHE BOOL "disable") + add_subdirectory("${PROJECT_SOURCE_DIR}/vendor/blaze") + include(Sourcemeta) + set(Blaze_FOUND ON) +endif() diff --git a/vendor/jsonbinpack/cmake/FindCore.cmake b/vendor/jsonbinpack/cmake/FindCore.cmake new file mode 100644 index 000000000..6985accb1 --- /dev/null +++ b/vendor/jsonbinpack/cmake/FindCore.cmake @@ -0,0 +1,25 @@ +if(NOT Core_FOUND) + if(JSONBINPACK_INSTALL) + set(SOURCEMETA_CORE_INSTALL ON CACHE BOOL "enable installation") + else() + set(SOURCEMETA_CORE_INSTALL OFF CACHE BOOL "disable installation") + endif() + + set(SOURCEMETA_CORE_LANG_PROCESS OFF CACHE BOOL "disable") + set(SOURCEMETA_CORE_LANG_PARALLEL OFF CACHE BOOL "disable") + set(SOURCEMETA_CORE_LANG_ERROR OFF CACHE BOOL "disable") + set(SOURCEMETA_CORE_LANG_STACKTRACE OFF CACHE BOOL "disable") + set(SOURCEMETA_CORE_GZIP OFF CACHE BOOL "disable") + set(SOURCEMETA_CORE_JSONL OFF CACHE BOOL "disable JSONL support") + set(SOURCEMETA_CORE_JSONRPC OFF CACHE BOOL "disable") + set(SOURCEMETA_CORE_MCP OFF CACHE BOOL "disable") + set(SOURCEMETA_CORE_HTTP OFF CACHE BOOL "disable") + set(SOURCEMETA_CORE_SEMVER OFF CACHE BOOL "disable") + set(SOURCEMETA_CORE_MARKDOWN OFF CACHE BOOL "disable") + set(SOURCEMETA_CORE_YAML ON CACHE BOOL "needed by Blaze") + set(SOURCEMETA_CORE_CONTRIB_GOOGLETEST ${JSONBINPACK_TESTS} CACHE BOOL "GoogleTest") + set(SOURCEMETA_CORE_CONTRIB_GOOGLEBENCHMARK OFF CACHE BOOL "GoogleBenchmark") + add_subdirectory("${PROJECT_SOURCE_DIR}/vendor/core") + include(Sourcemeta) + set(Core_FOUND ON) +endif() diff --git a/vendor/jsonbinpack/config.cmake.in b/vendor/jsonbinpack/config.cmake.in new file mode 100644 index 000000000..93c76fb20 --- /dev/null +++ b/vendor/jsonbinpack/config.cmake.in @@ -0,0 +1,25 @@ +@PACKAGE_INIT@ + +# Support both casing styles +list(APPEND JSONBINPACK_COMPONENTS ${JSONBinPack_FIND_COMPONENTS}) +list(APPEND JSONBINPACK_COMPONENTS ${jsonbinpack_FIND_COMPONENTS}) +if(NOT JSONBINPACK_COMPONENTS) + list(APPEND JSONBINPACK_COMPONENTS runtime) + list(APPEND JSONBINPACK_COMPONENTS compiler) +endif() + +include(CMakeFindDependencyMacro) +find_dependency(Core COMPONENTS json uri jsonpointer numeric regex io) +find_dependency(Blaze COMPONENTS foundation bundle alterschema) + +foreach(component ${JSONBINPACK_COMPONENTS}) + if(component STREQUAL "runtime") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_jsonbinpack_runtime.cmake") + elseif(component STREQUAL "compiler") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_jsonbinpack_compiler.cmake") + else() + message(FATAL_ERROR "Unknown JSON BinPack component: ${component}") + endif() +endforeach() + +check_required_components("@PROJECT_NAME@") diff --git a/vendor/jsonbinpack/src/compiler/CMakeLists.txt b/vendor/jsonbinpack/src/compiler/CMakeLists.txt new file mode 100644 index 000000000..7fb220f4f --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/CMakeLists.txt @@ -0,0 +1,34 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT jsonbinpack NAME compiler + FOLDER "JSON BinPack/Compiler" + SOURCES + encoding.h compiler.cc + mapper/enum_8_bit.h + mapper/enum_8_bit_top_level.h + mapper/enum_arbitrary.h + mapper/enum_singleton.h + mapper/integer_bounded_8_bit.h + mapper/integer_bounded_greater_than_8_bit.h + mapper/integer_bounded_multiplier_8_bit.h + mapper/integer_bounded_multiplier_greater_than_8_bit.h + mapper/integer_lower_bound.h + mapper/integer_lower_bound_multiplier.h + mapper/integer_unbound.h + mapper/integer_unbound_multiplier.h + mapper/integer_upper_bound.h + mapper/integer_upper_bound_multiplier.h + mapper/number_arbitrary.h) + +if(JSONBINPACK_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT jsonbinpack NAME compiler) +endif() + +target_link_libraries(sourcemeta_jsonbinpack_compiler PRIVATE + sourcemeta::core::numeric) +target_link_libraries(sourcemeta_jsonbinpack_compiler PUBLIC + sourcemeta::core::json) +target_link_libraries(sourcemeta_jsonbinpack_compiler PRIVATE + sourcemeta::core::jsonpointer) +target_link_libraries(sourcemeta_jsonbinpack_compiler PUBLIC + sourcemeta::blaze::foundation) +target_link_libraries(sourcemeta_jsonbinpack_compiler PRIVATE + sourcemeta::blaze::alterschema) diff --git a/vendor/jsonbinpack/src/compiler/compiler.cc b/vendor/jsonbinpack/src/compiler/compiler.cc new file mode 100644 index 000000000..62f6666a1 --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/compiler.cc @@ -0,0 +1,103 @@ +#include + +#include + +#include +#include + +#include "encoding.h" + +#include // assert +#include // std::true_type + +static auto transformer_callback_noop( + const sourcemeta::core::Pointer &, const std::string_view, + const std::string_view, + const sourcemeta::blaze::SchemaTransformRule::Result &, + [[maybe_unused]] const bool applied) -> void { + assert(applied); +} + +namespace sourcemeta::jsonbinpack { + +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); +} + +auto make_encoding(sourcemeta::core::JSON &document, + const std::string &encoding, + const sourcemeta::core::JSON &options) -> void { + document.into_object(); + document.assign("$schema", sourcemeta::core::JSON{ENCODING_V1}); + document.assign("binpackEncoding", sourcemeta::core::JSON{encoding}); + document.assign("binpackOptions", options); +} + +#include "mapper/enum_8_bit.h" +#include "mapper/enum_8_bit_top_level.h" +#include "mapper/enum_arbitrary.h" +#include "mapper/enum_singleton.h" +#include "mapper/integer_bounded_8_bit.h" +#include "mapper/integer_bounded_greater_than_8_bit.h" +#include "mapper/integer_bounded_multiplier_8_bit.h" +#include "mapper/integer_bounded_multiplier_greater_than_8_bit.h" +#include "mapper/integer_lower_bound.h" +#include "mapper/integer_lower_bound_multiplier.h" +#include "mapper/integer_unbound.h" +#include "mapper/integer_unbound_multiplier.h" +#include "mapper/integer_upper_bound.h" +#include "mapper/integer_upper_bound_multiplier.h" +#include "mapper/number_arbitrary.h" + +auto 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::blaze::SchemaTransformer mapper; + + // Enums + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + + // Integers + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + mapper.add(); + + // Numbers + mapper.add(); + + [[maybe_unused]] const auto mapper_result = + mapper.apply(schema, walker, make_resolver(resolver), + transformer_callback_noop, default_dialect); + assert(mapper_result.first); + + // The "any" encoding is always the last resort + const auto dialect{sourcemeta::blaze::dialect(schema)}; + if (dialect.empty() || dialect != ENCODING_V1) { + make_encoding(schema, "ANY_PACKED_TYPE_TAG_BYTE_PREFIX", + sourcemeta::core::JSON::make_object()); + } +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/compiler/encoding.h b/vendor/jsonbinpack/src/compiler/encoding.h new file mode 100644 index 000000000..ed618adfd --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/encoding.h @@ -0,0 +1,32 @@ +#ifndef SOURCEMETA_JSONBINPACK_COMPILER_ENCODING_H_ +#define SOURCEMETA_JSONBINPACK_COMPILER_ENCODING_H_ + +#include +#include + +namespace sourcemeta::jsonbinpack { + +constexpr auto ENCODING_V1{"tag:sourcemeta.com,2024:jsonbinpack/encoding/v1"}; + +inline auto make_resolver(const sourcemeta::blaze::SchemaResolver &fallback) + -> auto { + return [&fallback](std::string_view identifier) + -> std::optional { + if (identifier == ENCODING_V1) { + return sourcemeta::core::parse_json(R"JSON({ + "$id": "tag:sourcemeta.com,2024:jsonbinpack/encoding/v1", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "tag:sourcemeta.com,2024:jsonbinpack/encoding/v1": true + } + })JSON"); + } else { + return fallback(identifier); + } + }; +} + +} // namespace sourcemeta::jsonbinpack + +#endif diff --git a/vendor/jsonbinpack/src/compiler/include/sourcemeta/jsonbinpack/compiler.h b/vendor/jsonbinpack/src/compiler/include/sourcemeta/jsonbinpack/compiler.h new file mode 100644 index 000000000..05eb55729 --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/include/sourcemeta/jsonbinpack/compiler.h @@ -0,0 +1,88 @@ +#ifndef SOURCEMETA_JSONBINPACK_COMPILER_H_ +#define SOURCEMETA_JSONBINPACK_COMPILER_H_ + +#ifndef SOURCEMETA_JSONBINPACK_COMPILER_EXPORT +#include +#endif + +/// @defgroup compiler Compiler +/// @brief The built-time schema compiler of JSON BinPack +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +#include +#include + +#include // std::string_view + +namespace sourcemeta::jsonbinpack { + +/// @ingroup compiler +/// +/// Compile a JSON Schema into an encoding schema. Keep in mind this function +/// mutates the input schema. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// #include +/// +/// auto schema{sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "string" +/// })JSON")}; +/// +/// sourcemeta::jsonbinpack::compile( +/// schema, sourcemeta::blaze::schema_walker, +/// sourcemeta::blaze::schema_resolver); +/// +/// sourcemeta::core::prettify(schema, std::cout); +/// std::cout << std::endl; +/// ``` +SOURCEMETA_JSONBINPACK_COMPILER_EXPORT +auto compile(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &resolver, + std::string_view default_dialect = "") -> void; + +/// @ingroup compiler +/// +/// Transform a JSON Schema into its canonical form to prepare it for +/// compilation. Keep in mind this function mutates the input schema. Also, the +/// `compile` function already performs canonicalization. This function is +/// exposed mainly for debugging and testing purposes. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// #include +/// +/// auto schema{sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "string" +/// })JSON")}; +/// +/// sourcemeta::jsonbinpack::canonicalize( +/// schema, sourcemeta::blaze::schema_walker, +/// sourcemeta::blaze::schema_resolver); +/// +/// sourcemeta::core::prettify(schema, std::cout); +/// std::cout << std::endl; +/// ``` +SOURCEMETA_JSONBINPACK_COMPILER_EXPORT +auto canonicalize(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &resolver, + std::string_view default_dialect = "") -> void; + +} // namespace sourcemeta::jsonbinpack + +#endif diff --git a/vendor/jsonbinpack/src/compiler/mapper/enum_8_bit.h b/vendor/jsonbinpack/src/compiler/mapper/enum_8_bit.h new file mode 100644 index 000000000..ff54aa843 --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/enum_8_bit.h @@ -0,0 +1,33 @@ +// TODO: Unit test this mapping once we have container encodings +class Enum8Bit final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + Enum8Bit() : sourcemeta::blaze::SchemaTransformRule{"enum_8_bit", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) && + schema.is_object() && schema.defines("enum") && + schema.at("enum").is_array() && !location.pointer.empty() && + schema.at("enum").size() > 1 && + sourcemeta::core::is_byte(schema.at("enum").size() - 1); + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + auto options = sourcemeta::core::JSON::make_object(); + options.assign("choices", schema.at("enum")); + make_encoding(schema, "BYTE_CHOICE_INDEX", options); + } +}; diff --git a/vendor/jsonbinpack/src/compiler/mapper/enum_8_bit_top_level.h b/vendor/jsonbinpack/src/compiler/mapper/enum_8_bit_top_level.h new file mode 100644 index 000000000..0c0b348c4 --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/enum_8_bit_top_level.h @@ -0,0 +1,33 @@ +class Enum8BitTopLevel final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + Enum8BitTopLevel() + : sourcemeta::blaze::SchemaTransformRule{"enum_8_bit_top_level", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) && + schema.is_object() && schema.defines("enum") && + schema.at("enum").is_array() && location.pointer.empty() && + schema.at("enum").size() > 1 && + sourcemeta::core::is_byte(schema.at("enum").size() - 1); + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + auto options = sourcemeta::core::JSON::make_object(); + options.assign("choices", schema.at("enum")); + make_encoding(schema, "TOP_LEVEL_BYTE_CHOICE_INDEX", options); + } +}; diff --git a/vendor/jsonbinpack/src/compiler/mapper/enum_arbitrary.h b/vendor/jsonbinpack/src/compiler/mapper/enum_arbitrary.h new file mode 100644 index 000000000..31bfa5a33 --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/enum_arbitrary.h @@ -0,0 +1,34 @@ +// TODO: Unit test this mapping once we have container encodings +class EnumArbitrary final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + EnumArbitrary() + : sourcemeta::blaze::SchemaTransformRule{"enum_arbitrary", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) && + schema.is_object() && schema.defines("enum") && + schema.at("enum").is_array() && !location.pointer.empty() && + schema.at("enum").size() > 1 && + !sourcemeta::core::is_byte(schema.at("enum").size() - 1); + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + auto options = sourcemeta::core::JSON::make_object(); + options.assign("choices", schema.at("enum")); + make_encoding(schema, "LARGE_CHOICE_INDEX", options); + } +}; diff --git a/vendor/jsonbinpack/src/compiler/mapper/enum_singleton.h b/vendor/jsonbinpack/src/compiler/mapper/enum_singleton.h new file mode 100644 index 000000000..d1c6d9062 --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/enum_singleton.h @@ -0,0 +1,31 @@ +class EnumSingleton final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + EnumSingleton() + : sourcemeta::blaze::SchemaTransformRule{"enum_singleton", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) && + schema.is_object() && schema.defines("enum") && + schema.at("enum").is_array() && schema.at("enum").size() == 1; + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + auto options = sourcemeta::core::JSON::make_object(); + options.assign("value", schema.at("enum").at(0)); + make_encoding(schema, "CONST_NONE", options); + } +}; diff --git a/vendor/jsonbinpack/src/compiler/mapper/integer_bounded_8_bit.h b/vendor/jsonbinpack/src/compiler/mapper/integer_bounded_8_bit.h new file mode 100644 index 000000000..651cd8572 --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/integer_bounded_8_bit.h @@ -0,0 +1,39 @@ +class IntegerBounded8Bit final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerBounded8Bit() + : sourcemeta::blaze::SchemaTransformRule{"integer_bounded_8_bit", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + schema.defines("minimum") && schema.defines("maximum") && + sourcemeta::core::is_byte(schema.at("maximum").to_integer() - + schema.at("minimum").to_integer()) && + !schema.defines("multipleOf"); + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + auto minimum = schema.at("minimum"); + auto maximum = schema.at("maximum"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("minimum", std::move(minimum)); + options.assign("maximum", std::move(maximum)); + options.assign("multiplier", sourcemeta::core::JSON{1}); + make_encoding(schema, "BOUNDED_MULTIPLE_8BITS_ENUM_FIXED", options); + } +}; diff --git a/vendor/jsonbinpack/src/compiler/mapper/integer_bounded_greater_than_8_bit.h b/vendor/jsonbinpack/src/compiler/mapper/integer_bounded_greater_than_8_bit.h new file mode 100644 index 000000000..87bc75bd8 --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/integer_bounded_greater_than_8_bit.h @@ -0,0 +1,39 @@ +class IntegerBoundedGreaterThan8Bit final + : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerBoundedGreaterThan8Bit() + : sourcemeta::blaze::SchemaTransformRule{ + "integer_bounded_greater_than_8_bit", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + schema.defines("minimum") && schema.defines("maximum") && + !sourcemeta::core::is_byte(schema.at("maximum").to_integer() - + schema.at("minimum").to_integer()) && + !schema.defines("multipleOf"); + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + auto minimum = schema.at("minimum"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("minimum", std::move(minimum)); + options.assign("multiplier", sourcemeta::core::JSON{1}); + make_encoding(schema, "FLOOR_MULTIPLE_ENUM_VARINT", options); + } +}; diff --git a/vendor/jsonbinpack/src/compiler/mapper/integer_bounded_multiplier_8_bit.h b/vendor/jsonbinpack/src/compiler/mapper/integer_bounded_multiplier_8_bit.h new file mode 100644 index 000000000..c63d73534 --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/integer_bounded_multiplier_8_bit.h @@ -0,0 +1,49 @@ +class IntegerBoundedMultiplier8Bit final + : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerBoundedMultiplier8Bit() + : sourcemeta::blaze::SchemaTransformRule{ + "integer_bounded_multiplier_8_bit", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + if (location.dialect != "https://json-schema.org/draft/2020-12/schema" || + !vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) || + !schema.is_object() || !schema.defines("type") || + schema.at("type").to_string() != "integer" || + !schema.defines("minimum") || !schema.at("minimum").is_integer() || + !schema.defines("maximum") || !schema.at("maximum").is_integer() || + !schema.defines("multipleOf") || + !schema.at("multipleOf").is_integer()) { + return false; + } + + return sourcemeta::core::is_byte(sourcemeta::core::count_multiples( + schema.at("minimum").to_integer(), schema.at("maximum").to_integer(), + schema.at("multipleOf").to_integer())); + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + auto minimum = schema.at("minimum"); + auto maximum = schema.at("maximum"); + auto multiplier = schema.at("multipleOf"); + + auto options = sourcemeta::core::JSON::make_object(); + options.assign("minimum", std::move(minimum)); + options.assign("maximum", std::move(maximum)); + options.assign("multiplier", std::move(multiplier)); + make_encoding(schema, "BOUNDED_MULTIPLE_8BITS_ENUM_FIXED", options); + } +}; diff --git a/vendor/jsonbinpack/src/compiler/mapper/integer_bounded_multiplier_greater_than_8_bit.h b/vendor/jsonbinpack/src/compiler/mapper/integer_bounded_multiplier_greater_than_8_bit.h new file mode 100644 index 000000000..73f6a4221 --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/integer_bounded_multiplier_greater_than_8_bit.h @@ -0,0 +1,46 @@ +class IntegerBoundedMultiplierGreaterThan8Bit final + : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerBoundedMultiplierGreaterThan8Bit() + : sourcemeta::blaze::SchemaTransformRule{ + "integer_bounded_multiplier_greater_than_8_bit", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + if (location.dialect != "https://json-schema.org/draft/2020-12/schema" || + !vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) || + !schema.is_object() || !schema.defines("type") || + schema.at("type").to_string() != "integer" || + !schema.defines("minimum") || !schema.at("minimum").is_integer() || + !schema.defines("maximum") || !schema.at("maximum").is_integer() || + !schema.defines("multipleOf") || + !schema.at("multipleOf").is_integer()) { + return false; + } + + return !sourcemeta::core::is_byte(sourcemeta::core::count_multiples( + schema.at("minimum").to_integer(), schema.at("maximum").to_integer(), + schema.at("multipleOf").to_integer())); + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + auto minimum = schema.at("minimum"); + auto multiplier = schema.at("multipleOf"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("minimum", std::move(minimum)); + options.assign("multiplier", std::move(multiplier)); + make_encoding(schema, "FLOOR_MULTIPLE_ENUM_VARINT", options); + } +}; diff --git a/vendor/jsonbinpack/src/compiler/mapper/integer_lower_bound.h b/vendor/jsonbinpack/src/compiler/mapper/integer_lower_bound.h new file mode 100644 index 000000000..b9c655ef2 --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/integer_lower_bound.h @@ -0,0 +1,35 @@ +class IntegerLowerBound final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerLowerBound() + : sourcemeta::blaze::SchemaTransformRule{"integer_lower_bound", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + schema.defines("minimum") && !schema.defines("maximum") && + !schema.defines("multipleOf"); + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + auto minimum = schema.at("minimum"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("minimum", std::move(minimum)); + options.assign("multiplier", sourcemeta::core::JSON{1}); + make_encoding(schema, "FLOOR_MULTIPLE_ENUM_VARINT", options); + } +}; diff --git a/vendor/jsonbinpack/src/compiler/mapper/integer_lower_bound_multiplier.h b/vendor/jsonbinpack/src/compiler/mapper/integer_lower_bound_multiplier.h new file mode 100644 index 000000000..f22b41bdf --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/integer_lower_bound_multiplier.h @@ -0,0 +1,38 @@ +class IntegerLowerBoundMultiplier final + : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerLowerBoundMultiplier() + : sourcemeta::blaze::SchemaTransformRule{"integer_lower_bound_multiplier", + ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + schema.defines("minimum") && !schema.defines("maximum") && + schema.defines("multipleOf") && schema.at("multipleOf").is_integer(); + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + auto minimum = schema.at("minimum"); + auto multiplier = schema.at("multipleOf"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("minimum", std::move(minimum)); + options.assign("multiplier", std::move(multiplier)); + make_encoding(schema, "FLOOR_MULTIPLE_ENUM_VARINT", options); + } +}; diff --git a/vendor/jsonbinpack/src/compiler/mapper/integer_unbound.h b/vendor/jsonbinpack/src/compiler/mapper/integer_unbound.h new file mode 100644 index 000000000..f3992eb68 --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/integer_unbound.h @@ -0,0 +1,33 @@ +class IntegerUnbound final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerUnbound() + : sourcemeta::blaze::SchemaTransformRule{"integer_unbound", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + !schema.defines("minimum") && !schema.defines("maximum") && + !schema.defines("multipleOf"); + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + auto options = sourcemeta::core::JSON::make_object(); + options.assign("multiplier", sourcemeta::core::JSON{1}); + make_encoding(schema, "ARBITRARY_MULTIPLE_ZIGZAG_VARINT", options); + } +}; diff --git a/vendor/jsonbinpack/src/compiler/mapper/integer_unbound_multiplier.h b/vendor/jsonbinpack/src/compiler/mapper/integer_unbound_multiplier.h new file mode 100644 index 000000000..3125434be --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/integer_unbound_multiplier.h @@ -0,0 +1,36 @@ +class IntegerUnboundMultiplier final + : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerUnboundMultiplier() + : sourcemeta::blaze::SchemaTransformRule{"integer_unbound_multiplier", + ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + !schema.defines("minimum") && !schema.defines("maximum") && + schema.defines("multipleOf") && schema.at("multipleOf").is_integer(); + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + auto multiplier = schema.at("multipleOf"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("multiplier", std::move(multiplier)); + make_encoding(schema, "ARBITRARY_MULTIPLE_ZIGZAG_VARINT", options); + } +}; diff --git a/vendor/jsonbinpack/src/compiler/mapper/integer_upper_bound.h b/vendor/jsonbinpack/src/compiler/mapper/integer_upper_bound.h new file mode 100644 index 000000000..b6be2d3e3 --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/integer_upper_bound.h @@ -0,0 +1,35 @@ +class IntegerUpperBound final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerUpperBound() + : sourcemeta::blaze::SchemaTransformRule{"integer_upper_bound", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + !schema.defines("minimum") && schema.defines("maximum") && + !schema.defines("multipleOf"); + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + auto maximum = schema.at("maximum"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("maximum", std::move(maximum)); + options.assign("multiplier", sourcemeta::core::JSON{1}); + make_encoding(schema, "ROOF_MULTIPLE_MIRROR_ENUM_VARINT", options); + } +}; diff --git a/vendor/jsonbinpack/src/compiler/mapper/integer_upper_bound_multiplier.h b/vendor/jsonbinpack/src/compiler/mapper/integer_upper_bound_multiplier.h new file mode 100644 index 000000000..de962e64c --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/integer_upper_bound_multiplier.h @@ -0,0 +1,38 @@ +class IntegerUpperBoundMultiplier final + : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + IntegerUpperBoundMultiplier() + : sourcemeta::blaze::SchemaTransformRule{"integer_upper_bound_multiplier", + ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "integer" && + !schema.defines("minimum") && schema.defines("maximum") && + schema.defines("multipleOf") && schema.at("multipleOf").is_integer(); + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + auto maximum = schema.at("maximum"); + auto multiplier = schema.at("multipleOf"); + auto options = sourcemeta::core::JSON::make_object(); + options.assign("maximum", std::move(maximum)); + options.assign("multiplier", std::move(multiplier)); + make_encoding(schema, "ROOF_MULTIPLE_MIRROR_ENUM_VARINT", options); + } +}; diff --git a/vendor/jsonbinpack/src/compiler/mapper/number_arbitrary.h b/vendor/jsonbinpack/src/compiler/mapper/number_arbitrary.h new file mode 100644 index 000000000..2b0822e4c --- /dev/null +++ b/vendor/jsonbinpack/src/compiler/mapper/number_arbitrary.h @@ -0,0 +1,30 @@ +class NumberArbitrary final : public sourcemeta::blaze::SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + NumberArbitrary() + : sourcemeta::blaze::SchemaTransformRule{"number_arbitrary", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> sourcemeta::blaze::SchemaTransformRule::Result override { + return location.dialect == "https://json-schema.org/draft/2020-12/schema" && + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_2020_12_Validation) && + schema.is_object() && schema.defines("type") && + schema.at("type").to_string() == "number"; + } + + auto transform(sourcemeta::core::JSON &schema, + const sourcemeta::blaze::SchemaTransformRule::Result &) const + -> void override { + make_encoding(schema, "DOUBLE_VARINT_TUPLE", + sourcemeta::core::JSON::make_object()); + } +}; diff --git a/vendor/jsonbinpack/src/runtime/CMakeLists.txt b/vendor/jsonbinpack/src/runtime/CMakeLists.txt new file mode 100644 index 000000000..94dd8e1ca --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/CMakeLists.txt @@ -0,0 +1,47 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT jsonbinpack NAME runtime + FOLDER "JSON BinPack/Runtime" + PRIVATE_HEADERS + decoder.h + encoder.h + input_stream.h + output_stream.h + encoder_cache.h + encoding.h + SOURCES + input_stream.cc + output_stream.cc + unreachable.h + cache.cc + + loader.cc + loader_v1_any.h + loader_v1_array.h + loader_v1_integer.h + loader_v1_number.h + loader_v1_string.h + + decoder_any.cc + decoder_array.cc + decoder_common.cc + decoder_integer.cc + decoder_number.cc + decoder_object.cc + decoder_string.cc + encoder_any.cc + encoder_array.cc + encoder_common.cc + encoder_integer.cc + encoder_number.cc + encoder_object.cc + encoder_string.cc) + +if(JSONBINPACK_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT jsonbinpack NAME runtime) +endif() + +target_link_libraries(sourcemeta_jsonbinpack_runtime PUBLIC + sourcemeta::core::json) +target_link_libraries(sourcemeta_jsonbinpack_runtime PUBLIC + sourcemeta::core::numeric) +target_link_libraries(sourcemeta_jsonbinpack_runtime PUBLIC + sourcemeta::core::io) diff --git a/vendor/jsonbinpack/src/runtime/cache.cc b/vendor/jsonbinpack/src/runtime/cache.cc new file mode 100644 index 000000000..ce3679517 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/cache.cc @@ -0,0 +1,66 @@ +#include + +namespace sourcemeta::jsonbinpack { + +auto Cache::record(const sourcemeta::core::JSON::String &value, + const std::uint64_t offset, const Type type) -> void { + // Encoding a shared string has some overhead, such as the + // shared string marker + the offset, so its not worth + // doing for strings that are too small. + constexpr auto MINIMUM_STRING_LENGTH{3}; + + // We don't want to allow the context to grow + // forever, otherwise an attacker could force the + // program to exhaust memory given an input + // document that contains a high number of large strings. + constexpr auto MAXIMUM_BYTE_SIZE{20971520}; + + const auto value_size{value.size()}; + if (value_size < MINIMUM_STRING_LENGTH || value_size >= MAXIMUM_BYTE_SIZE) { + return; + } + + // Remove the oldest entries to make space if needed + while (!this->data.empty() && + this->byte_size + value_size >= MAXIMUM_BYTE_SIZE) { + this->remove_oldest(); + } + + auto result{this->data.insert({std::make_pair(value, type), offset})}; + if (result.second) { + this->byte_size += value_size; + this->order.emplace(offset, result.first->first); + } else if (offset > result.first->second) { + this->order.erase(result.first->second); + // If the string already exists, we want to + // bump the offset for locality purposes. + result.first->second = offset; + this->order.emplace(offset, result.first->first); + } + + // Otherwise we are doing something wrong + assert(this->order.size() == this->data.size()); +} + +auto Cache::remove_oldest() -> void { + assert(!this->data.empty()); + // std::map are by definition ordered by key, + // so the begin iterator points to the entry + // with the lowest offset, a.k.a. the oldest. + const auto iterator{this->order.cbegin()}; + this->byte_size -= iterator->second.get().first.size(); + this->data.erase(iterator->second.get()); + this->order.erase(iterator); +} + +auto Cache::find(const sourcemeta::core::JSON::String &value, + const Type type) const -> std::optional { + const auto result{this->data.find(std::make_pair(value, type))}; + if (result == this->data.cend()) { + return std::nullopt; + } + + return result->second; +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/decoder_any.cc b/vendor/jsonbinpack/src/runtime/decoder_any.cc new file mode 100644 index 000000000..4cd6f5690 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/decoder_any.cc @@ -0,0 +1,162 @@ +#include + +#include + +#include "unreachable.h" + +#include // assert +#include // std::uint8_t, std::uint16_t, std::uint64_t +#include // std::make_shared + +namespace sourcemeta::jsonbinpack { + +auto Decoder::BYTE_CHOICE_INDEX(const struct BYTE_CHOICE_INDEX &options) + -> sourcemeta::core::JSON { + assert(!options.choices.empty()); + assert(sourcemeta::core::is_byte(options.choices.size())); + const std::uint8_t index{this->get_byte()}; + assert(options.choices.size() > index); + return options.choices[index]; +} + +auto Decoder::LARGE_CHOICE_INDEX(const struct LARGE_CHOICE_INDEX &options) + -> sourcemeta::core::JSON { + assert(!options.choices.empty()); + const std::uint64_t index{this->get_varint()}; + assert(options.choices.size() > index); + return options.choices[index]; +} + +auto Decoder::TOP_LEVEL_BYTE_CHOICE_INDEX( + const struct TOP_LEVEL_BYTE_CHOICE_INDEX &options) + -> sourcemeta::core::JSON { + assert(!options.choices.empty()); + assert(sourcemeta::core::is_byte(options.choices.size())); + if (!this->has_more_data()) { + return options.choices.front(); + } else { + const std::uint16_t index{static_cast(this->get_byte() + 1)}; + assert(options.choices.size() > index); + return options.choices[index]; + } +} + +auto Decoder::CONST_NONE(const struct CONST_NONE &options) + -> sourcemeta::core::JSON { + return options.value; +} + +auto Decoder::ANY_PACKED_TYPE_TAG_BYTE_PREFIX( + const struct ANY_PACKED_TYPE_TAG_BYTE_PREFIX &) -> sourcemeta::core::JSON { + using namespace internal::ANY_PACKED_TYPE_TAG_BYTE_PREFIX; + const std::uint8_t byte{this->get_byte()}; + const std::uint8_t type{ + static_cast(byte & (0xff >> subtype_size))}; + const std::uint8_t subtype{static_cast(byte >> type_size)}; + + if (type == TYPE_OTHER) { + switch (subtype) { + case SUBTYPE_NULL: + return sourcemeta::core::JSON{nullptr}; + case SUBTYPE_FALSE: + return sourcemeta::core::JSON{false}; + case SUBTYPE_TRUE: + return sourcemeta::core::JSON{true}; + case SUBTYPE_NUMBER: + return this->DOUBLE_VARINT_TUPLE({}); + case SUBTYPE_POSITIVE_REAL_INTEGER_BYTE: + return sourcemeta::core::JSON{static_cast(this->get_byte())}; + case SUBTYPE_POSITIVE_INTEGER: + return sourcemeta::core::JSON{ + static_cast(this->get_varint())}; + case SUBTYPE_NEGATIVE_INTEGER: + return sourcemeta::core::JSON{ + -static_cast(this->get_varint()) - 1}; + case SUBTYPE_LONG_STRING_BASE_EXPONENT_7: + return sourcemeta::core::JSON{ + this->get_string_utf8(this->get_varint() + 128)}; + case SUBTYPE_LONG_STRING_BASE_EXPONENT_8: + return sourcemeta::core::JSON{ + this->get_string_utf8(this->get_varint() + 256)}; + case SUBTYPE_LONG_STRING_BASE_EXPONENT_9: + return sourcemeta::core::JSON{ + this->get_string_utf8(this->get_varint() + 512)}; + case SUBTYPE_LONG_STRING_BASE_EXPONENT_10: + return sourcemeta::core::JSON{ + this->get_string_utf8(this->get_varint() + 1024)}; + default: + unreachable(); + } + } else { + switch (type) { + case TYPE_POSITIVE_INTEGER_BYTE: + return sourcemeta::core::JSON{subtype > 0 ? subtype - 1 + : this->get_byte()}; + case TYPE_NEGATIVE_INTEGER_BYTE: + return sourcemeta::core::JSON{ + subtype > 0 ? static_cast(-subtype) + : static_cast(-this->get_byte() - 1)}; + case TYPE_SHARED_STRING: { + const auto length = subtype == 0 + ? this->get_varint() - 1 + + static_cast( + sourcemeta::core::uint_max<5>) * + 2 + : subtype - 1; + const std::uint64_t position{this->position()}; + const std::uint64_t current{this->rewind(this->get_varint(), position)}; + const sourcemeta::core::JSON value{this->get_string_utf8(length)}; + this->seek(current); + return value; + }; + case TYPE_STRING: + return subtype == 0 + ? this->FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED( + {static_cast( + sourcemeta::core::uint_max<5>) * + 2}) + : sourcemeta::core::JSON{this->get_string_utf8(subtype - 1)}; + case TYPE_LONG_STRING: + return sourcemeta::core::JSON{ + this->get_string_utf8(subtype + sourcemeta::core::uint_max<5>)}; + case TYPE_ARRAY: + return subtype == 0 + ? this->FIXED_TYPED_ARRAY( + {.size = this->get_varint() + + sourcemeta::core::uint_max<5>, + .encoding = std::make_shared( + sourcemeta::jsonbinpack:: + ANY_PACKED_TYPE_TAG_BYTE_PREFIX{}), + .prefix_encodings = {}}) + : this->FIXED_TYPED_ARRAY( + {.size = static_cast(subtype - 1), + .encoding = std::make_shared( + sourcemeta::jsonbinpack:: + ANY_PACKED_TYPE_TAG_BYTE_PREFIX{}), + .prefix_encodings = {}}); + case TYPE_OBJECT: + return subtype == 0 + ? this->FIXED_TYPED_ARBITRARY_OBJECT( + {.size = this->get_varint() + + sourcemeta::core::uint_max<5>, + .key_encoding = std::make_shared( + sourcemeta::jsonbinpack:: + PREFIX_VARINT_LENGTH_STRING_SHARED{}), + .encoding = std::make_shared( + sourcemeta::jsonbinpack:: + ANY_PACKED_TYPE_TAG_BYTE_PREFIX{})}) + : this->FIXED_TYPED_ARBITRARY_OBJECT( + {.size = static_cast(subtype - 1), + .key_encoding = std::make_shared( + sourcemeta::jsonbinpack:: + PREFIX_VARINT_LENGTH_STRING_SHARED{}), + .encoding = std::make_shared( + sourcemeta::jsonbinpack:: + ANY_PACKED_TYPE_TAG_BYTE_PREFIX{})}); + default: + unreachable(); + } + } +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/decoder_array.cc b/vendor/jsonbinpack/src/runtime/decoder_array.cc new file mode 100644 index 000000000..0d5f1c3cc --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/decoder_array.cc @@ -0,0 +1,62 @@ +#include + +#include + +#include // assert +#include // std::uint8_t, std::uint64_t +#include // std::move + +namespace sourcemeta::jsonbinpack { + +auto Decoder::FIXED_TYPED_ARRAY(const struct FIXED_TYPED_ARRAY &options) + -> sourcemeta::core::JSON { + const auto prefix_encodings{options.prefix_encodings.size()}; + sourcemeta::core::JSON result = sourcemeta::core::JSON::make_array(); + for (std::size_t index = 0; index < options.size; index++) { + const Encoding &encoding{prefix_encodings > index + ? options.prefix_encodings[index] + : *(options.encoding)}; + result.push_back(this->read(encoding)); + } + + assert(result.size() == options.size); + return result; +}; + +auto Decoder::BOUNDED_8BITS_TYPED_ARRAY( + const struct BOUNDED_8BITS_TYPED_ARRAY &options) -> sourcemeta::core::JSON { + assert(options.maximum >= options.minimum); + assert(sourcemeta::core::is_byte(options.maximum - options.minimum)); + const std::uint8_t byte{this->get_byte()}; + const std::uint64_t size{byte + options.minimum}; + assert(sourcemeta::core::is_within(size, options.minimum, options.maximum)); + return this->FIXED_TYPED_ARRAY( + {.size = size, + .encoding = options.encoding, + .prefix_encodings = options.prefix_encodings}); +}; + +auto Decoder::FLOOR_TYPED_ARRAY(const struct FLOOR_TYPED_ARRAY &options) + -> sourcemeta::core::JSON { + const std::uint64_t value{this->get_varint()}; + const std::uint64_t size{value + options.minimum}; + assert(size >= value); + assert(size >= options.minimum); + return this->FIXED_TYPED_ARRAY( + {.size = size, + .encoding = options.encoding, + .prefix_encodings = options.prefix_encodings}); +}; + +auto Decoder::ROOF_TYPED_ARRAY(const struct ROOF_TYPED_ARRAY &options) + -> sourcemeta::core::JSON { + const std::uint64_t value{this->get_varint()}; + const std::uint64_t size{options.maximum - value}; + assert(size <= options.maximum); + return this->FIXED_TYPED_ARRAY( + {.size = size, + .encoding = options.encoding, + .prefix_encodings = options.prefix_encodings}); +}; + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/decoder_common.cc b/vendor/jsonbinpack/src/runtime/decoder_common.cc new file mode 100644 index 000000000..282375ff2 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/decoder_common.cc @@ -0,0 +1,46 @@ +#include + +#include "unreachable.h" + +#include // assert +#include // std::get + +namespace sourcemeta::jsonbinpack { + +Decoder::Decoder(Stream &input) : InputStream{input} {} + +auto Decoder::read(const Encoding &encoding) -> sourcemeta::core::JSON { + switch (encoding.index()) { +#define HANDLE_DECODING(index, name) \ + case (index): \ + return this->name(std::get(encoding)); + HANDLE_DECODING(0, BOUNDED_MULTIPLE_8BITS_ENUM_FIXED) + HANDLE_DECODING(1, FLOOR_MULTIPLE_ENUM_VARINT) + HANDLE_DECODING(2, ROOF_MULTIPLE_MIRROR_ENUM_VARINT) + HANDLE_DECODING(3, ARBITRARY_MULTIPLE_ZIGZAG_VARINT) + HANDLE_DECODING(4, DOUBLE_VARINT_TUPLE) + HANDLE_DECODING(5, BYTE_CHOICE_INDEX) + HANDLE_DECODING(6, LARGE_CHOICE_INDEX) + HANDLE_DECODING(7, TOP_LEVEL_BYTE_CHOICE_INDEX) + HANDLE_DECODING(8, CONST_NONE) + HANDLE_DECODING(9, ANY_PACKED_TYPE_TAG_BYTE_PREFIX) + HANDLE_DECODING(10, UTF8_STRING_NO_LENGTH) + HANDLE_DECODING(11, FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED) + HANDLE_DECODING(12, ROOF_VARINT_PREFIX_UTF8_STRING_SHARED) + HANDLE_DECODING(13, BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED) + HANDLE_DECODING(14, RFC3339_DATE_INTEGER_TRIPLET) + HANDLE_DECODING(15, PREFIX_VARINT_LENGTH_STRING_SHARED) + HANDLE_DECODING(16, FIXED_TYPED_ARRAY) + HANDLE_DECODING(17, BOUNDED_8BITS_TYPED_ARRAY) + HANDLE_DECODING(18, FLOOR_TYPED_ARRAY) + HANDLE_DECODING(19, ROOF_TYPED_ARRAY) + HANDLE_DECODING(20, FIXED_TYPED_ARBITRARY_OBJECT) + HANDLE_DECODING(21, VARINT_TYPED_ARBITRARY_OBJECT) +#undef HANDLE_DECODING + default: + // We should never get here. If so, it is definitely a bug + unreachable(); + } +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/decoder_integer.cc b/vendor/jsonbinpack/src/runtime/decoder_integer.cc new file mode 100644 index 000000000..d4c15bf70 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/decoder_integer.cc @@ -0,0 +1,93 @@ +#include + +#include + +#include // assert +#include // std::uint8_t, std::uint32_t, std::int64_t, std::uint64_t + +namespace sourcemeta::jsonbinpack { + +auto Decoder::BOUNDED_MULTIPLE_8BITS_ENUM_FIXED( + const struct BOUNDED_MULTIPLE_8BITS_ENUM_FIXED &options) + -> sourcemeta::core::JSON { + assert(options.multiplier > 0); + const std::uint8_t byte{this->get_byte()}; + const std::int64_t closest_minimum{ + sourcemeta::core::divide_ceil(options.minimum, options.multiplier)}; + if (closest_minimum >= 0) { + const std::uint64_t closest_minimum_multiple{ + static_cast(closest_minimum) * options.multiplier}; + // We trust the encoder that the data we are seeing + // corresponds to a valid 64-bit signed integer. + return sourcemeta::core::JSON{static_cast( + (byte * options.multiplier) + closest_minimum_multiple)}; + } else { + const std::uint64_t closest_minimum_multiple{ + sourcemeta::core::abs(closest_minimum) * options.multiplier}; + // We trust the encoder that the data we are seeing + // corresponds to a valid 64-bit signed integer. + return sourcemeta::core::JSON{static_cast( + (byte * options.multiplier) - closest_minimum_multiple)}; + } +} + +auto Decoder::FLOOR_MULTIPLE_ENUM_VARINT( + const struct FLOOR_MULTIPLE_ENUM_VARINT &options) + -> sourcemeta::core::JSON { + assert(options.multiplier > 0); + const std::int64_t closest_minimum{ + sourcemeta::core::divide_ceil(options.minimum, options.multiplier)}; + if (closest_minimum >= 0) { + const std::uint64_t closest_minimum_multiple{ + static_cast(closest_minimum) * options.multiplier}; + // We trust the encoder that the data we are seeing + // corresponds to a valid 64-bit signed integer. + return sourcemeta::core::JSON{static_cast( + (this->get_varint() * options.multiplier) + closest_minimum_multiple)}; + } else { + const std::uint64_t closest_minimum_multiple{ + sourcemeta::core::abs(closest_minimum) * options.multiplier}; + // We trust the encoder that the data we are seeing + // corresponds to a valid 64-bit signed integer. + return sourcemeta::core::JSON{static_cast( + (this->get_varint() * options.multiplier) - closest_minimum_multiple)}; + } +} + +auto Decoder::ROOF_MULTIPLE_MIRROR_ENUM_VARINT( + const struct ROOF_MULTIPLE_MIRROR_ENUM_VARINT &options) + -> sourcemeta::core::JSON { + assert(options.multiplier > 0); + const std::int64_t closest_maximum{ + sourcemeta::core::divide_floor(options.maximum, options.multiplier)}; + if (closest_maximum >= 0) { + const std::uint64_t closest_maximum_multiple{ + static_cast(closest_maximum) * options.multiplier}; + // We trust the encoder that the data we are seeing + // corresponds to a valid 64-bit signed integer. + return sourcemeta::core::JSON{static_cast( + -(static_cast(this->get_varint() * options.multiplier)) + + static_cast(closest_maximum_multiple))}; + } else { + const std::uint64_t closest_maximum_multiple{ + sourcemeta::core::abs(closest_maximum) * options.multiplier}; + // We trust the encoder that the data we are seeing + // corresponds to a valid 64-bit signed integer. + return sourcemeta::core::JSON{static_cast( + -(static_cast(this->get_varint() * options.multiplier)) - + static_cast(closest_maximum_multiple))}; + } +} + +auto Decoder::ARBITRARY_MULTIPLE_ZIGZAG_VARINT( + const struct ARBITRARY_MULTIPLE_ZIGZAG_VARINT &options) + -> sourcemeta::core::JSON { + assert(options.multiplier > 0); + // We trust the encoder that the data we are seeing + // corresponds to a valid 64-bit signed integer. + return sourcemeta::core::JSON{ + static_cast(this->get_varint_zigzag() * + static_cast(options.multiplier))}; +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/decoder_number.cc b/vendor/jsonbinpack/src/runtime/decoder_number.cc new file mode 100644 index 000000000..9d0a68692 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/decoder_number.cc @@ -0,0 +1,25 @@ +#include + +#include // std::int64_t, std::uint64_t + +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC optimize("no-reciprocal-math") +#endif + +namespace sourcemeta::jsonbinpack { + +auto Decoder::DOUBLE_VARINT_TUPLE(const struct DOUBLE_VARINT_TUPLE &) + -> sourcemeta::core::JSON { +#ifdef __clang__ +#pragma clang fp reciprocal(off) +#endif + const std::int64_t digits{this->get_varint_zigzag()}; + const std::uint64_t point{this->get_varint()}; + double divisor{1.0}; + for (std::uint64_t i = 0; i < point; ++i) { + divisor *= 10.0; + } + return sourcemeta::core::JSON{static_cast(digits) / divisor}; +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/decoder_object.cc b/vendor/jsonbinpack/src/runtime/decoder_object.cc new file mode 100644 index 000000000..ebb056ec4 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/decoder_object.cc @@ -0,0 +1,37 @@ +#include + +#include // assert +#include // std::uint64_t + +namespace sourcemeta::jsonbinpack { + +auto Decoder::FIXED_TYPED_ARBITRARY_OBJECT( + const struct FIXED_TYPED_ARBITRARY_OBJECT &options) + -> sourcemeta::core::JSON { + sourcemeta::core::JSON document = sourcemeta::core::JSON::make_object(); + for (std::size_t index = 0; index < options.size; index++) { + const sourcemeta::core::JSON key = this->read(*(options.key_encoding)); + assert(key.is_string()); + document.assign(key.to_string(), this->read(*(options.encoding))); + } + + assert(document.size() == options.size); + return document; +}; + +auto Decoder::VARINT_TYPED_ARBITRARY_OBJECT( + const struct VARINT_TYPED_ARBITRARY_OBJECT &options) + -> sourcemeta::core::JSON { + const std::uint64_t size{this->get_varint()}; + sourcemeta::core::JSON document = sourcemeta::core::JSON::make_object(); + for (std::size_t index = 0; index < size; index++) { + const sourcemeta::core::JSON key = this->read(*(options.key_encoding)); + assert(key.is_string()); + document.assign(key.to_string(), this->read(*(options.encoding))); + } + + assert(document.size() == size); + return document; +}; + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/decoder_string.cc b/vendor/jsonbinpack/src/runtime/decoder_string.cc new file mode 100644 index 000000000..76661deb5 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/decoder_string.cc @@ -0,0 +1,118 @@ +#include + +#include // assert +#include // std::uint8_t, std::uint16_t, std::uint64_t +#include // std::setw, std::setfill +#include // std::basic_ostringstream + +namespace sourcemeta::jsonbinpack { + +auto Decoder::UTF8_STRING_NO_LENGTH(const struct UTF8_STRING_NO_LENGTH &options) + -> sourcemeta::core::JSON { + return sourcemeta::core::JSON{this->get_string_utf8(options.size)}; +} + +auto Decoder::FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED( + const struct FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED &options) + -> sourcemeta::core::JSON { + const std::uint64_t prefix{this->get_varint()}; + const bool is_shared{prefix == 0}; + const std::uint64_t length{(is_shared ? this->get_varint() : prefix) + + options.minimum - 1}; + assert(length >= options.minimum); + + if (is_shared) { + const std::uint64_t position{this->position()}; + const std::uint64_t current{this->rewind(this->get_varint(), position)}; + const sourcemeta::core::JSON value{this->get_string_utf8(length)}; + this->seek(current); + return value; + } else { + return UTF8_STRING_NO_LENGTH({length}); + } +} + +auto Decoder::ROOF_VARINT_PREFIX_UTF8_STRING_SHARED( + const struct ROOF_VARINT_PREFIX_UTF8_STRING_SHARED &options) + -> sourcemeta::core::JSON { + const std::uint64_t prefix{this->get_varint()}; + const bool is_shared{prefix == 0}; + const std::uint64_t length{options.maximum - + (is_shared ? this->get_varint() : prefix) + 1}; + assert(length <= options.maximum); + + if (is_shared) { + const std::uint64_t position{this->position()}; + const std::uint64_t current{this->rewind(this->get_varint(), position)}; + const sourcemeta::core::JSON value{UTF8_STRING_NO_LENGTH({length})}; + this->seek(current); + return value; + } else { + return UTF8_STRING_NO_LENGTH({length}); + } +} + +auto Decoder::BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED( + const struct BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED &options) + -> sourcemeta::core::JSON { + assert(options.minimum <= options.maximum); + assert(sourcemeta::core::is_byte(options.maximum - options.minimum)); + const std::uint8_t prefix{this->get_byte()}; + const bool is_shared{prefix == 0}; + const std::uint64_t length{(is_shared ? this->get_byte() : prefix) + + options.minimum - 1}; + assert(sourcemeta::core::is_within(length, options.minimum, options.maximum)); + + if (is_shared) { + const std::uint64_t position{this->position()}; + const std::uint64_t current{this->rewind(this->get_varint(), position)}; + const sourcemeta::core::JSON value{UTF8_STRING_NO_LENGTH({length})}; + this->seek(current); + return value; + } else { + return UTF8_STRING_NO_LENGTH({length}); + } +} + +auto Decoder::RFC3339_DATE_INTEGER_TRIPLET( + const struct RFC3339_DATE_INTEGER_TRIPLET &) -> sourcemeta::core::JSON { + const std::uint16_t year{this->get_word()}; + const std::uint8_t month{this->get_byte()}; + const std::uint8_t day{this->get_byte()}; + + assert(year <= 9999); + assert(month >= 1 && month <= 12); + assert(day >= 1 && day <= 31); + + std::basic_ostringstream + output; + output << std::setfill('0'); + output << std::setw(4) << year; + output << "-"; + // Cast the bytes to a larger integer, otherwise + // they will be interpreted as characters. + output << std::setw(2) << static_cast(month); + output << "-"; + output << std::setw(2) << static_cast(day); + + return sourcemeta::core::JSON{output.str()}; +} + +auto Decoder::PREFIX_VARINT_LENGTH_STRING_SHARED( + const struct PREFIX_VARINT_LENGTH_STRING_SHARED &options) + -> sourcemeta::core::JSON { + const std::uint64_t prefix{this->get_varint()}; + if (prefix == 0) { + const std::uint64_t position{this->position()}; + const std::uint64_t current{this->rewind(this->get_varint(), position)}; + const sourcemeta::core::JSON value{ + PREFIX_VARINT_LENGTH_STRING_SHARED(options)}; + this->seek(current); + return value; + } else { + return sourcemeta::core::JSON{this->get_string_utf8(prefix - 1)}; + } +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/encoder_any.cc b/vendor/jsonbinpack/src/runtime/encoder_any.cc new file mode 100644 index 000000000..43c907555 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/encoder_any.cc @@ -0,0 +1,209 @@ +#include + +#include + +#include "unreachable.h" + +#include // std::find_if +#include // assert +#include // std::uint8_t, std::int64_t, std::uint64_t +#include // std::cbegin, std::cend, std::distance +#include // std::make_shared +#include // std::ranges +#include // std::move + +namespace sourcemeta::jsonbinpack { + +auto Encoder::BYTE_CHOICE_INDEX(const sourcemeta::core::JSON &document, + const struct BYTE_CHOICE_INDEX &options) + -> void { + assert(!options.choices.empty()); + assert(sourcemeta::core::is_byte(options.choices.size())); + const auto iterator{std::ranges::find(options.choices, document)}; + assert(iterator != std::cend(options.choices)); + const auto cursor{std::distance(std::cbegin(options.choices), iterator)}; + assert(sourcemeta::core::is_within( + cursor, 0, static_cast(options.choices.size()))); + this->put_byte(static_cast(cursor)); +} + +auto Encoder::LARGE_CHOICE_INDEX(const sourcemeta::core::JSON &document, + const struct LARGE_CHOICE_INDEX &options) + -> void { + assert(options.choices.size() > 0); + const auto iterator{ + std::ranges::find_if(options.choices, [&document](const auto &choice) { + return choice == document; + })}; + assert(iterator != std::cend(options.choices)); + const auto cursor{std::distance(std::cbegin(options.choices), iterator)}; + assert(sourcemeta::core::is_within(cursor, static_cast(0), + options.choices.size() - 1)); + this->put_varint(static_cast(cursor)); +} + +auto Encoder::TOP_LEVEL_BYTE_CHOICE_INDEX( + const sourcemeta::core::JSON &document, + const struct TOP_LEVEL_BYTE_CHOICE_INDEX &options) -> void { + assert(options.choices.size() > 0); + assert(sourcemeta::core::is_byte(options.choices.size())); + const auto iterator{ + std::ranges::find_if(options.choices, [&document](auto const &choice) { + return choice == document; + })}; + assert(iterator != std::cend(options.choices)); + const auto cursor{std::distance(std::cbegin(options.choices), iterator)}; + assert(sourcemeta::core::is_within( + cursor, 0, static_cast(options.choices.size()) - 1)); + // This encoding encodes the first option of the enum as "no data" + if (cursor > 0) { + this->put_byte(static_cast(cursor - 1)); + } +} + +auto Encoder::CONST_NONE( +#ifndef NDEBUG + const sourcemeta::core::JSON &document, const struct CONST_NONE &options) +#else + const sourcemeta::core::JSON &, const struct CONST_NONE &) +#endif + -> void { + assert(document == options.value); +} + +auto Encoder::ANY_PACKED_TYPE_TAG_BYTE_PREFIX( + const sourcemeta::core::JSON &document, + const struct ANY_PACKED_TYPE_TAG_BYTE_PREFIX &) -> void { + using namespace internal::ANY_PACKED_TYPE_TAG_BYTE_PREFIX; + if (document.is_null()) { + this->put_byte(TYPE_OTHER | (SUBTYPE_NULL << type_size)); + } else if (document.is_boolean()) { + const std::uint8_t subtype{document.to_boolean() ? SUBTYPE_TRUE + : SUBTYPE_FALSE}; + this->put_byte(TYPE_OTHER | + static_cast(subtype << type_size)); + } else if (document.is_real() && document.is_integral()) { + const auto value{document.as_integer()}; + if (value >= 0 && sourcemeta::core::is_byte(value)) { + this->put_byte(TYPE_OTHER | SUBTYPE_POSITIVE_REAL_INTEGER_BYTE + << type_size); + this->put_byte(static_cast(value)); + } else { + this->put_byte(TYPE_OTHER | SUBTYPE_NUMBER << type_size); + this->DOUBLE_VARINT_TUPLE(document, {}); + } + } else if (document.is_real()) { + this->put_byte(TYPE_OTHER | SUBTYPE_NUMBER << type_size); + this->DOUBLE_VARINT_TUPLE(document, {}); + } else if (document.is_integer()) { + const std::int64_t value{document.to_integer()}; + const bool is_positive{value >= 0}; + const std::uint64_t absolute{is_positive + ? static_cast(value) + : sourcemeta::core::abs(value) - 1}; + if (sourcemeta::core::is_byte(absolute)) { + const std::uint8_t type{is_positive ? TYPE_POSITIVE_INTEGER_BYTE + : TYPE_NEGATIVE_INTEGER_BYTE}; + const std::uint8_t absolute_byte{static_cast(absolute)}; + if (absolute < sourcemeta::core::uint_max<5>) { + this->put_byte( + type | static_cast((absolute_byte + 1) << type_size)); + } else { + this->put_byte(type); + this->put_byte(absolute_byte); + } + } else { + const std::uint8_t subtype{is_positive ? SUBTYPE_POSITIVE_INTEGER + : SUBTYPE_NEGATIVE_INTEGER}; + this->put_byte(TYPE_OTHER | + static_cast(subtype << type_size)); + this->put_varint(absolute); + } + } else if (document.is_string()) { + const sourcemeta::core::JSON::String &value{document.to_string()}; + const auto size{document.byte_size()}; + const auto shared{this->cache_.find(value, Cache::Type::Standalone)}; + if (size < sourcemeta::core::uint_max<5>) { + const std::uint8_t type{shared.has_value() ? TYPE_SHARED_STRING + : TYPE_STRING}; + this->put_byte( + static_cast(type | ((size + 1) << type_size))); + if (shared.has_value()) { + this->put_varint(this->position() - shared.value()); + } else { + this->cache_.record(value, this->position(), Cache::Type::Standalone); + this->put_string_utf8(value, size); + } + } else if (size >= sourcemeta::core::uint_max<5> && + size < + static_cast(sourcemeta::core::uint_max<5>) * + 2 && + !shared.has_value()) { + this->put_byte(static_cast( + TYPE_LONG_STRING | + ((size - sourcemeta::core::uint_max<5>) << type_size))); + this->put_string_utf8(value, size); + } else if (size >= 2 << (SUBTYPE_LONG_STRING_BASE_EXPONENT_7 - 1) && + !shared.has_value()) { + const std::uint8_t exponent{sourcemeta::core::closest_smallest_exponent( + size, 2, SUBTYPE_LONG_STRING_BASE_EXPONENT_7, + SUBTYPE_LONG_STRING_BASE_EXPONENT_10)}; + this->put_byte( + static_cast(TYPE_OTHER | (exponent << type_size))); + this->put_varint(size - static_cast(2 << (exponent - 1))); + this->put_string_utf8(value, size); + } else { + // Exploit the fact that a shared string always starts + // with an impossible length marker (0) to avoid having + // to encode an additional tag + if (!shared.has_value()) { + this->put_byte(TYPE_STRING); + } + + // If we got this far, the string is at least a certain length + return FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED( + document, + {static_cast(sourcemeta::core::uint_max<5> * 2)}); + } + } else if (document.is_array()) { + const auto size{document.size()}; + if (size >= sourcemeta::core::uint_max<5>) { + this->put_byte(TYPE_ARRAY); + this->put_varint(size - sourcemeta::core::uint_max<5>); + } else { + this->put_byte( + static_cast(TYPE_ARRAY | ((size + 1) << type_size))); + } + + Encoding encoding{ + sourcemeta::jsonbinpack::ANY_PACKED_TYPE_TAG_BYTE_PREFIX{}}; + this->FIXED_TYPED_ARRAY( + document, {.size = size, + .encoding = std::make_shared(std::move(encoding)), + .prefix_encodings = {}}); + } else if (document.is_object()) { + const auto size{document.size()}; + if (size >= sourcemeta::core::uint_max<5>) { + this->put_byte(TYPE_OBJECT); + this->put_varint(size - sourcemeta::core::uint_max<5>); + } else { + this->put_byte( + static_cast(TYPE_OBJECT | ((size + 1) << type_size))); + } + + Encoding key_encoding{ + sourcemeta::jsonbinpack::PREFIX_VARINT_LENGTH_STRING_SHARED{}}; + Encoding value_encoding{ + sourcemeta::jsonbinpack::ANY_PACKED_TYPE_TAG_BYTE_PREFIX{}}; + this->FIXED_TYPED_ARBITRARY_OBJECT( + document, + {.size = size, + .key_encoding = std::make_shared(std::move(key_encoding)), + .encoding = std::make_shared(std::move(value_encoding))}); + } else { + // We should never get here + unreachable(); + } +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/encoder_array.cc b/vendor/jsonbinpack/src/runtime/encoder_array.cc new file mode 100644 index 000000000..e9e3f0258 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/encoder_array.cc @@ -0,0 +1,63 @@ +#include + +#include + +#include // assert +#include // std::uint8_t +#include // std::move + +namespace sourcemeta::jsonbinpack { + +auto Encoder::FIXED_TYPED_ARRAY(const sourcemeta::core::JSON &document, + const struct FIXED_TYPED_ARRAY &options) + -> void { + assert(document.is_array()); + assert(document.size() == options.size); + const auto prefix_encodings{options.prefix_encodings.size()}; + assert(prefix_encodings <= document.size()); + for (std::size_t index = 0; index < options.size; index++) { + const Encoding &encoding{prefix_encodings > index + ? options.prefix_encodings[index] + : *(options.encoding)}; + this->write(document.at(index), encoding); + } +} + +auto Encoder::BOUNDED_8BITS_TYPED_ARRAY( + const sourcemeta::core::JSON &document, + const struct BOUNDED_8BITS_TYPED_ARRAY &options) -> void { + assert(options.maximum >= options.minimum); + const auto size{document.size()}; + assert(sourcemeta::core::is_within(size, options.minimum, options.maximum)); + assert(sourcemeta::core::is_byte(options.maximum - options.minimum)); + this->put_byte(static_cast(size - options.minimum)); + this->FIXED_TYPED_ARRAY(document, + {.size = size, + .encoding = options.encoding, + .prefix_encodings = options.prefix_encodings}); +} + +auto Encoder::FLOOR_TYPED_ARRAY(const sourcemeta::core::JSON &document, + const struct FLOOR_TYPED_ARRAY &options) + -> void { + const auto size{document.size()}; + assert(size >= options.minimum); + this->put_varint(size - options.minimum); + this->FIXED_TYPED_ARRAY(document, + {.size = size, + .encoding = options.encoding, + .prefix_encodings = options.prefix_encodings}); +} + +auto Encoder::ROOF_TYPED_ARRAY(const sourcemeta::core::JSON &document, + const struct ROOF_TYPED_ARRAY &options) -> void { + const auto size{document.size()}; + assert(size <= options.maximum); + this->put_varint(options.maximum - size); + this->FIXED_TYPED_ARRAY(document, + {.size = size, + .encoding = options.encoding, + .prefix_encodings = options.prefix_encodings}); +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/encoder_common.cc b/vendor/jsonbinpack/src/runtime/encoder_common.cc new file mode 100644 index 000000000..2a73947f4 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/encoder_common.cc @@ -0,0 +1,48 @@ +#include + +#include "unreachable.h" + +#include // assert +#include // std::get + +namespace sourcemeta::jsonbinpack { + +Encoder::Encoder(Stream &output) : OutputStream{output} {} + +auto Encoder::write(const sourcemeta::core::JSON &document, + const Encoding &encoding) -> void { + switch (encoding.index()) { +#define HANDLE_ENCODING(index, name) \ + case (index): \ + return this->name(document, \ + std::get(encoding)); + HANDLE_ENCODING(0, BOUNDED_MULTIPLE_8BITS_ENUM_FIXED) + HANDLE_ENCODING(1, FLOOR_MULTIPLE_ENUM_VARINT) + HANDLE_ENCODING(2, ROOF_MULTIPLE_MIRROR_ENUM_VARINT) + HANDLE_ENCODING(3, ARBITRARY_MULTIPLE_ZIGZAG_VARINT) + HANDLE_ENCODING(4, DOUBLE_VARINT_TUPLE) + HANDLE_ENCODING(5, BYTE_CHOICE_INDEX) + HANDLE_ENCODING(6, LARGE_CHOICE_INDEX) + HANDLE_ENCODING(7, TOP_LEVEL_BYTE_CHOICE_INDEX) + HANDLE_ENCODING(8, CONST_NONE) + HANDLE_ENCODING(9, ANY_PACKED_TYPE_TAG_BYTE_PREFIX) + HANDLE_ENCODING(10, UTF8_STRING_NO_LENGTH) + HANDLE_ENCODING(11, FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED) + HANDLE_ENCODING(12, ROOF_VARINT_PREFIX_UTF8_STRING_SHARED) + HANDLE_ENCODING(13, BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED) + HANDLE_ENCODING(14, RFC3339_DATE_INTEGER_TRIPLET) + HANDLE_ENCODING(15, PREFIX_VARINT_LENGTH_STRING_SHARED) + HANDLE_ENCODING(16, FIXED_TYPED_ARRAY) + HANDLE_ENCODING(17, BOUNDED_8BITS_TYPED_ARRAY) + HANDLE_ENCODING(18, FLOOR_TYPED_ARRAY) + HANDLE_ENCODING(19, ROOF_TYPED_ARRAY) + HANDLE_ENCODING(20, FIXED_TYPED_ARBITRARY_OBJECT) + HANDLE_ENCODING(21, VARINT_TYPED_ARBITRARY_OBJECT) +#undef HANDLE_ENCODING + default: + // We should never get here. If so, it is definitely a bug + unreachable(); + } +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/encoder_integer.cc b/vendor/jsonbinpack/src/runtime/encoder_integer.cc new file mode 100644 index 000000000..c229f255c --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/encoder_integer.cc @@ -0,0 +1,78 @@ +#include + +#include + +#include // assert +#include // std::uint8_t, std::int64_t, std::uint64_t + +namespace sourcemeta::jsonbinpack { + +auto Encoder::BOUNDED_MULTIPLE_8BITS_ENUM_FIXED( + const sourcemeta::core::JSON &document, + const struct BOUNDED_MULTIPLE_8BITS_ENUM_FIXED &options) -> void { + assert(document.is_integer()); + const std::int64_t value{document.to_integer()}; + assert(sourcemeta::core::is_within(value, options.minimum, options.maximum)); + assert(options.multiplier > 0); + assert(sourcemeta::core::abs(value) % options.multiplier == 0); + const std::int64_t enum_minimum{ + sourcemeta::core::divide_ceil(options.minimum, options.multiplier)}; +#ifndef NDEBUG + const std::int64_t enum_maximum{ + sourcemeta::core::divide_floor(options.maximum, options.multiplier)}; +#endif + assert(sourcemeta::core::is_byte(enum_maximum - enum_minimum)); + this->put_byte(static_cast( + (value / static_cast(options.multiplier)) - enum_minimum)); +} + +auto Encoder::FLOOR_MULTIPLE_ENUM_VARINT( + const sourcemeta::core::JSON &document, + const struct FLOOR_MULTIPLE_ENUM_VARINT &options) -> void { + assert(document.is_integer()); + const std::int64_t value{document.to_integer()}; + assert(options.minimum <= value); + assert(options.multiplier > 0); + assert(sourcemeta::core::abs(value) % options.multiplier == 0); + if (options.multiplier == 1) { + return this->put_varint( + static_cast(value - options.minimum)); + } + + return this->put_varint( + (static_cast(value) / options.multiplier) - + static_cast(sourcemeta::core::divide_ceil( + options.minimum, static_cast(options.multiplier)))); +} + +auto Encoder::ROOF_MULTIPLE_MIRROR_ENUM_VARINT( + const sourcemeta::core::JSON &document, + const struct ROOF_MULTIPLE_MIRROR_ENUM_VARINT &options) -> void { + assert(document.is_integer()); + const std::int64_t value{document.to_integer()}; + assert(value <= options.maximum); + assert(options.multiplier > 0); + assert(sourcemeta::core::abs(value) % options.multiplier == 0); + if (options.multiplier == 1) { + return this->put_varint( + static_cast(options.maximum - value)); + } + + return this->put_varint( + static_cast( + sourcemeta::core::divide_floor(options.maximum, options.multiplier)) - + (static_cast(value) / options.multiplier)); +} + +auto Encoder::ARBITRARY_MULTIPLE_ZIGZAG_VARINT( + const sourcemeta::core::JSON &document, + const struct ARBITRARY_MULTIPLE_ZIGZAG_VARINT &options) -> void { + assert(document.is_integer()); + const std::int64_t value{document.to_integer()}; + assert(options.multiplier > 0); + assert(sourcemeta::core::abs(value) % options.multiplier == 0); + this->put_varint_zigzag(value / + static_cast(options.multiplier)); +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/encoder_number.cc b/vendor/jsonbinpack/src/runtime/encoder_number.cc new file mode 100644 index 000000000..2812f6d49 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/encoder_number.cc @@ -0,0 +1,21 @@ +#include + +#include + +#include // assert +#include // std::uint64_t + +namespace sourcemeta::jsonbinpack { + +auto Encoder::DOUBLE_VARINT_TUPLE(const sourcemeta::core::JSON &document, + const struct DOUBLE_VARINT_TUPLE &) -> void { + assert(document.is_real()); + const auto value{document.to_real()}; + std::uint64_t point_position; + const std::int64_t integral{ + sourcemeta::core::real_digits(value, point_position)}; + this->put_varint_zigzag(integral); + this->put_varint(point_position); +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/encoder_object.cc b/vendor/jsonbinpack/src/runtime/encoder_object.cc new file mode 100644 index 000000000..c2f5465be --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/encoder_object.cc @@ -0,0 +1,32 @@ +#include + +#include // assert + +namespace sourcemeta::jsonbinpack { + +auto Encoder::FIXED_TYPED_ARBITRARY_OBJECT( + const sourcemeta::core::JSON &document, + const struct FIXED_TYPED_ARBITRARY_OBJECT &options) -> void { + assert(document.is_object()); + assert(document.size() == options.size); + + for (const auto &entry : document.as_object()) { + this->write(sourcemeta::core::JSON{entry.first}, *(options.key_encoding)); + this->write(entry.second, *(options.encoding)); + } +} + +auto Encoder::VARINT_TYPED_ARBITRARY_OBJECT( + const sourcemeta::core::JSON &document, + const struct VARINT_TYPED_ARBITRARY_OBJECT &options) -> void { + assert(document.is_object()); + const auto size{document.size()}; + this->put_varint(size); + + for (const auto &entry : document.as_object()) { + this->write(sourcemeta::core::JSON{entry.first}, *(options.key_encoding)); + this->write(entry.second, *(options.encoding)); + } +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/encoder_string.cc b/vendor/jsonbinpack/src/runtime/encoder_string.cc new file mode 100644 index 000000000..f74516697 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/encoder_string.cc @@ -0,0 +1,150 @@ +#include + +#include // assert +#include // std::uint8_t, std::uint16_t +#include // std::stoul + +namespace sourcemeta::jsonbinpack { + +auto Encoder::UTF8_STRING_NO_LENGTH(const sourcemeta::core::JSON &document, + const struct UTF8_STRING_NO_LENGTH &options) + -> void { + assert(document.is_string()); + const sourcemeta::core::JSON::String &value{document.to_string()}; + this->put_string_utf8(value, options.size); +} + +auto Encoder::FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED( + const sourcemeta::core::JSON &document, + const struct FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED &options) -> void { + assert(document.is_string()); + const sourcemeta::core::JSON::String &value{document.to_string()}; + const auto size{value.size()}; + assert(document.byte_size() == size); + const auto shared{this->cache_.find(value, Cache::Type::Standalone)}; + + // (1) Write 0x00 if shared, else do nothing + if (shared.has_value()) { + this->put_byte(0); + } + + // (2) Write length of the string + 1 (so it will never be zero) + this->put_varint(size - options.minimum + 1); + + // (3) Write relative offset if shared, else write plain string + if (shared.has_value()) { + this->put_varint(this->position() - shared.value()); + } else { + this->cache_.record(value, this->position(), Cache::Type::Standalone); + this->put_string_utf8(value, size); + } +} + +auto Encoder::ROOF_VARINT_PREFIX_UTF8_STRING_SHARED( + const sourcemeta::core::JSON &document, + const struct ROOF_VARINT_PREFIX_UTF8_STRING_SHARED &options) -> void { + assert(document.is_string()); + const sourcemeta::core::JSON::String &value{document.to_string()}; + const auto size{value.size()}; + assert(document.byte_size() == size); + assert(size <= options.maximum); + const auto shared{this->cache_.find(value, Cache::Type::Standalone)}; + + // (1) Write 0x00 if shared, else do nothing + if (shared.has_value()) { + this->put_byte(0); + } + + // (2) Write length of the string + 1 (so it will never be zero) + this->put_varint(options.maximum - size + 1); + + // (3) Write relative offset if shared, else write plain string + if (shared.has_value()) { + this->put_varint(this->position() - shared.value()); + } else { + this->cache_.record(value, this->position(), Cache::Type::Standalone); + this->put_string_utf8(value, size); + } +} + +auto Encoder::BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED( + const sourcemeta::core::JSON &document, + const struct BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED &options) -> void { + assert(document.is_string()); + const sourcemeta::core::JSON::String &value{document.to_string()}; + const auto size{value.size()}; + assert(document.byte_size() == size); + assert(options.minimum <= options.maximum); + assert(sourcemeta::core::is_byte(options.maximum - options.minimum + 1)); + assert(sourcemeta::core::is_within(size, options.minimum, options.maximum)); + const auto shared{this->cache_.find(value, Cache::Type::Standalone)}; + + // (1) Write 0x00 if shared, else do nothing + if (shared.has_value()) { + this->put_byte(0); + } + + // (2) Write length of the string + 1 (so it will never be zero) + this->put_byte(static_cast(size - options.minimum + 1)); + + // (3) Write relative offset if shared, else write plain string + if (shared.has_value()) { + this->put_varint(this->position() - shared.value()); + } else { + this->cache_.record(value, this->position(), Cache::Type::Standalone); + this->put_string_utf8(value, size); + } +} + +auto Encoder::RFC3339_DATE_INTEGER_TRIPLET( + const sourcemeta::core::JSON &document, + const struct RFC3339_DATE_INTEGER_TRIPLET &) -> void { + assert(document.is_string()); + const auto &value{document.to_string()}; + assert(value.size() == 10); + assert(document.size() == value.size()); + + // As according to RFC3339: Internet Protocols MUST + // generate four digit years in dates. + const std::uint16_t year{ + static_cast(std::stoul(value.substr(0, 4)))}; + const std::uint8_t month{ + static_cast(std::stoul(value.substr(5, 2)))}; + const std::uint8_t day{ + static_cast(std::stoul(value.substr(8, 2)))}; + assert(month >= 1 && month <= 12); + assert(day >= 1 && day <= 31); + + this->put_word(year); + this->put_byte(month); + this->put_byte(day); +} + +auto Encoder::PREFIX_VARINT_LENGTH_STRING_SHARED( + const sourcemeta::core::JSON &document, + const struct PREFIX_VARINT_LENGTH_STRING_SHARED &) -> void { + assert(document.is_string()); + const sourcemeta::core::JSON::String &value{document.to_string()}; + + const auto shared{ + this->cache_.find(value, Cache::Type::PrefixLengthVarintPlusOne)}; + if (shared.has_value()) { + const auto new_offset{this->position()}; + this->put_byte(0); + this->put_varint(this->position() - shared.value()); + // Bump the context cache for locality purposes + this->cache_.record(value, new_offset, + Cache::Type::PrefixLengthVarintPlusOne); + } else { + const auto size{value.size()}; + assert(document.byte_size() == size); + this->cache_.record(value, this->position(), + Cache::Type::PrefixLengthVarintPlusOne); + this->put_varint(size + 1); + // Also record a standalone variant of it + this->cache_.record(value, this->position(), Cache::Type::Standalone); + this->put_string_utf8(value, size); + } +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime.h b/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime.h new file mode 100644 index 000000000..f4663635b --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime.h @@ -0,0 +1,61 @@ +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_H_ +#define SOURCEMETA_JSONBINPACK_RUNTIME_H_ + +/// @defgroup runtime Runtime +/// @brief The encoder/decoder parts of JSON BinPack +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT +#include +#endif + +#include + +#include +#include +#include + +#include // std::exception +#include // std::move + +namespace sourcemeta::jsonbinpack { + +/// @ingroup runtime +SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT +auto load(const sourcemeta::core::JSON &input) -> Encoding; + +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup runtime +/// This class represents an encoding error +class SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT EncodingError + : public std::exception { +public: + EncodingError(sourcemeta::core::JSON::String message) + : message_{std::move(message)} {} + + [[nodiscard]] auto what() const noexcept -> const char * override { + return this->message_.c_str(); + } + +private: + sourcemeta::core::JSON::String message_; +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::jsonbinpack + +#endif diff --git a/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_decoder.h b/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_decoder.h new file mode 100644 index 000000000..aabfec004 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_decoder.h @@ -0,0 +1,70 @@ +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_DECODER_H_ +#define SOURCEMETA_JSONBINPACK_RUNTIME_DECODER_H_ + +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT +#include +#endif + +#include +#include + +#include + +namespace sourcemeta::jsonbinpack { + +/// @ingroup runtime +class SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT Decoder : private InputStream { +public: + Decoder(Stream &input); + auto read(const Encoding &encoding) -> sourcemeta::core::JSON; + +// The methods that implement individual encodings as considered private +#ifndef DOXYGEN +#define DECLARE_ENCODING(name) \ + auto name(const name &) -> sourcemeta::core::JSON; + + // Integer + DECLARE_ENCODING(BOUNDED_MULTIPLE_8BITS_ENUM_FIXED) + DECLARE_ENCODING(FLOOR_MULTIPLE_ENUM_VARINT) + DECLARE_ENCODING(ROOF_MULTIPLE_MIRROR_ENUM_VARINT) + DECLARE_ENCODING(ARBITRARY_MULTIPLE_ZIGZAG_VARINT) + + // Number + DECLARE_ENCODING(DOUBLE_VARINT_TUPLE) + + // Any + DECLARE_ENCODING(BYTE_CHOICE_INDEX) + DECLARE_ENCODING(LARGE_CHOICE_INDEX) + DECLARE_ENCODING(TOP_LEVEL_BYTE_CHOICE_INDEX) + DECLARE_ENCODING(CONST_NONE) + DECLARE_ENCODING(ANY_PACKED_TYPE_TAG_BYTE_PREFIX) + + // String + DECLARE_ENCODING(UTF8_STRING_NO_LENGTH) + DECLARE_ENCODING(FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED) + DECLARE_ENCODING(ROOF_VARINT_PREFIX_UTF8_STRING_SHARED) + DECLARE_ENCODING(BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED) + DECLARE_ENCODING(RFC3339_DATE_INTEGER_TRIPLET) + DECLARE_ENCODING(PREFIX_VARINT_LENGTH_STRING_SHARED) + // TODO: Implement STRING_BROTLI encoding + // TODO: Implement STRING_DICTIONARY_COMPRESSOR encoding + // TODO: Implement STRING_UNBOUNDED_SCOPED_PREFIX_LENGTH encoding + // TODO: Implement URL_PROTOCOL_HOST_REST encoding + + // Array + DECLARE_ENCODING(FIXED_TYPED_ARRAY) + DECLARE_ENCODING(BOUNDED_8BITS_TYPED_ARRAY) + DECLARE_ENCODING(FLOOR_TYPED_ARRAY) + DECLARE_ENCODING(ROOF_TYPED_ARRAY) + + // Object + DECLARE_ENCODING(FIXED_TYPED_ARBITRARY_OBJECT) + DECLARE_ENCODING(VARINT_TYPED_ARBITRARY_OBJECT) + +#undef DECLARE_ENCODING +#endif +}; + +} // namespace sourcemeta::jsonbinpack + +#endif diff --git a/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_encoder.h b/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_encoder.h new file mode 100644 index 000000000..ebdb9d01e --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_encoder.h @@ -0,0 +1,75 @@ +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_ENCODER_H_ +#define SOURCEMETA_JSONBINPACK_RUNTIME_ENCODER_H_ + +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT +#include +#endif + +#include +#include +#include + +#include + +namespace sourcemeta::jsonbinpack { + +/// @ingroup runtime +class SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT Encoder : private OutputStream { +public: + Encoder(Stream &output); + auto write(const sourcemeta::core::JSON &document, const Encoding &encoding) + -> void; + +// The methods that implement individual encodings as considered private +#ifndef DOXYGEN +#define DECLARE_ENCODING(name) \ + auto name(const sourcemeta::core::JSON &document, const name &) -> void; + + // Integer + DECLARE_ENCODING(BOUNDED_MULTIPLE_8BITS_ENUM_FIXED) + DECLARE_ENCODING(FLOOR_MULTIPLE_ENUM_VARINT) + DECLARE_ENCODING(ROOF_MULTIPLE_MIRROR_ENUM_VARINT) + DECLARE_ENCODING(ARBITRARY_MULTIPLE_ZIGZAG_VARINT) + + // Number + DECLARE_ENCODING(DOUBLE_VARINT_TUPLE) + + // Any + DECLARE_ENCODING(BYTE_CHOICE_INDEX) + DECLARE_ENCODING(LARGE_CHOICE_INDEX) + DECLARE_ENCODING(TOP_LEVEL_BYTE_CHOICE_INDEX) + DECLARE_ENCODING(CONST_NONE) + DECLARE_ENCODING(ANY_PACKED_TYPE_TAG_BYTE_PREFIX) + + // String + DECLARE_ENCODING(UTF8_STRING_NO_LENGTH) + DECLARE_ENCODING(FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED) + DECLARE_ENCODING(ROOF_VARINT_PREFIX_UTF8_STRING_SHARED) + DECLARE_ENCODING(BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED) + DECLARE_ENCODING(RFC3339_DATE_INTEGER_TRIPLET) + DECLARE_ENCODING(PREFIX_VARINT_LENGTH_STRING_SHARED) + // TODO: Implement STRING_BROTLI encoding + // TODO: Implement STRING_DICTIONARY_COMPRESSOR encoding + // TODO: Implement STRING_UNBOUNDED_SCOPED_PREFIX_LENGTH encoding + // TODO: Implement URL_PROTOCOL_HOST_REST encoding + + // Array + DECLARE_ENCODING(FIXED_TYPED_ARRAY) + DECLARE_ENCODING(BOUNDED_8BITS_TYPED_ARRAY) + DECLARE_ENCODING(FLOOR_TYPED_ARRAY) + DECLARE_ENCODING(ROOF_TYPED_ARRAY) + + // Object + DECLARE_ENCODING(FIXED_TYPED_ARBITRARY_OBJECT) + DECLARE_ENCODING(VARINT_TYPED_ARBITRARY_OBJECT) + +#undef DECLARE_ENCODING +#endif + +private: + Cache cache_; +}; + +} // namespace sourcemeta::jsonbinpack + +#endif diff --git a/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_encoder_cache.h b/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_encoder_cache.h new file mode 100644 index 000000000..79dc14d8b --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_encoder_cache.h @@ -0,0 +1,51 @@ +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_ENCODER_CACHE_H_ +#define SOURCEMETA_JSONBINPACK_RUNTIME_ENCODER_CACHE_H_ +#ifndef DOXYGEN + +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT +#include +#endif + +#include + +#include // std::reference_wrapper +#include // std::map +#include // std::optional +#include // std::pair + +namespace sourcemeta::jsonbinpack { + +class SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT Cache { +public: + enum class Type : std::uint8_t { Standalone, PrefixLengthVarintPlusOne }; + auto record(const sourcemeta::core::JSON::String &value, + const std::uint64_t offset, const Type type) -> void; + [[nodiscard]] auto find(const sourcemeta::core::JSON::String &value, + const Type type) const + -> std::optional; + +#ifndef DOXYGEN + // This method is considered private. We only expose it for testing purposes + auto remove_oldest() -> void; +#endif + +private: +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + std::uint64_t byte_size{0}; + using Entry = std::pair; + std::map data; + std::map> order; +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif +}; + +} // namespace sourcemeta::jsonbinpack + +#endif +#endif diff --git a/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_encoding.h b/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_encoding.h new file mode 100644 index 000000000..f239e8243 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_encoding.h @@ -0,0 +1,1054 @@ +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_ENCODING_H_ +#define SOURCEMETA_JSONBINPACK_RUNTIME_ENCODING_H_ + +#include +#include + +#include // std::int64_t, std::uint64_t +#include // std::shared_ptr +#include // std::variant +#include // std::vector + +namespace sourcemeta::jsonbinpack { + +// Forward declarations for the sole purpose of being bale to define circular +// structures +#ifndef DOXYGEN +struct BOUNDED_MULTIPLE_8BITS_ENUM_FIXED; +struct FLOOR_MULTIPLE_ENUM_VARINT; +struct ROOF_MULTIPLE_MIRROR_ENUM_VARINT; +struct ARBITRARY_MULTIPLE_ZIGZAG_VARINT; +struct DOUBLE_VARINT_TUPLE; +struct BYTE_CHOICE_INDEX; +struct LARGE_CHOICE_INDEX; +struct TOP_LEVEL_BYTE_CHOICE_INDEX; +struct CONST_NONE; +struct ANY_PACKED_TYPE_TAG_BYTE_PREFIX; +struct UTF8_STRING_NO_LENGTH; +struct FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED; +struct ROOF_VARINT_PREFIX_UTF8_STRING_SHARED; +struct BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED; +struct RFC3339_DATE_INTEGER_TRIPLET; +struct PREFIX_VARINT_LENGTH_STRING_SHARED; +struct FIXED_TYPED_ARRAY; +struct BOUNDED_8BITS_TYPED_ARRAY; +struct FLOOR_TYPED_ARRAY; +struct ROOF_TYPED_ARRAY; +struct FIXED_TYPED_ARBITRARY_OBJECT; +struct VARINT_TYPED_ARBITRARY_OBJECT; +#endif + +/// @ingroup runtime +/// Represents an encoding +using Encoding = std::variant< + BOUNDED_MULTIPLE_8BITS_ENUM_FIXED, FLOOR_MULTIPLE_ENUM_VARINT, + ROOF_MULTIPLE_MIRROR_ENUM_VARINT, ARBITRARY_MULTIPLE_ZIGZAG_VARINT, + DOUBLE_VARINT_TUPLE, BYTE_CHOICE_INDEX, LARGE_CHOICE_INDEX, + TOP_LEVEL_BYTE_CHOICE_INDEX, CONST_NONE, ANY_PACKED_TYPE_TAG_BYTE_PREFIX, + UTF8_STRING_NO_LENGTH, FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED, + ROOF_VARINT_PREFIX_UTF8_STRING_SHARED, + BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED, RFC3339_DATE_INTEGER_TRIPLET, + PREFIX_VARINT_LENGTH_STRING_SHARED, FIXED_TYPED_ARRAY, + BOUNDED_8BITS_TYPED_ARRAY, FLOOR_TYPED_ARRAY, ROOF_TYPED_ARRAY, + FIXED_TYPED_ARBITRARY_OBJECT, VARINT_TYPED_ARBITRARY_OBJECT>; + +/// @ingroup runtime +/// @defgroup encoding_integer Integer Encodings +/// @{ + +// clang-format off +/// @brief The encoding consists of the integer value divided by the +/// `multiplier`, minus the ceil of `minimum` divided by the `multiplier`, +/// encoded as an 8-bit fixed-length unsigned integer. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |--------------|--------|-----------------------------| +/// | `minimum` | `int` | The inclusive minimum value | +/// | `maximum` | `int` | The inclusive maximum value | +/// | `multiplier` | `uint` | The multiplier value | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |------------------------------|---------------------------------------------------------------------| +/// | `value >= minimum` | The input value must be greater than or equal to the minimum | +/// | `value <= maximum` | The input value must be less than or equal to the maximum | +/// | `value % multiplier == 0` | The input value must be divisible by the multiplier | +/// | `floor(maximum / multiplier) - ceil(minimum / multiplier) < 2 ** 8` | The divided range must be representable in 8 bits | +/// +/// ### Examples +/// +/// Given the input value 15, where the minimum is 1, the maximum is 19, and the +/// multiplier is 5, the encoding results in the 8-bit unsigned integer 2: +/// +/// ``` +/// +------+ +/// | 0x02 | +/// +------+ +/// ``` +// clang-format on +struct BOUNDED_MULTIPLE_8BITS_ENUM_FIXED { + /// The inclusive minimum value + std::int64_t minimum; + /// The inclusive maximum value + std::int64_t maximum; + /// The multiplier value + std::uint64_t multiplier; +}; + +// clang-format off +/// @brief The encoding consists of the integer value divided by the +/// `multiplier`, minus the ceil of `minimum` divided by the `multiplier`, +/// encoded as a Base-128 64-bit Little Endian variable-length unsigned +/// integer. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |--------------|--------|-----------------------------| +/// | `minimum` | `int` | The inclusive minimum value | +/// | `multiplier` | `uint` | The multiplier value | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |---------------------------|--------------------------------------------------------------| +/// | `value >= minimum` | The input value must be greater than or equal to the minimum | +/// | `value % multiplier == 0` | The input value must be divisible by the multiplier | +/// +/// ### Examples +/// +/// Given the input value 1000, where the minimum is -2 and the multiplier is 4, +/// the encoding results in the Base-128 64-bit Little Endian variable-length +/// unsigned integer 250: +/// +/// ``` +/// +------+------+ +/// | 0xfa | 0x01 | +/// +------+------+ +/// ``` +// clang-format on +struct FLOOR_MULTIPLE_ENUM_VARINT { + /// The inclusive minimum value + std::int64_t minimum; + /// The multiplier value + std::uint64_t multiplier; +}; + +// clang-format off +/// @brief The encoding consists of the floor of `maximum` divided by the +/// `multiplier`, minus the integer value divided by the `multiplier`, encoded +/// as a Base-128 64-bit Little Endian variable-length unsigned integer. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |--------------|--------|-----------------------------| +/// | `maximum` | `int` | The inclusive maximum value | +/// | `multiplier` | `uint` | The multiplier value | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |---------------------------|-----------------------------------------------------------| +/// | `value <= maximum` | The input value must be less than or equal to the maximum | +/// | `value % multiplier == 0` | The input value must be divisible by the multiplier | +/// +/// ### Examples +/// +/// Given the input value 5, where the maximum is 16 and the multiplier is 5, the +/// encoding results in the Base-128 64-bit Little Endian variable-length unsigned +/// integer 2: +/// +/// ``` +/// +------+ +/// | 0x02 | +/// +------+ +/// ``` +// clang-format on +struct ROOF_MULTIPLE_MIRROR_ENUM_VARINT { + /// The inclusive maximum value + std::int64_t maximum; + /// The multiplier value + std::uint64_t multiplier; +}; + +// clang-format off +/// @brief The encoding consists of the the integer value divided by the +/// `multiplier` encoded as a ZigZag-encoded Base-128 64-bit Little Endian +/// variable-length unsigned integer. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |--------------|--------|----------------------| +/// | `multiplier` | `uint` | The multiplier value | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |---------------------------|-----------------------------------------------------| +/// | `value % multiplier == 0` | The input value must be divisible by the multiplier | +/// +/// ### Examples +/// +/// Given the input value 10, where the multiplier is 5, the encoding results in +/// the Base-128 64-bit Little Endian variable-length unsigned integer 4: +/// +/// ``` +/// +------+ +/// | 0x04 | +/// +------+ +/// ``` +// clang-format on +struct ARBITRARY_MULTIPLE_ZIGZAG_VARINT { + /// The multiplier value + std::uint64_t multiplier; +}; + +/// @} + +/// @ingroup runtime +/// @defgroup encoding_number Number Encodings +/// @{ + +// clang-format off +/// @brief The encoding consists of a sequence of two integers: The signed +/// integer that results from concatenating the integral part and the decimal +/// part of the number, if any, as a ZigZag-encoded Base-128 64-bit Little +/// Endian variable-length unsigned integer; and the position of the decimal +/// mark from the last digit of the number encoded as a Base-128 64-bit Little +/// Endian variable-length unsigned integer. +/// +/// ### Options +/// +/// None +/// +/// ### Conditions +/// +/// None +/// +/// ### Examples +/// +/// Given the input value 3.14, the encoding results in the variable-length integer +/// 628 (the ZigZag encoding of 314) followed by the variable-length unsigned +/// integer 2 (the number of decimal digits in the number). +/// +/// ``` +/// +------+------+------+ +/// | 0xf4 | 0x04 | 0x02 | +/// +------+------+------+ +/// ``` +/// +/// Real numbers that represent integers are encoded with a decimal mark of zero. +/// Given the input value -5.0, the encoding results in the variable-length integer +/// 9 (the ZigZag encoding of -5) followed by the variable-length unsigned integer +/// 0. +/// +/// ``` +/// +------+------+ +/// | 0x09 | 0x00 | +/// +------+------+ +/// ``` +// clang-format on +struct DOUBLE_VARINT_TUPLE {}; + +/// @} + +/// @ingroup runtime +/// @defgroup encoding_any Any Encodings +/// @{ + +// clang-format off +/// @brief The encoding consists of an index to the enumeration choices encoded +/// as an 8-bit fixed-length unsigned integer. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |-----------|---------|--------------------------| +/// | `choices` | `any[]` | The set of choice values | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |--------------------------|--------------------------------------------------------| +/// | `len(choices) > 0` | The choices array must not be empty | +/// | `len(choices) < 2 ** 8` | The number of choices must be representable in 8 bits | +/// | `value in choices` | The input value must be included in the set of choices | +/// +/// ### Examples +/// +/// Given an enumeration `[ "foo", "bar", "baz" ]` and an input value `"bar"`, the +/// encoding results in the unsigned 8 bit integer 1: +/// +/// ``` +/// +------+ +/// | 0x01 | +/// +------+ +/// ``` +/// +/// Given an enumeration `[ "foo", "bar", "baz" ]` and an input value `"foo"`, the +/// encoding results in the unsigned 8 bit integer 0: +/// +/// ``` +/// +------+ +/// | 0x00 | +/// +------+ +/// ``` +// clang-format on +struct BYTE_CHOICE_INDEX { + /// The set of choice values + std::vector choices; +}; + +// clang-format off +/// @brief The encoding consists of an index to the enumeration choices encoded +/// as a Base-128 64-bit Little Endian variable-length unsigned integer. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |-----------|---------|--------------------------| +/// | `choices` | `any[]` | The set of choice values | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |--------------------------|--------------------------------------------------------| +/// | `len(choices) > 0` | The choices array must not be empty | +/// | `value in choices` | The input value must be included in the set of choices | +/// +/// ### Examples +/// +/// Given an enumeration with 1000 members and an input value that equals the 300th +/// enumeration value, the encoding results in the Base-128 64-bit Little Endian +/// variable-length unsigned integer 300: +/// +/// ``` +/// +------+------+ +/// | 0xac | 0x02 | +/// +------+------+ +/// ``` +// clang-format on +struct LARGE_CHOICE_INDEX { + /// The set of choice values + std::vector choices; +}; + +// clang-format off +/// @brief If the input value corresponds to the index 0 to the enumeration +/// choices, the encoding stores no data. Otherwise, the encoding consists of +/// an index to the enumeration choices minus 1 encoded as an 8-bit +/// fixed-length unsigned integer. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |-----------|---------|--------------------------| +/// | `choices` | `any[]` | The set of choice values | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |--------------------------|--------------------------------------------------------| +/// | `len(choices) > 0` | The choices array must not be empty | +/// | `len(choices) < 2 ** 8` | The number of choices must be representable in 8 bits | +/// | `value in choices` | The input value must be included in the set of choices | +/// +/// ### Examples +/// +/// Given an enumeration `[ "foo", "bar", "baz" ]` and an input value `"bar"`, the +/// encoding results in the unsigned 8 bit integer 0: +/// +/// ``` +/// +------+ +/// | 0x00 | +/// +------+ +/// ``` +/// +/// Given an enumeration `[ "foo", "bar", "baz" ]` and an input value `"foo"`, the +/// value is not encoded. +// clang-format on +struct TOP_LEVEL_BYTE_CHOICE_INDEX { + /// The set of choice values + std::vector choices; +}; + +// clang-format off +/// @brief The constant input value is not encoded. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |---------|-------|--------------------| +/// | `value` | `any` | The constant value | +/// +/// ### Conditions +/// +/// None +/// +/// ### Examples +/// +/// The input value that matches the `value` option is not encoded. +// clang-format on +struct CONST_NONE { + /// The constant value + sourcemeta::core::JSON value; +}; + +// TODO: Write brief description +struct ANY_PACKED_TYPE_TAG_BYTE_PREFIX {}; +#ifndef DOXYGEN +namespace internal::ANY_PACKED_TYPE_TAG_BYTE_PREFIX { +constexpr auto type_size = 3; +constexpr std::uint8_t TYPE_SHARED_STRING = 0b00000000; +constexpr std::uint8_t TYPE_STRING = 0b00000001; +constexpr std::uint8_t TYPE_LONG_STRING = 0b00000010; +constexpr std::uint8_t TYPE_OBJECT = 0b00000011; +constexpr std::uint8_t TYPE_ARRAY = 0b00000100; +constexpr std::uint8_t TYPE_POSITIVE_INTEGER_BYTE = 0b00000101; +constexpr std::uint8_t TYPE_NEGATIVE_INTEGER_BYTE = 0b00000110; +constexpr std::uint8_t TYPE_OTHER = 0b00000111; +static_assert(TYPE_SHARED_STRING <= sourcemeta::core::uint_max); +static_assert(TYPE_STRING <= sourcemeta::core::uint_max); +static_assert(TYPE_LONG_STRING <= sourcemeta::core::uint_max); +static_assert(TYPE_OBJECT <= sourcemeta::core::uint_max); +static_assert(TYPE_ARRAY <= sourcemeta::core::uint_max); +static_assert(TYPE_POSITIVE_INTEGER_BYTE <= + sourcemeta::core::uint_max); +static_assert(TYPE_NEGATIVE_INTEGER_BYTE <= + sourcemeta::core::uint_max); +static_assert(TYPE_OTHER <= sourcemeta::core::uint_max); + +constexpr auto subtype_size = 5; +constexpr std::uint8_t SUBTYPE_FALSE = 0b00000000; +constexpr std::uint8_t SUBTYPE_TRUE = 0b00000001; +constexpr std::uint8_t SUBTYPE_NULL = 0b00000010; +constexpr std::uint8_t SUBTYPE_POSITIVE_INTEGER = 0b00000011; +constexpr std::uint8_t SUBTYPE_NEGATIVE_INTEGER = 0b00000100; +constexpr std::uint8_t SUBTYPE_NUMBER = 0b00000101; +constexpr std::uint8_t SUBTYPE_POSITIVE_REAL_INTEGER_BYTE = 0b00000110; +constexpr std::uint8_t SUBTYPE_LONG_STRING_BASE_EXPONENT_7 = 0b00000111; +constexpr std::uint8_t SUBTYPE_LONG_STRING_BASE_EXPONENT_8 = 0b00001000; +constexpr std::uint8_t SUBTYPE_LONG_STRING_BASE_EXPONENT_9 = 0b00001001; +constexpr std::uint8_t SUBTYPE_LONG_STRING_BASE_EXPONENT_10 = 0b00001010; + +static_assert(SUBTYPE_FALSE <= sourcemeta::core::uint_max); +static_assert(SUBTYPE_TRUE <= sourcemeta::core::uint_max); +static_assert(SUBTYPE_NULL <= sourcemeta::core::uint_max); +static_assert(SUBTYPE_POSITIVE_INTEGER <= + sourcemeta::core::uint_max); +static_assert(SUBTYPE_NEGATIVE_INTEGER <= + sourcemeta::core::uint_max); +static_assert(SUBTYPE_NUMBER <= sourcemeta::core::uint_max); +static_assert(SUBTYPE_POSITIVE_REAL_INTEGER_BYTE <= + sourcemeta::core::uint_max); +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_7 <= + sourcemeta::core::uint_max); +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_8 <= + sourcemeta::core::uint_max); +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_9 <= + sourcemeta::core::uint_max); +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_10 <= + sourcemeta::core::uint_max); + +// Note that the binary values actually match the declared exponents +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_7 == 7); +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_8 == 8); +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_9 == 9); +static_assert(SUBTYPE_LONG_STRING_BASE_EXPONENT_10 == 10); +} // namespace internal::ANY_PACKED_TYPE_TAG_BYTE_PREFIX +#endif + +/// @} + +/// @ingroup runtime +/// @defgroup encoding_string String Encodings +/// @{ + +// clang-format off +/// @brief The encoding consist in the UTF-8 encoding of the input string. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |--------|--------|------------------------------| +/// | `size` | `uint` | The string UTF-8 byte-length | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |----------------------|-----------------------------------------------------------| +/// | `len(value) == size` | The input string must have the declared UTF-8 byte-length | +/// +/// ### Examples +/// +/// Given the input value "foo bar" with a corresponding size of 7, the encoding +/// results in: +/// +/// ``` +/// +------+------+------+------+------+------+------+ +/// | 0x66 | 0x6f | 0x6f | 0x20 | 0x62 | 0x61 | 0x72 | +/// +------+------+------+------+------+------+------+ +/// f o o b a r +/// ``` +// clang-format on +struct UTF8_STRING_NO_LENGTH { + /// The string UTF-8 byte-length + std::uint64_t size; +}; + +// clang-format off +/// @brief The encoding consists of the byte-length of the string minus +/// `minimum` plus 1 as a Base-128 64-bit Little Endian variable-length +/// unsigned integer followed by the UTF-8 encoding of the input value. +/// +/// Optionally, if the input string has already been encoded to the buffer +/// using UTF-8, the encoding may consist of the byte constant `0x00` followed +/// by the byte-length of the string minus `minimum` plus 1 as a Base-128 +/// 64-bit Little Endian variable-length unsigned integer, followed by the +/// current offset minus the offset to the start of the UTF-8 string value in +/// the buffer encoded as a Base-128 64-bit Little Endian variable-length +/// unsigned integer. +/// +/// #### Options +/// +/// | Option | Type | Description | +/// |-----------|--------|------------------------------------------------| +/// | `minimum` | `uint` | The inclusive minimum string UTF-8 byte-length | +/// +/// #### Conditions +/// +/// | Condition | Description | +/// |-------------------------|----------------------------------------------------------------------| +/// | `len(value) >= minimum` | The input string byte-length is equal to or greater than the minimum | +/// +/// #### Examples +/// +/// Given the input string `foo` with a minimum 3 where the string has not been +/// previously encoded, the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0x01 | 0x66 | 0x6f | 0x6f | +/// +------+------+------+------+ +/// f o o +/// ``` +/// +/// Given the encoding of `foo` with a minimum of 0 followed by the encoding of +/// `foo` with a minimum of 3, the encoding may result in: +/// +/// ``` +/// 0 1 2 3 4 5 6 +/// ^ ^ ^ ^ ^ ^ ^ +/// +------+------+------+------+------+------+------+ +/// | 0x04 | 0x66 | 0x6f | 0x6f | 0x00 | 0x01 | 0x05 | +/// +------+------+------+------+------+------+------+ +/// f o o 6 - 1 +/// ``` +// clang-format on +struct FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED { + /// The inclusive minimum string UTF-8 byte-length + std::uint64_t minimum; +}; + +// clang-format off +/// @brief The encoding consists of `maximum` minus the byte-length of the +/// string plus 1 as a Base-128 64-bit Little Endian variable-length unsigned +/// integer followed by the UTF-8 encoding of the input value. +/// +/// Optionally, if the input string has already been encoded to the buffer +/// using UTF-8, the encoding may consist of the byte constant `0x00` followed +/// by `maximum` minus the byte-length of the string plus 1 as a Base-128 +/// 64-bit Little Endian variable-length unsigned integer, followed by the +/// current offset minus the offset to the start of the UTF-8 string value in +/// the buffer encoded as a Base-128 64-bit Little Endian variable-length +/// unsigned integer. +/// +/// #### Options +/// +/// | Option | Type | Description | +/// |-----------|--------|------------------------------------------------| +/// | `maximum` | `uint` | The inclusive maximum string UTF-8 byte-length | +/// +/// #### Conditions +/// +/// | Condition | Description | +/// |-------------------------|-------------------------------------------------------------------| +/// | `len(value) <= maximum` | The input string byte-length is equal to or less than the maximum | +/// +/// #### Examples +/// +/// Given the input string `foo` with a maximum 4 where the string has not been +/// previously encoded, the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0x02 | 0x66 | 0x6f | 0x6f | +/// +------+------+------+------+ +/// f o o +/// ``` +/// +/// Given the encoding of `foo` with a maximum of 3 followed by the encoding of +/// `foo` with a maximum of 5, the encoding may result in: +/// +/// ``` +/// 0 1 2 3 4 5 6 +/// ^ ^ ^ ^ ^ ^ ^ +/// +------+------+------+------+------+------+------+ +/// | 0x01 | 0x66 | 0x6f | 0x6f | 0x00 | 0x03 | 0x05 | +/// +------+------+------+------+------+------+------+ +/// f o o 6 - 1 +/// ``` +// clang-format on +struct ROOF_VARINT_PREFIX_UTF8_STRING_SHARED { + /// The inclusive maximum string UTF-8 byte-length + std::uint64_t maximum; +}; + +// clang-format off +/// @brief The encoding consists of the byte-length of the string minus +/// `minimum` plus 1 as an 8-bit fixed-length unsigned integer followed by the +/// UTF-8 encoding of the input value. +/// +/// Optionally, if the input string has already been encoded to the buffer +/// using UTF-8, the encoding may consist of the byte constant `0x00` followed +/// by the byte-length of the string minus `minimum` plus 1 as an 8-bit +/// fixed-length unsigned integer, followed by the current offset minus the +/// offset to the start of the UTF-8 string value in the buffer encoded as a +/// Base-128 64-bit Little Endian variable-length unsigned integer. +/// +/// The byte-length of the string is encoded even if `maximum` equals `minimum` +/// in order to disambiguate between shared and non-shared fixed strings. +/// +/// #### Options +/// +/// | Option | Type | Description | +/// |-----------|--------|------------------------------------------------| +/// | `minimum` | `uint` | The inclusive minimum string UTF-8 byte-length | +/// | `maximum` | `uint` | The inclusive maximum string UTF-8 byte-length | +/// +/// #### Conditions +/// +/// | Condition | Description | +/// |----------------------------------|----------------------------------------------------------------------| +/// | `len(value) >= minimum` | The input string byte-length is equal to or greater than the minimum | +/// | `len(value) <= maximum` | The input string byte-length is equal to or less than the maximum | +/// | `maximum - minimum < 2 ** 8 - 1` | The range minus 1 must be representable in 8 bits | +/// +/// #### Examples +/// +/// Given the input string `foo` with a minimum 3 and a maximum 5 where the string +/// has not been previously encoded, the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0x01 | 0x66 | 0x6f | 0x6f | +/// +------+------+------+------+ +/// f o o +/// ``` +/// +/// Given the encoding of `foo` with a minimum of 0 and a maximum of 6 followed by +/// the encoding of `foo` with a minimum of 3 and a maximum of 100, the encoding +/// may result in: +/// +/// ``` +/// 0 1 2 3 4 5 6 +/// ^ ^ ^ ^ ^ ^ ^ +/// +------+------+------+------+------+------+------+ +/// | 0x04 | 0x66 | 0x6f | 0x6f | 0x00 | 0x01 | 0x05 | +/// +------+------+------+------+------+------+------+ +/// f o o 6 - 1 +/// ``` +// clang-format on +struct BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED { + /// The inclusive minimum string UTF-8 byte-length + std::uint64_t minimum; + /// The inclusive maximum string UTF-8 byte-length + std::uint64_t maximum; +}; + +// clang-format off +/// @brief The encoding consists of an implementation of +/// [RFC3339](https://datatracker.ietf.org/doc/html/rfc3339) date expressions +/// as the sequence of 3 integers: the year as a 16-bit fixed-length Little +/// Endian unsigned integer, the month as an 8-bit fixed-length unsigned +/// integer, and the day as an 8-bit fixed-length unsigned integer. +/// +/// #### Options +/// +/// None +/// +/// #### Conditions +/// +/// | Condition | Description | +/// |----------------------|-------------------------------------------------------------| +/// | `len(value) == 10` | The input string consists of 10 characters | +/// | `value[0:4] >= 0` | The year is greater than or equal to 0 | +/// | `value[0:4] <= 9999` | The year is less than or equal to 9999 as stated by RFC3339 | +/// | `value[4] == "-"` | The year and the month are divided by a hyphen | +/// | `value[5:7] >= 1` | The month is greater than or equal to 1 | +/// | `value[5:7] <= 12` | The month is less than or equal to 12 | +/// | `value[7] == "-"` | The month and the day are divided by a hyphen | +/// | `value[8:10] >= 1` | The day is greater than or equal to 1 | +/// | `value[8:10] <= 31` | The day is less than or equal to 31 | +/// +/// #### Examples +/// +/// Given the input string `2014-10-01`, the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0xde | 0x07 | 0x0a | 0x01 | +/// +------+------+------+------+ +/// year ... month day +/// ``` +// clang-format on +struct RFC3339_DATE_INTEGER_TRIPLET {}; + +// clang-format off +/// @brief The encoding consists of the byte-length of the string plus 1 as a +/// Base-128 64-bit Little Endian variable-length unsigned integer followed by +/// the UTF-8 encoding of the input value. +/// +/// Optionally, if the input string has already been encoded to the buffer +/// using this encoding the encoding may consist of the byte constant `0x00` +/// followed by the current offset minus the offset to the start of the string +/// as a Base-128 64-bit Little Endian variable-length unsigned integer. It is +/// permissible to point to another instance of the string that is a pointer +/// itself. +/// +/// ### Options +/// +/// None +/// +/// ### Conditions +/// +/// None +/// +/// ### Examples +/// +/// Given the input string `foo` where the string has not been previously encoded, +/// the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0x04 | 0x66 | 0x6f | 0x6f | +/// +------+------+------+------+ +/// f o o +/// ``` +/// +/// Given the encoding of `foo` repeated 3 times, the encoding may result in: +/// +/// ``` +/// 0 1 2 3 4 5 6 7 +/// ^ ^ ^ ^ ^ ^ ^ ^ +/// +------+------+------+------+------+------+------+------+ +/// | 0x04 | 0x66 | 0x6f | 0x6f | 0x00 | 0x05 | 0x00 | 0x03 | +/// +------+------+------+------+------+------+------+------+ +/// f o o 5 - 0 7 - 4 +/// ``` +// clang-format on +struct PREFIX_VARINT_LENGTH_STRING_SHARED {}; + +/// @} + +/// @ingroup runtime +/// @defgroup encoding_array Array Encodings +/// @{ + +// clang-format off +/// @brief The encoding consists of the elements of the fixed array encoded in +/// order. The encoding of the element at index `i` is either +/// `prefix_encodings[i]` if set, or `encoding`. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |-------------------|--------------|----------------------| +/// | `size` | `uint` | The array length | +/// | `prefixEncodings` | `encoding[]` | Positional encodings | +/// | `encoding` | `encoding` | Element encoding | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |--------------------------------|-----------------------------------------------------------------------| +/// | `len(prefixEncodings) <= size` | The number of prefix encodings must be less than or equal to the size | +/// | `len(value) == size` | The input array must have the declared size | +/// +/// ### Examples +/// +/// Given the array `[ 1, 2, true ]` where the `prefixEncodings` corresponds to +/// BOUNDED_MULTIPLE_8BITS_ENUM_FIXED (minimum 0, maximum 10, multiplier 1) and +/// BOUNDED_MULTIPLE_8BITS_ENUM_FIXED (minimum 0, maximum 10, multiplier 1) and +/// `encoding` corresponds to BYTE_CHOICE_INDEX with choices `[ false, true ]`, +/// the encoding results in: +/// +/// ``` +/// +------+------+------+ +/// | 0x00 | 0x01 | 0x01 | +/// +------+------+------+ +/// 1 2 true +/// ``` +// clang-format on +struct FIXED_TYPED_ARRAY { + /// The array length + std::uint64_t size; + /// Element encoding + std::shared_ptr encoding; + /// Positional encodings + std::vector prefix_encodings; +}; + +// clang-format off +/// @brief The encoding consists of the length of the array minus `minimum` +/// encoded as an 8-bit fixed-length unsigned integer followed by the elements +/// of the array encoded in order. The encoding of the element at index `i` is +/// either `prefix_encodings[i]` if set, or `encoding`. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |-------------------|--------------|---------------------------------| +/// | `minimum` | `uint` | The minimum length of the array | +/// | `maximum` | `uint` | The maximum length of the array | +/// | `prefixEncodings` | `encoding[]` | Positional encodings | +/// | `encoding` | `encoding` | Element encoding | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |----------------------------------------|---------------------------------------------------------------------------------------| +/// | `len(value) >= minimum` | The length of the array must be greater than or equal to the minimum | +/// | `len(value) <= maximum` | The length of the array must be less than or equal to the maximum | +/// | `len(prefixEncodings) <= maximum` | The number of prefix encodings must be less than or equal to the maximum array length | +/// | `len(maximum) - len(minimum) < 2 ** 8` | The array length must be representable in 8 bits | +/// +/// ### Examples +/// +/// Given the array `[ true, false, 5 ]` where the minimum is 1 and the maximum is +/// 3, the `prefixEncodings` corresponds to BYTE_CHOICE_INDEX with +/// choices `[ false, true ]` and BYTE_CHOICE_INDEX with choices `[ +/// false, true ]` and `encoding` corresponds to +/// BOUNDED_MULTIPLE_8BITS_ENUM_FIXED with minimum 0 and maximum +/// 255, the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0x02 | 0x01 | 0x00 | 0x05 | +/// +------+------+------+------+ +/// size true false 5 +/// ``` +// clang-format on +struct BOUNDED_8BITS_TYPED_ARRAY { + /// The minimum length of the array + std::uint64_t minimum; + /// The maximum length of the array + std::uint64_t maximum; + /// Element encoding + std::shared_ptr encoding; + /// Positional encodings + std::vector prefix_encodings; +}; + +// clang-format off +/// @brief The encoding consists of the length of the array minus `minimum` +/// encoded as a Base-128 64-bit Little Endian variable-length unsigned integer +/// followed by the elements of the array encoded in order. The encoding of the +/// element at index `i` is either `prefix_encodings[i]` if set, or `encoding`. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |-------------------|--------------|---------------------------------| +/// | `minimum` | `uint` | The minimum length of the array | +/// | `prefixEncodings` | `encoding[]` | Positional encodings | +/// | `encoding` | `encoding` | Element encoding | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |-------------------------|----------------------------------------------------------------------| +/// | `len(value) >= minimum` | The length of the array must be greater than or equal to the minimum | +/// +/// ### Examples +/// +/// TODO: Give an example of an array with more than 8-bit of elements +/// +/// Given the array `[ true, false, 5 ]` where the minimum is 1, the +/// `prefixEncodings` corresponds to BYTE_CHOICE_INDEX with choices `[ +/// false, true ]` and BYTE_CHOICE_INDEX with choices `[ false, true ]` +/// and `encoding` corresponds to BOUNDED_MULTIPLE_8BITS_ENUM_FIXED +/// with minimum 0 and maximum 255, the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0x02 | 0x01 | 0x00 | 0x05 | +/// +------+------+------+------+ +/// size true false 5 +/// ``` +// clang-format on +struct FLOOR_TYPED_ARRAY { + /// The minimum length of the array + std::uint64_t minimum; + /// Element encoding + std::shared_ptr encoding; + /// Positional encodings + std::vector prefix_encodings; +}; + +// clang-format off +/// @brief The encoding consists of `maximum` minus the length of the array +/// encoded as a Base-128 64-bit Little Endian variable-length unsigned integer +/// followed by the elements of the array encoded in order. The encoding of the +/// element at index `i` is either `prefix_encodings[i]` if set, or `encoding`. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |-------------------|--------------|---------------------------------| +/// | `maximum` | `uint` | The maximum length of the array | +/// | `prefixEncodings` | `encoding[]` | Positional encodings | +/// | `encoding` | `encoding` | Element encoding | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |-----------------------------------|---------------------------------------------------------------------------------------| +/// | `len(prefixEncodings) <= maximum` | The number of prefix encodings must be less than or equal to the maximum array length | +/// +/// ### Examples +/// +/// Given the array `[ true, false, 5 ]` where the maximum is 3, the +/// `prefixEncodings` options corresponds to BYTE_CHOICE_INDEX with +/// choices `[ false, true ]` and BYTE_CHOICE_INDEX with choices `[ +/// false, true ]` and `encoding` corresponds to +/// BOUNDED_MULTIPLE_8BITS_ENUM_FIXED with minimum 0 and maximum +/// 255, the encoding results in: +/// +/// ``` +/// +------+------+------+------+ +/// | 0x00 | 0x01 | 0x00 | 0x05 | +/// +------+------+------+------+ +/// size true false 5 +/// ``` +// clang-format on +struct ROOF_TYPED_ARRAY { + /// The maximum length of the array + std::uint64_t maximum; + /// Element encoding + std::shared_ptr encoding; + /// Positional encodings + std::vector prefix_encodings; +}; + +/// @} + +/// @ingroup runtime +/// @defgroup encoding_object Object Encodings +/// @{ + +// clang-format off +/// @brief The encoding consists of each pair encoded as the key followed by +/// the value according to `key_encoding` and `encoding`. The order in which +/// pairs are encoded is undefined. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |---------------|------------|-----------------| +/// | `size` | `uint` | The object size | +/// | `keyEncoding` | `encoding` | Key encoding | +/// | `encoding` | `encoding` | Value encoding | +/// +/// ### Conditions +/// +/// | Condition | Description | +/// |----------------------|-----------------------------------------------------------| +/// | `len(value) == size` | The input object must have the declared amount of entries | +/// +/// ### Examples +/// +/// Given the array `{ "foo": 1, "bar": 2 }` where `keyEncoding` corresponds to +/// UTF8_STRING_NO_LENGTH (size 3) and `encoding` corresponds to +/// BOUNDED_MULTIPLE_8BITS_ENUM_FIXED (minimum 0, maximum 10, +/// multiplier 1), the encoding results in: +/// +/// ``` +/// +------+------+------+------+------+------+------+------+ +/// | 0x66 | 0x6f | 0x6f | 0x01 | 0x62 | 0x61 | 0x72 | 0x02 | +/// +------+------+------+------+------+------+------+------+ +/// f o o 1 b a r 2 +/// ``` +/// +/// Or: +/// +/// ``` +/// +------+------+------+------+------+------+------+------+ +/// | 0x62 | 0x61 | 0x72 | 0x02 | 0x66 | 0x6f | 0x6f | 0x01 | +/// +------+------+------+------+------+------+------+------+ +/// b a r 2 f o o 1 +/// ``` +// clang-format on +struct FIXED_TYPED_ARBITRARY_OBJECT { + /// The object size + std::uint64_t size; + /// Key encoding + std::shared_ptr key_encoding; + /// Value encoding + std::shared_ptr encoding; +}; + +// clang-format off +/// @brief The encoding consists of the number of key-value pairs in the input +/// object as a Base-128 64-bit Little Endian variable-length unsigned integer +/// followed by each pair encoded as the key followed by the value according to +/// `key_encoding` and `encoding`. The order in which pairs are encoded is +/// undefined. +/// +/// ### Options +/// +/// | Option | Type | Description | +/// |---------------|------------|----------------| +/// | `keyEncoding` | `encoding` | Key encoding | +/// | `encoding` | `encoding` | Value encoding | +/// +/// ### Examples +/// +/// Given the array `{ "foo": 1, "bar": 2 }` where `keyEncoding` corresponds to +/// UTF8_STRING_NO_LENGTH (size 3) and `encoding` corresponds to +/// BOUNDED_MULTIPLE_8BITS_ENUM_FIXED (minimum 0, maximum 10, +/// multiplier 1), the encoding results in: +/// +/// ``` +/// +------+------+------+------+------+------+------+------+------+ +/// | 0x02 | 0x66 | 0x6f | 0x6f | 0x01 | 0x62 | 0x61 | 0x72 | 0x02 | +/// +------+------+------+------+------+------+------+------+------+ +/// 2 f o o 1 b a r 2 +/// ``` +/// +/// Or: +/// +/// ``` +/// +------+------+------+------+------+------+------+------+------+ +/// | 0x02 | 0x62 | 0x61 | 0x72 | 0x02 | 0x66 | 0x6f | 0x6f | 0x01 | +/// +------+------+------+------+------+------+------+------+------+ +/// 2 b a r 2 f o o 1 +/// ``` +// clang-format on +struct VARINT_TYPED_ARBITRARY_OBJECT { + /// Key encoding + std::shared_ptr key_encoding; + /// Value encoding + std::shared_ptr encoding; +}; + +/// @} + +} // namespace sourcemeta::jsonbinpack + +#endif diff --git a/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_input_stream.h b/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_input_stream.h new file mode 100644 index 000000000..ee9d4bac9 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_input_stream.h @@ -0,0 +1,35 @@ +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_INPUT_STREAM_H_ +#define SOURCEMETA_JSONBINPACK_RUNTIME_INPUT_STREAM_H_ + +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT +#include +#endif + +#include +#include + +#include // std::uint64_t, std::int64_t +#include // std::basic_istream + +namespace sourcemeta::jsonbinpack { + +/// @ingroup runtime +class SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT InputStream + : public sourcemeta::core::BinaryReader { +public: + using Stream = std::basic_istream; + InputStream(Stream &input); + + // Seek backwards given a relative offset + auto rewind(const std::uint64_t relative_offset, const std::uint64_t position) + -> std::uint64_t; + auto get_varint() -> std::uint64_t; + auto get_varint_zigzag() -> std::int64_t; + auto get_string_utf8(const std::uint64_t length) + -> sourcemeta::core::JSON::String; +}; + +} // namespace sourcemeta::jsonbinpack + +#endif diff --git a/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_output_stream.h b/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_output_stream.h new file mode 100644 index 000000000..3207d66b6 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/include/sourcemeta/jsonbinpack/runtime_output_stream.h @@ -0,0 +1,32 @@ +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_OUTPUT_STREAM_H_ +#define SOURCEMETA_JSONBINPACK_RUNTIME_OUTPUT_STREAM_H_ + +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT +#include +#endif + +#include +#include + +#include // std::uint64_t, std::int64_t +#include // std::basic_ostream + +namespace sourcemeta::jsonbinpack { + +/// @ingroup runtime +class SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT OutputStream + : public sourcemeta::core::BinaryWriter { +public: + using Stream = std::basic_ostream; + OutputStream(Stream &output); + + auto put_varint(const std::uint64_t value) -> void; + auto put_varint_zigzag(const std::int64_t value) -> void; + auto put_string_utf8(const sourcemeta::core::JSON::String &string, + const std::uint64_t length) -> void; +}; + +} // namespace sourcemeta::jsonbinpack + +#endif diff --git a/vendor/jsonbinpack/src/runtime/input_stream.cc b/vendor/jsonbinpack/src/runtime/input_stream.cc new file mode 100644 index 000000000..f98ed5af7 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/input_stream.cc @@ -0,0 +1,68 @@ +#include + +#include + +#include // assert +#include // std::size_t +#include // std::uint8_t, std::uint64_t, std::int64_t + +namespace sourcemeta::jsonbinpack { + +InputStream::InputStream(Stream &input) + : sourcemeta::core::BinaryReader{input} {} + +auto InputStream::rewind(const std::uint64_t relative_offset, + const std::uint64_t position) -> std::uint64_t { + assert(position >= relative_offset); + const std::uint64_t offset{position - relative_offset}; + assert(offset < position); + const std::uint64_t current{this->position()}; + this->seek(offset); + return current; +} + +auto InputStream::get_varint() -> std::uint64_t { + constexpr std::uint8_t LEAST_SIGNIFICANT_BITS{0b01111111}; + constexpr std::uint8_t MOST_SIGNIFICANT_BIT{0b10000000}; + constexpr std::uint8_t SHIFT{7}; + std::uint64_t result{0}; + std::size_t cursor{0}; + while (true) { + const std::uint8_t byte{this->get_byte()}; + const std::uint64_t value{ + static_cast(byte & LEAST_SIGNIFICANT_BITS)}; +#ifndef NDEBUG + const std::uint64_t current = result; +#endif + result += static_cast(value << SHIFT * cursor); + // Try to catch potential overflows from the above addition + assert(result >= current); + cursor += 1; + if ((byte & MOST_SIGNIFICANT_BIT) == 0) { + break; + } + } + + return result; +} + +auto InputStream::get_varint_zigzag() -> std::int64_t { + return sourcemeta::core::zigzag_decode(this->get_varint()); +} + +auto InputStream::get_string_utf8(const std::uint64_t length) + -> sourcemeta::core::JSON::String { + sourcemeta::core::JSON::String result; + result.reserve(length); + std::uint64_t counter = 0; + while (counter < length) { + result += static_cast(this->get_byte()); + counter += 1; + } + + assert(counter == length); + assert(result.size() == length); + return result; +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/loader.cc b/vendor/jsonbinpack/src/runtime/loader.cc new file mode 100644 index 000000000..2f5b61d55 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/loader.cc @@ -0,0 +1,60 @@ +#include + +#include "loader_v1_any.h" +#include "loader_v1_array.h" +#include "loader_v1_integer.h" +#include "loader_v1_number.h" +#include "loader_v1_string.h" + +#include // assert +#include // std::ostringstream +#include // std::runtime_error + +namespace sourcemeta::jsonbinpack { + +auto load(const sourcemeta::core::JSON &input) -> Encoding { + assert(input.defines("binpackEncoding")); + assert(input.defines("binpackOptions")); + const auto encoding{input.at("binpackEncoding").to_string()}; + const auto &options{input.at("binpackOptions")}; + +#define PARSE_ENCODING(version, name) \ + if (encoding == #name) \ + return version::name(options); + + // Integers + PARSE_ENCODING(v1, BOUNDED_MULTIPLE_8BITS_ENUM_FIXED) + PARSE_ENCODING(v1, FLOOR_MULTIPLE_ENUM_VARINT) + PARSE_ENCODING(v1, ROOF_MULTIPLE_MIRROR_ENUM_VARINT) + PARSE_ENCODING(v1, ARBITRARY_MULTIPLE_ZIGZAG_VARINT) + // Numbers + PARSE_ENCODING(v1, DOUBLE_VARINT_TUPLE) + // Any + PARSE_ENCODING(v1, BYTE_CHOICE_INDEX) + PARSE_ENCODING(v1, LARGE_CHOICE_INDEX) + PARSE_ENCODING(v1, TOP_LEVEL_BYTE_CHOICE_INDEX) + PARSE_ENCODING(v1, CONST_NONE) + PARSE_ENCODING(v1, ANY_PACKED_TYPE_TAG_BYTE_PREFIX) + // Strings + PARSE_ENCODING(v1, UTF8_STRING_NO_LENGTH) + PARSE_ENCODING(v1, FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED) + PARSE_ENCODING(v1, ROOF_VARINT_PREFIX_UTF8_STRING_SHARED) + PARSE_ENCODING(v1, BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED) + PARSE_ENCODING(v1, RFC3339_DATE_INTEGER_TRIPLET) + PARSE_ENCODING(v1, PREFIX_VARINT_LENGTH_STRING_SHARED) + // Arrays + PARSE_ENCODING(v1, FIXED_TYPED_ARRAY) + PARSE_ENCODING(v1, BOUNDED_8BITS_TYPED_ARRAY) + PARSE_ENCODING(v1, FLOOR_TYPED_ARRAY) + PARSE_ENCODING(v1, ROOF_TYPED_ARRAY) + + // TODO: Handle object encodings + +#undef PARSE_ENCODING + + std::ostringstream error; + error << "Unrecognized encoding: " << encoding; + throw EncodingError(error.str()); +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/loader_v1_any.h b/vendor/jsonbinpack/src/runtime/loader_v1_any.h new file mode 100644 index 000000000..0cfaecb57 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/loader_v1_any.h @@ -0,0 +1,57 @@ +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_LOADER_V1_ANY_H_ +#define SOURCEMETA_JSONBINPACK_RUNTIME_LOADER_V1_ANY_H_ + +#include + +#include + +#include // assert +#include // std::move +#include // std::vector + +namespace sourcemeta::jsonbinpack::v1 { + +auto BYTE_CHOICE_INDEX(const sourcemeta::core::JSON &options) -> Encoding { + assert(options.defines("choices")); + const auto &choices{options.at("choices")}; + assert(choices.is_array()); + const auto &array{choices.as_array()}; + std::vector elements{array.cbegin(), array.cend()}; + return sourcemeta::jsonbinpack::BYTE_CHOICE_INDEX{.choices = + std::move(elements)}; +} + +auto LARGE_CHOICE_INDEX(const sourcemeta::core::JSON &options) -> Encoding { + assert(options.defines("choices")); + const auto &choices{options.at("choices")}; + assert(choices.is_array()); + const auto &array{choices.as_array()}; + std::vector elements{array.cbegin(), array.cend()}; + return sourcemeta::jsonbinpack::LARGE_CHOICE_INDEX{.choices = + std::move(elements)}; +} + +auto TOP_LEVEL_BYTE_CHOICE_INDEX(const sourcemeta::core::JSON &options) + -> Encoding { + assert(options.defines("choices")); + const auto &choices{options.at("choices")}; + assert(choices.is_array()); + const auto &array{choices.as_array()}; + std::vector elements{array.cbegin(), array.cend()}; + return sourcemeta::jsonbinpack::TOP_LEVEL_BYTE_CHOICE_INDEX{ + .choices = std::move(elements)}; +} + +auto CONST_NONE(const sourcemeta::core::JSON &options) -> Encoding { + assert(options.defines("value")); + return sourcemeta::jsonbinpack::CONST_NONE{.value = options.at("value")}; +} + +auto ANY_PACKED_TYPE_TAG_BYTE_PREFIX(const sourcemeta::core::JSON &) + -> Encoding { + return sourcemeta::jsonbinpack::ANY_PACKED_TYPE_TAG_BYTE_PREFIX{}; +} + +} // namespace sourcemeta::jsonbinpack::v1 + +#endif diff --git a/vendor/jsonbinpack/src/runtime/loader_v1_array.h b/vendor/jsonbinpack/src/runtime/loader_v1_array.h new file mode 100644 index 000000000..253fb63c4 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/loader_v1_array.h @@ -0,0 +1,117 @@ +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_LOADER_V1_ARRAY_H_ +#define SOURCEMETA_JSONBINPACK_RUNTIME_LOADER_V1_ARRAY_H_ + +#include + +#include + +#include // std::transform +#include // assert +#include // std::uint64_t +#include // std::back_inserter +#include // std::make_shared +#include // std::vector + +namespace sourcemeta::jsonbinpack::v1 { + +auto FIXED_TYPED_ARRAY(const sourcemeta::core::JSON &options) -> Encoding { + assert(options.defines("size")); + assert(options.defines("encoding")); + assert(options.defines("prefixEncodings")); + const auto &size{options.at("size")}; + const auto &array_encoding{options.at("encoding")}; + const auto &prefix_encodings{options.at("prefixEncodings")}; + assert(size.is_integer()); + assert(size.is_positive()); + assert(array_encoding.is_object()); + assert(prefix_encodings.is_array()); + std::vector encodings; + std::transform(prefix_encodings.as_array().cbegin(), + prefix_encodings.as_array().cend(), + std::back_inserter(encodings), + [](const auto &element) { return load(element); }); + assert(encodings.size() == prefix_encodings.size()); + return sourcemeta::jsonbinpack::FIXED_TYPED_ARRAY{ + .size = static_cast(size.to_integer()), + .encoding = std::make_shared(load(array_encoding)), + .prefix_encodings = std::move(encodings)}; +} + +auto BOUNDED_8BITS_TYPED_ARRAY(const sourcemeta::core::JSON &options) + -> Encoding { + assert(options.defines("minimum")); + assert(options.defines("maximum")); + assert(options.defines("encoding")); + assert(options.defines("prefixEncodings")); + const auto &minimum{options.at("minimum")}; + const auto &maximum{options.at("maximum")}; + const auto &array_encoding{options.at("encoding")}; + const auto &prefix_encodings{options.at("prefixEncodings")}; + assert(minimum.is_integer()); + assert(maximum.is_integer()); + assert(minimum.is_positive()); + assert(maximum.is_positive()); + assert(array_encoding.is_object()); + assert(prefix_encodings.is_array()); + std::vector encodings; + std::transform(prefix_encodings.as_array().cbegin(), + prefix_encodings.as_array().cend(), + std::back_inserter(encodings), + [](const auto &element) { return load(element); }); + assert(encodings.size() == prefix_encodings.size()); + return sourcemeta::jsonbinpack::BOUNDED_8BITS_TYPED_ARRAY{ + .minimum = static_cast(minimum.to_integer()), + .maximum = static_cast(maximum.to_integer()), + .encoding = std::make_shared(load(array_encoding)), + .prefix_encodings = std::move(encodings)}; +} + +auto FLOOR_TYPED_ARRAY(const sourcemeta::core::JSON &options) -> Encoding { + assert(options.defines("minimum")); + assert(options.defines("encoding")); + assert(options.defines("prefixEncodings")); + const auto &minimum{options.at("minimum")}; + const auto &array_encoding{options.at("encoding")}; + const auto &prefix_encodings{options.at("prefixEncodings")}; + assert(minimum.is_integer()); + assert(minimum.is_positive()); + assert(array_encoding.is_object()); + assert(prefix_encodings.is_array()); + std::vector encodings; + std::transform(prefix_encodings.as_array().cbegin(), + prefix_encodings.as_array().cend(), + std::back_inserter(encodings), + [](const auto &element) { return load(element); }); + assert(encodings.size() == prefix_encodings.size()); + return sourcemeta::jsonbinpack::FLOOR_TYPED_ARRAY{ + .minimum = static_cast(minimum.to_integer()), + .encoding = std::make_shared(load(array_encoding)), + .prefix_encodings = std::move(encodings)}; +} + +auto ROOF_TYPED_ARRAY(const sourcemeta::core::JSON &options) -> Encoding { + assert(options.defines("maximum")); + assert(options.defines("encoding")); + assert(options.defines("prefixEncodings")); + const auto &maximum{options.at("maximum")}; + const auto &array_encoding{options.at("encoding")}; + const auto &prefix_encodings{options.at("prefixEncodings")}; + assert(maximum.is_integer()); + assert(maximum.is_positive()); + assert(array_encoding.is_object()); + assert(prefix_encodings.is_array()); + std::vector encodings; + std::transform(prefix_encodings.as_array().cbegin(), + prefix_encodings.as_array().cend(), + std::back_inserter(encodings), + [](const auto &element) { return load(element); }); + assert(encodings.size() == prefix_encodings.size()); + return sourcemeta::jsonbinpack::ROOF_TYPED_ARRAY{ + .maximum = static_cast(maximum.to_integer()), + .encoding = std::make_shared(load(array_encoding)), + .prefix_encodings = std::move(encodings)}; +} + +} // namespace sourcemeta::jsonbinpack::v1 + +#endif diff --git a/vendor/jsonbinpack/src/runtime/loader_v1_integer.h b/vendor/jsonbinpack/src/runtime/loader_v1_integer.h new file mode 100644 index 000000000..74e45d5af --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/loader_v1_integer.h @@ -0,0 +1,71 @@ +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_LOADER_V1_INTEGER_H_ +#define SOURCEMETA_JSONBINPACK_RUNTIME_LOADER_V1_INTEGER_H_ + +#include + +#include + +#include // assert +#include // std::uint64_t + +namespace sourcemeta::jsonbinpack::v1 { + +auto BOUNDED_MULTIPLE_8BITS_ENUM_FIXED(const sourcemeta::core::JSON &options) + -> Encoding { + assert(options.defines("minimum")); + assert(options.defines("maximum")); + assert(options.defines("multiplier")); + const auto &minimum{options.at("minimum")}; + const auto &maximum{options.at("maximum")}; + const auto &multiplier{options.at("multiplier")}; + assert(minimum.is_integer()); + assert(maximum.is_integer()); + assert(multiplier.is_integer()); + assert(multiplier.is_positive()); + return sourcemeta::jsonbinpack::BOUNDED_MULTIPLE_8BITS_ENUM_FIXED{ + .minimum = minimum.to_integer(), + .maximum = maximum.to_integer(), + .multiplier = static_cast(multiplier.to_integer())}; +} + +auto FLOOR_MULTIPLE_ENUM_VARINT(const sourcemeta::core::JSON &options) + -> Encoding { + assert(options.defines("minimum")); + assert(options.defines("multiplier")); + const auto &minimum{options.at("minimum")}; + const auto &multiplier{options.at("multiplier")}; + assert(minimum.is_integer()); + assert(multiplier.is_integer()); + assert(multiplier.is_positive()); + return sourcemeta::jsonbinpack::FLOOR_MULTIPLE_ENUM_VARINT{ + .minimum = minimum.to_integer(), + .multiplier = static_cast(multiplier.to_integer())}; +} + +auto ROOF_MULTIPLE_MIRROR_ENUM_VARINT(const sourcemeta::core::JSON &options) + -> Encoding { + assert(options.defines("maximum")); + assert(options.defines("multiplier")); + const auto &maximum{options.at("maximum")}; + const auto &multiplier{options.at("multiplier")}; + assert(maximum.is_integer()); + assert(multiplier.is_integer()); + assert(multiplier.is_positive()); + return sourcemeta::jsonbinpack::ROOF_MULTIPLE_MIRROR_ENUM_VARINT{ + .maximum = maximum.to_integer(), + .multiplier = static_cast(multiplier.to_integer())}; +} + +auto ARBITRARY_MULTIPLE_ZIGZAG_VARINT(const sourcemeta::core::JSON &options) + -> Encoding { + assert(options.defines("multiplier")); + const auto &multiplier{options.at("multiplier")}; + assert(multiplier.is_integer()); + assert(multiplier.is_positive()); + return sourcemeta::jsonbinpack::ARBITRARY_MULTIPLE_ZIGZAG_VARINT{ + .multiplier = static_cast(multiplier.to_integer())}; +} + +} // namespace sourcemeta::jsonbinpack::v1 + +#endif diff --git a/vendor/jsonbinpack/src/runtime/loader_v1_number.h b/vendor/jsonbinpack/src/runtime/loader_v1_number.h new file mode 100644 index 000000000..5de9180b2 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/loader_v1_number.h @@ -0,0 +1,16 @@ +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_LOADER_V1_NUMBER_H_ +#define SOURCEMETA_JSONBINPACK_RUNTIME_LOADER_V1_NUMBER_H_ + +#include + +#include + +namespace sourcemeta::jsonbinpack::v1 { + +auto DOUBLE_VARINT_TUPLE(const sourcemeta::core::JSON &) -> Encoding { + return sourcemeta::jsonbinpack::DOUBLE_VARINT_TUPLE{}; +} + +} // namespace sourcemeta::jsonbinpack::v1 + +#endif diff --git a/vendor/jsonbinpack/src/runtime/loader_v1_string.h b/vendor/jsonbinpack/src/runtime/loader_v1_string.h new file mode 100644 index 000000000..3b6340bd8 --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/loader_v1_string.h @@ -0,0 +1,68 @@ +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_LOADER_V1_STRING_H_ +#define SOURCEMETA_JSONBINPACK_RUNTIME_LOADER_V1_STRING_H_ + +#include + +#include + +#include // assert +#include // std::uint64_t + +namespace sourcemeta::jsonbinpack::v1 { + +auto UTF8_STRING_NO_LENGTH(const sourcemeta::core::JSON &options) -> Encoding { + assert(options.defines("size")); + const auto &size{options.at("size")}; + assert(size.is_integer()); + assert(size.is_positive()); + return sourcemeta::jsonbinpack::UTF8_STRING_NO_LENGTH{ + .size = static_cast(size.to_integer())}; +} + +auto FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED( + const sourcemeta::core::JSON &options) -> Encoding { + assert(options.defines("minimum")); + const auto &minimum{options.at("minimum")}; + assert(minimum.is_integer()); + assert(minimum.is_positive()); + return sourcemeta::jsonbinpack::FLOOR_VARINT_PREFIX_UTF8_STRING_SHARED{ + .minimum = static_cast(minimum.to_integer())}; +} + +auto ROOF_VARINT_PREFIX_UTF8_STRING_SHARED( + const sourcemeta::core::JSON &options) -> Encoding { + assert(options.defines("maximum")); + const auto &maximum{options.at("maximum")}; + assert(maximum.is_integer()); + assert(maximum.is_positive()); + return sourcemeta::jsonbinpack::ROOF_VARINT_PREFIX_UTF8_STRING_SHARED{ + .maximum = static_cast(maximum.to_integer())}; +} + +auto BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED( + const sourcemeta::core::JSON &options) -> Encoding { + assert(options.defines("minimum")); + assert(options.defines("maximum")); + const auto &minimum{options.at("minimum")}; + const auto &maximum{options.at("maximum")}; + assert(minimum.is_integer()); + assert(maximum.is_integer()); + assert(minimum.is_positive()); + assert(maximum.is_positive()); + return sourcemeta::jsonbinpack::BOUNDED_8BIT_PREFIX_UTF8_STRING_SHARED{ + .minimum = static_cast(minimum.to_integer()), + .maximum = static_cast(maximum.to_integer())}; +} + +auto RFC3339_DATE_INTEGER_TRIPLET(const sourcemeta::core::JSON &) -> Encoding { + return sourcemeta::jsonbinpack::RFC3339_DATE_INTEGER_TRIPLET{}; +} + +auto PREFIX_VARINT_LENGTH_STRING_SHARED(const sourcemeta::core::JSON &) + -> Encoding { + return sourcemeta::jsonbinpack::PREFIX_VARINT_LENGTH_STRING_SHARED{}; +} + +} // namespace sourcemeta::jsonbinpack::v1 + +#endif diff --git a/vendor/jsonbinpack/src/runtime/output_stream.cc b/vendor/jsonbinpack/src/runtime/output_stream.cc new file mode 100644 index 000000000..b95789b9f --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/output_stream.cc @@ -0,0 +1,42 @@ +#include + +#include + +#include // assert +#include // std::uint8_t, std::uint64_t, std::int64_t + +namespace sourcemeta::jsonbinpack { + +OutputStream::OutputStream(Stream &output) + : sourcemeta::core::BinaryWriter{output} {} + +auto OutputStream::put_varint(const std::uint64_t value) -> void { + constexpr std::uint8_t LEAST_SIGNIFICANT_BITS{0b01111111}; + constexpr std::uint8_t MOST_SIGNIFICANT_BIT{0b10000000}; + constexpr std::uint8_t SHIFT{7}; + std::uint64_t accumulator = value; + + while (accumulator > LEAST_SIGNIFICANT_BITS) { + this->put_byte(static_cast( + (accumulator & LEAST_SIGNIFICANT_BITS) | MOST_SIGNIFICANT_BIT)); + accumulator >>= SHIFT; + } + + this->put_byte(static_cast(accumulator)); +} + +auto OutputStream::put_varint_zigzag(const std::int64_t value) -> void { + this->put_varint(sourcemeta::core::zigzag_encode(value)); +} + +auto OutputStream::put_string_utf8(const sourcemeta::core::JSON::String &string, + const std::uint64_t length) -> void { + assert(string.size() == length); + // Do a manual for-loop based on the provided length instead of a range + // loop based on the string value to avoid accidental overflows + for (std::uint64_t index = 0; index < length; index++) { + this->put_byte(static_cast(string[index])); + } +} + +} // namespace sourcemeta::jsonbinpack diff --git a/vendor/jsonbinpack/src/runtime/unreachable.h b/vendor/jsonbinpack/src/runtime/unreachable.h new file mode 100644 index 000000000..21f95159e --- /dev/null +++ b/vendor/jsonbinpack/src/runtime/unreachable.h @@ -0,0 +1,17 @@ +#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_UNREACHABLE_H_ +#define SOURCEMETA_JSONBINPACK_RUNTIME_UNREACHABLE_H_ + +#include // assert + +// Until we are on C++23 and can use std::unreachable +// See https://en.cppreference.com/w/cpp/utility/unreachable +[[noreturn]] inline void unreachable() { + assert(false); +#if defined(_MSC_VER) && !defined(__clang__) + __assume(false); +#else + __builtin_unreachable(); +#endif +} + +#endif diff --git a/vendor/jsonschema/CMakeLists.txt b/vendor/jsonschema/CMakeLists.txt new file mode 100644 index 000000000..acf5a42ee --- /dev/null +++ b/vendor/jsonschema/CMakeLists.txt @@ -0,0 +1,112 @@ +cmake_minimum_required(VERSION 3.16) +file(READ "${CMAKE_CURRENT_LIST_DIR}/VERSION" JSONSCHEMA_VERSION) +string(STRIP "${JSONSCHEMA_VERSION}" JSONSCHEMA_VERSION) +project(jsonschema VERSION "${JSONSCHEMA_VERSION}" LANGUAGES C CXX) +if(APPLE) + enable_language(OBJCXX) +endif() +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") + +# Options +option(JSONSCHEMA_TESTS "Build the JSON Schema CLI tests" OFF) +option(JSONSCHEMA_TESTS_CI "Build the JSON Schema CLI CI tests" OFF) +option(JSONSCHEMA_DEVELOPMENT "Build the JSON Schema CLI in development mode" OFF) +option(JSONSCHEMA_CONTINUOUS "Perform a continuous JSON Schema CLI release" ON) +option(JSONSCHEMA_PORTABLE "Build a portable JSON Schema CLI binary to increase platform support" OFF) +option(JSONSCHEMA_USE_SYSTEM_CURL "Use the system cURL library" OFF) + +find_package(Core REQUIRED) +if(NOT JSONSCHEMA_PORTABLE AND NOT MSVC) + message(STATUS "Building an optimised but less portable binary (JSONSCHEMA_PORTABLE)") + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -march=native") + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -mtune=native") +endif() +# We can still enable SIMD for Apple targets, as Apple controls the hardware +if(NOT JSONSCHEMA_PORTABLE OR APPLE) + sourcemeta_enable_simd() +endif() + +find_package(Blaze REQUIRED) +find_package(JSONBinPack REQUIRED) + +add_subdirectory(src) + +if(JSONSCHEMA_DEVELOPMENT) + sourcemeta_target_clang_format(SOURCES + src/*.h src/*.cc src/*.mm) + sourcemeta_target_shellcheck(SOURCES + test/*.sh install *.sh completion/*) +endif() + +# Testing +if(JSONSCHEMA_TESTS) + enable_testing() + add_subdirectory(test) +endif() + +if(PROJECT_IS_TOP_LEVEL) + # As a sanity check + if(EXISTS "${PROJECT_SOURCE_DIR}/action.yml") + file(READ "${PROJECT_SOURCE_DIR}/action.yml" ACTION_YML) + string(FIND "${ACTION_YML}" "${PROJECT_VERSION}" ACTION_YML_HAS_VERSION) + if(${ACTION_YML_HAS_VERSION} EQUAL -1) + message(FATAL_ERROR + "The GitHub Action definition must set the correct version: ${PROJECT_VERSION}") + endif() + endif() + + # Packaging + find_program(GIT_BIN NAMES git) + if(GIT_BIN AND JSONSCHEMA_CONTINUOUS) + execute_process(COMMAND "${GIT_BIN}" rev-parse --git-dir + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE + OUTPUT_VARIABLE GIT_DIR) + endif() + if(GIT_BIN AND EXISTS "${GIT_DIR}") + execute_process( + COMMAND "${GIT_BIN}" rev-parse --short=8 HEAD + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + OUTPUT_VARIABLE PROJECT_GIT_SHA + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY) + else() + if(NOT GIT_BIN) + message(STATUS "Could not determine current Git SHA: Git not available") + elseif(NOT EXISTS "${GIT_DIR}") + message(STATUS "Could not determine current Git SHA: Git directory does not exist (${GIT_DIR})") + endif() + + set(PROJECT_GIT_SHA "unknown") + endif() + + set(CPACK_GENERATOR ZIP) + string(TOLOWER ${CMAKE_SYSTEM_NAME} LOWER_SYSTEM_NAME) + string(TOLOWER ${CMAKE_SYSTEM_PROCESSOR} LOWER_SYSTEM_PROCESSOR) + if(LOWER_SYSTEM_PROCESSOR STREQUAL "amd64") + set(LOWER_SYSTEM_PROCESSOR "x86_64") + endif() + if(LOWER_SYSTEM_PROCESSOR STREQUAL "aarch64") + set(LOWER_SYSTEM_PROCESSOR "arm64") + endif() + set(PLATFORM_SUFFIX "${LOWER_SYSTEM_NAME}-${LOWER_SYSTEM_PROCESSOR}") + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND NOT CMAKE_CROSSCOMPILING) + find_program(LDD_BIN NAMES ldd) + if(LDD_BIN) + execute_process(COMMAND "${LDD_BIN}" --version + OUTPUT_VARIABLE LDD_OUTPUT + ERROR_VARIABLE LDD_OUTPUT) + if(LDD_OUTPUT MATCHES "musl") + set(PLATFORM_SUFFIX "${PLATFORM_SUFFIX}-musl") + endif() + endif() + endif() + if(JSONSCHEMA_CONTINUOUS) + set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-${PROJECT_GIT_SHA}-${PLATFORM_SUFFIX}") + else() + set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-${PLATFORM_SUFFIX}") + endif() + set(CPACK_VERBATIM_VARIABLES YES) + include(CPack) +endif() diff --git a/vendor/jsonschema/DEPENDENCIES b/vendor/jsonschema/DEPENDENCIES new file mode 100644 index 000000000..3a23e0298 --- /dev/null +++ b/vendor/jsonschema/DEPENDENCIES @@ -0,0 +1,5 @@ +vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 +core https://github.com/sourcemeta/core bb1c78e8fa148a2ece951bb776798a43fe328821 +jsonbinpack https://github.com/sourcemeta/jsonbinpack e2f99ed5e69ab17b027c3d7bb0ef95b27953bb08 +blaze https://github.com/sourcemeta/blaze 04832d45bf4327d4ec874fa67f339797cd49b375 +ctrf https://github.com/ctrf-io/ctrf 93ea827d951390190171d37443bff169cf47c808 diff --git a/vendor/jsonschema/LICENSE b/vendor/jsonschema/LICENSE new file mode 100644 index 000000000..d79ff8187 --- /dev/null +++ b/vendor/jsonschema/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + JSON Schema CLI + Copyright (C) 2022 Juan Cruz Viotti + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/vendor/jsonschema/VERSION b/vendor/jsonschema/VERSION new file mode 100644 index 000000000..946789e61 --- /dev/null +++ b/vendor/jsonschema/VERSION @@ -0,0 +1 @@ +16.0.0 diff --git a/vendor/jsonschema/completion/jsonschema.bash b/vendor/jsonschema/completion/jsonschema.bash new file mode 100644 index 000000000..25a8fbe99 --- /dev/null +++ b/vendor/jsonschema/completion/jsonschema.bash @@ -0,0 +1,214 @@ +# shellcheck disable=SC2207 +_jsonschema() { + local current previous commands global_options + COMPREPLY=() + current="${COMP_WORDS[COMP_CWORD]}" + + if [ "${COMP_CWORD}" -gt 0 ] + then + previous="${COMP_WORDS[COMP_CWORD-1]}" + else + previous="" + fi + + commands="validate metaschema compile test fmt lint bundle inspect encode decode codegen install upgrade version help" + + global_options="--verbose -v --resolve -r --default-dialect -d --json -j --http -h --debug -g --header -H" + + if [ "${COMP_CWORD}" -eq 1 ] + then + COMPREPLY=( $(compgen -W "${commands}" -- "${current}") ) + return 0 + fi + + if [ "${#COMP_WORDS[@]}" -lt 2 ] + then + return 0 + fi + + local command="${COMP_WORDS[1]}" + + case "${previous}" in + --extension|-e) + COMPREPLY=( $(compgen -W ".json .yaml .yml" -- "${current}") ) + return 0 + ;; + --resolve|-r|--ignore|-i) + COMPREPLY=( $(compgen -f -d -- "${current}") ) + return 0 + ;; + --default-dialect|-d) + COMPREPLY=( $(compgen -W "https://json-schema.org/draft/2020-12/schema https://json-schema.org/draft/2019-09/schema https://json-schema.org/draft-07/schema https://json-schema.org/draft-06/schema https://json-schema.org/draft-04/schema https://json-schema.org/draft-03/schema" -- "${current}") ) + return 0 + ;; + --header|-H) + return 0 + ;; + --indentation) + COMPREPLY=( $(compgen -W "2 4 8" -- "${current}") ) + return 0 + ;; + -n) + if [ "${command}" = "fmt" ] || [ "${command}" = "lint" ] + then + COMPREPLY=( $(compgen -W "2 4 8" -- "${current}") ) + fi + return 0 + ;; + --loop|-l) + return 0 + ;; + --exclude|-x|--only|-o) + return 0 + ;; + --template|-m) + COMPREPLY=( $(compgen -f -X '!*.json' -- "${current}") ) + return 0 + ;; + --target) + COMPREPLY=( $(compgen -W "typescript" -- "${current}") ) + return 0 + ;; + --to) + COMPREPLY=( $(compgen -W "draft4 draft6 draft7 2019-09 2020-12" -- "${current}") ) + return 0 + ;; + -t) + if [ "${command}" = "upgrade" ] + then + COMPREPLY=( $(compgen -W "draft4 draft6 draft7 2019-09 2020-12" -- "${current}") ) + else + COMPREPLY=( $(compgen -W "typescript" -- "${current}") ) + fi + return 0 + ;; + esac + + case "${command}" in + validate) + local options="--benchmark -b --loop -l --extension -e --ignore -i --trace -t --fast -f --template -m" + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${options} ${global_options}" -- "${current}") ) + else + COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.yaml' -X '!*.yml' -X '!*.jsonl' -- "${current}") ) + fi + ;; + metaschema) + local options="--extension -e --ignore -i --trace -t" + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${options} ${global_options}" -- "${current}") ) + else + COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.yaml' -X '!*.yml' -- "${current}") ) + fi + ;; + compile) + local options="--extension -e --ignore -i --fast -f --minify -m" + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${options} ${global_options}" -- "${current}") ) + else + COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.yaml' -X '!*.yml' -- "${current}") ) + fi + ;; + test) + local options="--extension -e --ignore -i" + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${options} ${global_options}" -- "${current}") ) + else + COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.yaml' -X '!*.yml' -- "${current}") ) + fi + ;; + fmt) + local options="--check -c --extension -e --ignore -i --keep-ordering -k --indentation -n" + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${options} ${global_options}" -- "${current}") ) + else + COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.yaml' -X '!*.yml' -- "${current}") ) + fi + ;; + lint) + local options="--fix -f --extension -e --ignore -i --exclude -x --only -o --list -l --indentation -n" + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${options} ${global_options}" -- "${current}") ) + else + COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.yaml' -X '!*.yml' -- "${current}") ) + fi + ;; + bundle) + local options="--extension -e --ignore -i --without-id -w" + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${options} ${global_options}" -- "${current}") ) + else + COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.yaml' -X '!*.yml' -- "${current}") ) + fi + ;; + inspect) + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${global_options}" -- "${current}") ) + else + COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.yaml' -X '!*.yml' -- "${current}") ) + fi + ;; + encode) + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${global_options}" -- "${current}") ) + else + COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.jsonl' -- "${current}") ) + fi + ;; + decode) + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${global_options}" -- "${current}") ) + else + COMPREPLY=( $(compgen -f -X '!*.binpack' -- "${current}") ) + fi + ;; + codegen) + local options="--name -n --target -t" + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${options} ${global_options}" -- "${current}") ) + else + COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.yaml' -X '!*.yml' -- "${current}") ) + fi + ;; + install) + local options="--force -f --frozen -z" + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${options} ${global_options}" -- "${current}") ) + else + COMPREPLY=( $(compgen -f -d -- "${current}") ) + fi + ;; + upgrade) + local options="--to -t" + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${options} ${global_options}" -- "${current}") ) + else + COMPREPLY=( $(compgen -f -X '!*.json' -X '!*.yaml' -X '!*.yml' -- "${current}") ) + fi + ;; + version|help) + COMPREPLY=() + ;; + *) + if [[ ${current} == -* ]] + then + COMPREPLY=( $(compgen -W "${global_options}" -- "${current}") ) + fi + ;; + esac +} + +complete -F _jsonschema jsonschema diff --git a/vendor/jsonschema/completion/jsonschema.zsh b/vendor/jsonschema/completion/jsonschema.zsh new file mode 100644 index 000000000..9b41d0f27 --- /dev/null +++ b/vendor/jsonschema/completion/jsonschema.zsh @@ -0,0 +1,181 @@ +#compdef jsonschema +# shellcheck shell=bash disable=SC2034,SC2154,SC1087,SC2125,SC2068 + +_jsonschema() { + local context state state_descr line + typeset -A opt_args + + local -a commands + commands=( + 'validate:Validate instances against a schema' + 'metaschema:Validate schemas against their metaschemas' + 'compile:Compile a schema into an optimised representation' + 'test:Run unit tests against a schema' + 'fmt:Format schemas in-place or check formatting' + 'lint:Lint schemas and optionally fix issues' + 'bundle:Inline remote references in a schema' + 'inspect:Display schema locations and references' + 'encode:Encode JSON using JSON BinPack' + 'decode:Decode JSON using JSON BinPack' + 'codegen:Generate code from a JSON Schema' + 'install:Fetch and install external schema dependencies' + 'upgrade:Upgrade a schema to a newer JSON Schema dialect' + 'version:Print version information' + 'help:Print help information' + ) + + local -a global_options + global_options=( + '(--verbose -v)'{--verbose,-v}'[Enable verbose output]' + '(--resolve -r)'{--resolve,-r}'[Import schemas into resolution context]:schema file:_files -g "*.json *.yaml *.yml"' + '(--default-dialect -d)'{--default-dialect,-d}'[Specify default dialect URI]:dialect URI:_jsonschema_dialects' + '(--json -j)'{--json,-j}'[Prefer JSON output if supported]' + '(--http -h)'{--http,-h}'[Enable HTTP resolution]' + '(--debug -g)'{--debug,-g}'[Enable debug output]' + '*'{--header,-H}'[Send a custom HTTP header (Name: Value)]:header:' + ) + + _arguments -C \ + '1: :->command' \ + '*:: :->option-or-argument' \ + && return 0 + + case $state in + command) + _describe -t commands 'jsonschema command' commands + ;; + option-or-argument) + local command=$line[1] + case $command in + validate) + _arguments \ + ${global_options[@]} \ + '(--benchmark -b)'{--benchmark,-b}'[Enable benchmarking mode]' \ + '(--loop -l)'{--loop,-l}'[Number of loop iterations]:iterations:' \ + '(--extension -e)'{--extension,-e}'[Specify file extension]:extension:_jsonschema_extensions' \ + '(--ignore -i)'{--ignore,-i}'[Ignore schemas or directories]:path:_files' \ + '(--trace -t)'{--trace,-t}'[Enable trace output]' \ + '(--fast -f)'{--fast,-f}'[Optimise for speed]' \ + '(--template -m)'{--template,-m}'[Use pre-compiled schema template]:template file:_files -g "*.json"' \ + '1:schema file:_files -g "*.json *.yaml *.yml"' \ + '*:instance file:_files -g "*.json *.yaml *.yml *.jsonl"' + ;; + metaschema) + _arguments \ + ${global_options[@]} \ + '(--extension -e)'{--extension,-e}'[Specify file extension]:extension:_jsonschema_extensions' \ + '(--ignore -i)'{--ignore,-i}'[Ignore schemas or directories]:path:_files' \ + '(--trace -t)'{--trace,-t}'[Enable trace output]' \ + '*:schema file:_files -g "*.json *.yaml *.yml"' + ;; + compile) + _arguments \ + ${global_options[@]} \ + '(--extension -e)'{--extension,-e}'[Specify file extension]:extension:_jsonschema_extensions' \ + '(--ignore -i)'{--ignore,-i}'[Ignore schemas or directories]:path:_files' \ + '(--fast -f)'{--fast,-f}'[Optimise for speed]' \ + '(--minify -m)'{--minify,-m}'[Minify output]' \ + '1:schema file:_files -g "*.json *.yaml *.yml"' + ;; + test) + _arguments \ + ${global_options[@]} \ + '(--extension -e)'{--extension,-e}'[Specify file extension]:extension:_jsonschema_extensions' \ + '(--ignore -i)'{--ignore,-i}'[Ignore schemas or directories]:path:_files' \ + '*:schema file:_files -g "*.json *.yaml *.yml"' + ;; + fmt) + _arguments \ + ${global_options[@]} \ + '(--check -c)'{--check,-c}'[Check formatting without modifying]' \ + '(--extension -e)'{--extension,-e}'[Specify file extension]:extension:_jsonschema_extensions' \ + '(--ignore -i)'{--ignore,-i}'[Ignore schemas or directories]:path:_files' \ + '(--keep-ordering -k)'{--keep-ordering,-k}'[Keep original key ordering]' \ + '(--indentation -n)'{--indentation,-n}'[Specify indentation spaces]:spaces:(2 4 8)' \ + '*:schema file:_files -g "*.json *.yaml *.yml"' + ;; + lint) + _arguments \ + ${global_options[@]} \ + '(--fix -f)'{--fix,-f}'[Fix issues automatically]' \ + '(--extension -e)'{--extension,-e}'[Specify file extension]:extension:_jsonschema_extensions' \ + '(--ignore -i)'{--ignore,-i}'[Ignore schemas or directories]:path:_files' \ + '(--exclude -x)'{--exclude,-x}'[Exclude specific rule]:rule name:' \ + '(--only -o)'{--only,-o}'[Only run specific rule]:rule name:' \ + '(--list -l)'{--list,-l}'[List all enabled rules]' \ + '(--indentation -n)'{--indentation,-n}'[Specify indentation spaces]:spaces:(2 4 8)' \ + '*:schema file:_files -g "*.json *.yaml *.yml"' + ;; + bundle) + _arguments \ + ${global_options[@]} \ + '(--extension -e)'{--extension,-e}'[Specify file extension]:extension:_jsonschema_extensions' \ + '(--ignore -i)'{--ignore,-i}'[Ignore schemas or directories]:path:_files' \ + '(--without-id -w)'{--without-id,-w}'[Bundle without ID]' \ + '1:schema file:_files -g "*.json *.yaml *.yml"' + ;; + inspect) + _arguments \ + ${global_options[@]} \ + '1:schema file:_files -g "*.json *.yaml *.yml"' + ;; + encode) + _arguments \ + ${global_options[@]} \ + '1:input file:_files -g "*.json *.jsonl"' \ + '2:output file:_files' + ;; + decode) + _arguments \ + ${global_options[@]} \ + '1:input file:_files -g "*.binpack"' \ + '2:output file:_files -g "*.json *.jsonl"' + ;; + codegen) + _arguments \ + ${global_options[@]} \ + '(--name -n)'{--name,-n}'[Specify type name prefix]:name:' \ + '(--target -t)'{--target,-t}'[Specify target language]:target:(typescript)' \ + '1:schema file:_files -g "*.json *.yaml *.yml"' + ;; + install) + _arguments \ + ${global_options[@]} \ + '(--force -f)'{--force,-f}'[Re-fetch all dependencies]' \ + '(--frozen -z)'{--frozen,-z}'[Strictly verify against the lock file]' \ + '1:schema URI:' \ + '2:destination path:_files' + ;; + upgrade) + _arguments \ + ${global_options[@]} \ + '(--to -t)'{--to,-t}'[Target JSON Schema dialect]:dialect:(draft4 draft6 draft7 2019-09 2020-12)' \ + '1:schema file:_files -g "*.json *.yaml *.yml"' + ;; + version|help) + ;; + esac + ;; + esac +} + +_jsonschema_extensions() { + local -a extensions + extensions=('.json' '.yaml' '.yml') + _describe -t extensions 'file extension' extensions +} + +_jsonschema_dialects() { + local -a dialects + dialects=( + 'https://json-schema.org/draft/2020-12/schema:Draft 2020-12' + 'https://json-schema.org/draft/2019-09/schema:Draft 2019-09' + 'https://json-schema.org/draft-07/schema:Draft 07' + 'https://json-schema.org/draft-06/schema:Draft 06' + 'https://json-schema.org/draft-04/schema:Draft 04' + 'https://json-schema.org/draft-03/schema:Draft 03' + ) + _describe -t dialects 'JSON Schema dialect' dialects +} + +_jsonschema "$@" diff --git a/vendor/jsonschema/src/CMakeLists.txt b/vendor/jsonschema/src/CMakeLists.txt new file mode 100644 index 000000000..de9beb66f --- /dev/null +++ b/vendor/jsonschema/src/CMakeLists.txt @@ -0,0 +1,64 @@ +sourcemeta_executable( + PROJECT jsonschema + NAME cli + SOURCES + main.cc configure.h.in command.h + utils.h error.h exit_code.h logger.h configuration.h input.h resolver.h + command_fmt.cc + command_inspect.cc + command_bundle.cc + command_test.cc + command_lint.cc + command_metaschema.cc + command_validate.cc + command_encode.cc + command_decode.cc + command_compile.cc + command_codegen.cc + command_install.cc + command_upgrade.cc) + +set_target_properties(jsonschema_cli PROPERTIES OUTPUT_NAME jsonschema) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::error) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::io) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::uri) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::json) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::jsonl) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::gzip) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::http) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::jsonpointer) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::yaml) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::regex) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::options) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::foundation) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::frame) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::bundle) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::format) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::editor) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::configuration) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::jsonbinpack::compiler) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::jsonbinpack::runtime) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::compiler) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::evaluator) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::output) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::test) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::alterschema) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::codegen) + +configure_file(configure.h.in configure.h @ONLY) +target_include_directories(jsonschema_cli PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") + +include(GNUInstallDirs) +install(TARGETS jsonschema_cli + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" + COMPONENT sourcemeta_jsonschema) + +install(FILES "${PROJECT_SOURCE_DIR}/completion/jsonschema.bash" + DESTINATION "${CMAKE_INSTALL_DATADIR}/bash-completion/completions" + RENAME jsonschema + COMPONENT sourcemeta_jsonschema) + +install(FILES "${PROJECT_SOURCE_DIR}/completion/jsonschema.zsh" + DESTINATION "${CMAKE_INSTALL_DATADIR}/zsh/site-functions" + RENAME _jsonschema + COMPONENT sourcemeta_jsonschema) diff --git a/vendor/jsonschema/src/command.h b/vendor/jsonschema/src/command.h new file mode 100644 index 000000000..448a7bd42 --- /dev/null +++ b/vendor/jsonschema/src/command.h @@ -0,0 +1,22 @@ +#ifndef SOURCEMETA_JSONSCHEMA_CLI_COMMAND_H_ +#define SOURCEMETA_JSONSCHEMA_CLI_COMMAND_H_ + +#include + +namespace sourcemeta::jsonschema { +auto fmt(const sourcemeta::core::Options &options) -> void; +auto inspect(const sourcemeta::core::Options &options) -> void; +auto bundle(const sourcemeta::core::Options &options) -> void; +auto test(const sourcemeta::core::Options &options) -> void; +auto lint(const sourcemeta::core::Options &options) -> void; +auto validate(const sourcemeta::core::Options &options) -> void; +auto metaschema(const sourcemeta::core::Options &options) -> void; +auto compile(const sourcemeta::core::Options &options) -> void; +auto encode(const sourcemeta::core::Options &options) -> void; +auto decode(const sourcemeta::core::Options &options) -> void; +auto codegen(const sourcemeta::core::Options &options) -> void; +auto install(const sourcemeta::core::Options &options) -> void; +auto upgrade(const sourcemeta::core::Options &options) -> void; +} // namespace sourcemeta::jsonschema + +#endif diff --git a/vendor/jsonschema/src/command_bundle.cc b/vendor/jsonschema/src/command_bundle.cc new file mode 100644 index 000000000..71d75fbae --- /dev/null +++ b/vendor/jsonschema/src/command_bundle.cc @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include +#include +#include + +#include // std::cout + +#include "command.h" +#include "configuration.h" +#include "error.h" +#include "input.h" +#include "logger.h" +#include "resolver.h" +#include "utils.h" + +auto sourcemeta::jsonschema::bundle(const sourcemeta::core::Options &options) + -> void { + if (options.positional().size() < 1) { + throw PositionalArgumentError{"This command expects a path to a schema", + "jsonschema bundle path/to/schema.json"}; + } + + validate_http_headers(options); + + const std::filesystem::path schema_path{options.positional().front()}; + const bool schema_from_stdin = (schema_path == "-"); + + if (!schema_from_stdin && std::filesystem::is_directory(schema_path)) { + throw sourcemeta::core::IOIsADirectoryError{schema_path}; + } + + const auto schema_resolution_base{ + schema_from_stdin ? std::filesystem::current_path() : schema_path}; + const auto schema_display_path{schema_from_stdin ? stdin_path() + : schema_path}; + + const auto configuration_path{find_configuration(schema_resolution_base)}; + const auto &configuration{ + read_configuration(options, configuration_path, schema_resolution_base)}; + const auto dialect{default_dialect(options, configuration)}; + auto parsed_schema{schema_from_stdin ? read_from_stdin() + : read_file(schema_path)}; + + if (!sourcemeta::blaze::is_schema(parsed_schema.document)) { + throw NotSchemaError{schema_display_path}; + } + + auto &schema{parsed_schema.document}; + + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + + try { + sourcemeta::blaze::bundle( + schema, sourcemeta::blaze::schema_walker, custom_resolver, + sourcemeta::blaze::BundleMode::NonOfficialMetaschemas, dialect, + sourcemeta::jsonschema::default_id(schema_resolution_base)); + + if (options.contains("without-id")) { + sourcemeta::jsonschema::LOG_WARNING() + << "You are opting in to remove schema identifiers in " + "the bundled schema.\n" + << "The only legit use case of this advanced feature we know of " + "is to workaround\n" + << "non-compliant JSON Schema implementations such as Visual " + "Studio Code.\n" + << "Otherwise, this is not needed and may harm other use " + "cases. For example,\n" + << "you will be unable to reference the resulting schema from " + "other schemas\n" + << "using the --resolve/-r option.\n"; + + sourcemeta::blaze::for_editor(schema, sourcemeta::blaze::schema_walker, + custom_resolver, dialect); + } + + sourcemeta::blaze::format(schema, sourcemeta::blaze::schema_walker, + custom_resolver, dialect); + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError( + schema_display_path, error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError( + schema_display_path, error); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{parsed_schema.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_display_path, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>(schema_display_path, + error); + } catch (const sourcemeta::blaze::SchemaReferenceError &error) { + throw sourcemeta::core::FileError( + schema_display_path, error.identifier(), error.location(), + error.what()); + } catch ( + const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( + schema_display_path, error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError( + schema_display_path, error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>(schema_display_path); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>(schema_display_path); + } catch (const sourcemeta::blaze::SchemaError &error) { + throw sourcemeta::core::FileError( + schema_display_path, error.what()); + } + + sourcemeta::core::prettify(schema, std::cout); + std::cout << "\n"; +} diff --git a/vendor/jsonschema/src/command_codegen.cc b/vendor/jsonschema/src/command_codegen.cc new file mode 100644 index 000000000..ae5194419 --- /dev/null +++ b/vendor/jsonschema/src/command_codegen.cc @@ -0,0 +1,122 @@ +#include +#include +#include +#include +#include +#include + +#include // std::cout +#include // std::ostringstream +#include // std::runtime_error +#include // std::string + +#include "command.h" +#include "configuration.h" +#include "error.h" +#include "resolver.h" +#include "utils.h" + +auto sourcemeta::jsonschema::codegen(const sourcemeta::core::Options &options) + -> void { + if (options.positional().size() < 1) { + throw PositionalArgumentError{"This command expects a path to a schema", + "jsonschema codegen path/to/schema.json " + "--name MyType --target typescript"}; + } + + validate_http_headers(options); + + if (!options.contains("target")) { + throw OptionConflictError{ + "You must pass a target using the `--target/-t` option"}; + } + + const auto &target{options.at("target").front()}; + if (target != "typescript") { + throw InvalidOptionEnumerationValueError{ + "Unknown code generation target", "target", {"typescript"}}; + } + + const std::filesystem::path schema_path{options.positional().front()}; + auto parsed_schema{read_file(schema_path)}; + const auto &schema{parsed_schema.document}; + + const auto configuration_path{find_configuration(schema_path)}; + const auto &configuration{ + read_configuration(options, configuration_path, schema_path)}; + const auto dialect{default_dialect(options, configuration)}; + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + + sourcemeta::blaze::CodegenIRResult result; + try { + result = sourcemeta::blaze::compile( + schema, sourcemeta::blaze::schema_walker, custom_resolver, + sourcemeta::blaze::default_compiler, dialect, + sourcemeta::jsonschema::default_id(schema_path)); + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError( + schema_path, error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError( + schema_path, error); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{parsed_schema.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_path, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>(schema_path, error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError( + schema_path, error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>(schema_path); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>(schema_path); + } catch (const sourcemeta::blaze::SchemaError &error) { + throw sourcemeta::core::FileError( + schema_path, error.what()); + } catch (const sourcemeta::blaze::SchemaVocabularyError &error) { + throw sourcemeta::core::FileError( + schema_path, error.uri(), error.what()); + } catch (const sourcemeta::blaze::CodegenUnsupportedKeywordError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CodegenUnsupportedKeywordError>( + schema_path, error.json(), error.pointer(), + std::string{error.keyword()}, error.what()); + } catch ( + const sourcemeta::blaze::CodegenUnsupportedKeywordValueError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CodegenUnsupportedKeywordValueError>( + schema_path, error.json(), error.pointer(), + std::string{error.keyword()}, error.what()); + } catch (const sourcemeta::blaze::CodegenUnexpectedSchemaError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CodegenUnexpectedSchemaError>( + schema_path, error.json(), error.pointer(), error.what()); + } + + std::ostringstream output; + if (options.contains("name")) { + sourcemeta::blaze::generate( + output, result, options.at("name").front()); + } else { + sourcemeta::blaze::generate(output, result); + } + + if (options.contains("json")) { + auto json_output{sourcemeta::core::JSON::make_object()}; + json_output.assign("code", sourcemeta::core::JSON{output.str()}); + sourcemeta::core::prettify(json_output, std::cout); + std::cout << "\n"; + } else { + std::cout << output.str(); + } +} diff --git a/vendor/jsonschema/src/command_compile.cc b/vendor/jsonschema/src/command_compile.cc new file mode 100644 index 000000000..f0af0f931 --- /dev/null +++ b/vendor/jsonschema/src/command_compile.cc @@ -0,0 +1,210 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include // std::transform +#include // std::toupper +#include // std::hex, std::setw, std::setfill +#include // std::cerr, std::cout +#include // std::ostringstream + +#include "command.h" +#include "configuration.h" +#include "error.h" +#include "resolver.h" +#include "utils.h" + +auto sourcemeta::jsonschema::compile(const sourcemeta::core::Options &options) + -> void { + if (options.positional().size() < 1) { + throw PositionalArgumentError{"This command expects a path to a schema", + "jsonschema compile path/to/schema.json"}; + } + + validate_http_headers(options); + + const auto &schema_path{options.positional().at(0)}; + const auto configuration_path{find_configuration(schema_path)}; + const auto &configuration{ + read_configuration(options, configuration_path, schema_path)}; + const auto dialect{default_dialect(options, configuration)}; + + auto parsed_schema{read_file(schema_path)}; + + if (!sourcemeta::blaze::is_schema(parsed_schema.document)) { + throw NotSchemaError{schema_path}; + } + + const auto &schema{parsed_schema.document}; + + const auto fast_mode{options.contains("fast")}; + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + const auto schema_default_id{sourcemeta::jsonschema::default_id(schema_path)}; + + sourcemeta::blaze::Template schema_template; + try { + if (options.contains("entrypoint") && !options.at("entrypoint").empty()) { + const sourcemeta::core::JSON bundled{sourcemeta::blaze::bundle( + schema, sourcemeta::blaze::schema_walker, custom_resolver, + sourcemeta::blaze::BundleMode::References, dialect, + schema_default_id)}; + + sourcemeta::blaze::SchemaFrame frame{ + sourcemeta::blaze::SchemaFrame::Mode::References}; + frame.analyse(bundled, sourcemeta::blaze::schema_walker, custom_resolver, + dialect, schema_default_id); + + std::string entrypoint_uri; + try { + entrypoint_uri = + resolve_entrypoint(frame, options.at("entrypoint").front()); + } catch (const sourcemeta::blaze::CompilerInvalidEntryPoint &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerInvalidEntryPoint>(schema_path, error); + } + + schema_template = sourcemeta::blaze::compile( + bundled, sourcemeta::blaze::schema_walker, custom_resolver, + sourcemeta::blaze::default_schema_compiler, frame, entrypoint_uri, + fast_mode ? sourcemeta::blaze::Mode::FastValidation + : sourcemeta::blaze::Mode::Exhaustive, + sourcemeta::jsonschema::format_assertion_tweaks(options)); + } else { + schema_template = sourcemeta::blaze::compile( + schema, sourcemeta::blaze::schema_walker, custom_resolver, + sourcemeta::blaze::default_schema_compiler, + fast_mode ? sourcemeta::blaze::Mode::FastValidation + : sourcemeta::blaze::Mode::Exhaustive, + dialect, schema_default_id, "", + sourcemeta::jsonschema::format_assertion_tweaks(options)); + } + } catch (const sourcemeta::blaze::CompilerInvalidEntryPoint &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerInvalidEntryPoint>(schema_path, error); + } catch (const sourcemeta::blaze::CompilerInvalidRegexError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerInvalidRegexError>(schema_path, error); + } catch ( + const sourcemeta::blaze::CompilerReferenceTargetNotSchemaError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerReferenceTargetNotSchemaError>(schema_path, + error); + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError( + schema_path, error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError( + schema_path, error); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{parsed_schema.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_path, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>(schema_path, error); + } catch ( + const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>(schema_path, + error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError( + schema_path, error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>(schema_path); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>(schema_path); + } catch (const sourcemeta::blaze::SchemaVocabularyError &error) { + throw sourcemeta::core::FileError( + schema_path, error.uri(), error.what()); + } catch (const sourcemeta::blaze::SchemaError &error) { + throw sourcemeta::core::FileError( + schema_path, error.what()); + } + + const auto template_json{sourcemeta::blaze::to_json(schema_template)}; + + if (options.contains("include") && !options.at("include").empty()) { + std::string name{options.at("include").front()}; + + static const auto IDENTIFIER_PATTERN{ + sourcemeta::core::to_regex("^[A-Za-z_][A-Za-z0-9_]*$")}; + if (!IDENTIFIER_PATTERN.has_value() || + !sourcemeta::core::matches(IDENTIFIER_PATTERN.value(), name)) { + throw InvalidIncludeIdentifier{name}; + } + + std::transform(name.begin(), name.end(), name.begin(), + [](unsigned char character) -> unsigned char { + return static_cast(std::toupper(character)); + }); + + std::ostringstream json_stream; + sourcemeta::core::stringify(template_json, json_stream); + const auto json_data{std::move(json_stream).str()}; + + constexpr auto BYTES_PER_LINE{12}; + + std::cout << "#ifndef SOURCEMETA_JSONSCHEMA_INCLUDE_" << name << "_H_\n"; + std::cout << "#define SOURCEMETA_JSONSCHEMA_INCLUDE_" << name << "_H_\n"; + std::cout << "\n"; + std::cout << "#ifdef __cplusplus\n"; + std::cout << "#include \n"; + std::cout << "#include \n"; + std::cout << "#endif\n"; + std::cout << "\n"; + std::cout << "static const char " << name << "_DATA[] = {"; + + for (std::size_t index = 0; index < json_data.size(); ++index) { + if (index % BYTES_PER_LINE == 0) { + std::cout << "\n "; + } + + std::cout << "0x" << std::hex << std::setw(2) << std::setfill('0') + << (static_cast( + static_cast(json_data[index]))); + + std::cout << ","; + if ((index + 1) % BYTES_PER_LINE != 0) { + std::cout << " "; + } + } + + if (json_data.size() % BYTES_PER_LINE != 0) { + std::cout << "0x00"; + } else { + std::cout << "\n 0x00"; + } + + std::cout << "\n};\n"; + std::cout << "\n"; + std::cout << std::dec; + std::cout << "static const unsigned int " << name + << "_LENGTH = " << json_data.size() << ";\n"; + std::cout << "\n"; + std::cout << "#ifdef __cplusplus\n"; + std::cout << "static constexpr std::string_view " << name << "{" << name + << "_DATA, " << name << "_LENGTH};\n"; + std::cout << "#endif\n"; + std::cout << "\n"; + std::cout << "#endif\n"; + } else if (options.contains("minify")) { + sourcemeta::core::stringify(template_json, std::cout); + std::cout << "\n"; + } else { + sourcemeta::core::prettify(template_json, std::cout); + std::cout << "\n"; + } +} diff --git a/vendor/jsonschema/src/command_decode.cc b/vendor/jsonschema/src/command_decode.cc new file mode 100644 index 000000000..7e1e01d75 --- /dev/null +++ b/vendor/jsonschema/src/command_decode.cc @@ -0,0 +1,96 @@ +#include +#include +#include + +#include +#include + +#include // assert +#include // std::filesystem +#include // std::ifstream + +#include "command.h" +#include "configuration.h" +#include "error.h" +#include "logger.h" +#include "resolver.h" +#include "utils.h" + +static auto has_data(std::ifstream &stream) -> bool { + if (!stream.is_open()) { + return false; + } + + std::streampos current_pos = stream.tellg(); + stream.seekg(0, std::ios::end); + std::streampos end_pos = stream.tellg(); + stream.seekg(current_pos); + + return (current_pos < end_pos) && stream.good(); +} + +auto sourcemeta::jsonschema::decode(const sourcemeta::core::Options &options) + -> void { + if (options.positional().size() < 2) { + throw PositionalArgumentError{ + "This command expects a path to a binary file and an output path", + "jsonschema decode path/to/output.binpack path/to/document.json"}; + } + + validate_http_headers(options); + + // TODO: Take a real schema as argument + auto schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema" + })JSON")}; + + const auto configuration_path{ + find_configuration(options.positional().front())}; + const auto &configuration{read_configuration(options, configuration_path)}; + const auto dialect{default_dialect(options, configuration)}; + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + + sourcemeta::jsonbinpack::compile(schema, sourcemeta::blaze::schema_walker, + custom_resolver); + const auto encoding{sourcemeta::jsonbinpack::load(schema)}; + + std::ifstream input_stream{ + sourcemeta::core::weakly_canonical(options.positional().front()), + std::ios::binary}; + assert(!input_stream.fail()); + assert(input_stream.is_open()); + + const std::filesystem::path output{options.positional().at(1)}; + std::ofstream output_stream(sourcemeta::core::weakly_canonical(output), + std::ios::binary); + output_stream.exceptions(std::ios_base::badbit); + sourcemeta::jsonbinpack::Decoder decoder{input_stream}; + + if (output.extension() == ".jsonl") { + LOG_VERBOSE(options) << "Interpreting input as JSONL: " + << sourcemeta::core::weakly_canonical( + options.positional().front()) + .string() + << "\n"; + + std::size_t count{0}; + while (has_data(input_stream)) { + LOG_VERBOSE(options) << "Decoding entry #" << count << "\n"; + auto document{decoder.read(encoding)}; + if (count > 0) { + output_stream << "\n"; + } + + sourcemeta::core::prettify(document, output_stream); + count += 1; + } + } else { + auto document{decoder.read(encoding)}; + sourcemeta::core::prettify(document, output_stream); + } + + output_stream << "\n"; + output_stream.flush(); + output_stream.close(); +} diff --git a/vendor/jsonschema/src/command_encode.cc b/vendor/jsonschema/src/command_encode.cc new file mode 100644 index 000000000..0bf742d1c --- /dev/null +++ b/vendor/jsonschema/src/command_encode.cc @@ -0,0 +1,92 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include // std::filesystem +#include // std::ofstream +#include // std::println + +#include "command.h" +#include "configuration.h" +#include "error.h" +#include "input.h" +#include "logger.h" +#include "resolver.h" +#include "utils.h" + +auto sourcemeta::jsonschema::encode(const sourcemeta::core::Options &options) + -> void { + if (options.positional().size() < 2) { + throw PositionalArgumentError{ + "This command expects a path to a JSON document and an output path", + "jsonschema encode path/to/document.json path/to/output.binpack"}; + } + + validate_http_headers(options); + + // TODO: Take a real schema as argument + auto schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema" + })JSON")}; + + const auto configuration_path{ + find_configuration(options.positional().front())}; + const auto &configuration{read_configuration(options, configuration_path)}; + const auto dialect{default_dialect(options, configuration)}; + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + + sourcemeta::jsonbinpack::compile(schema, sourcemeta::blaze::schema_walker, + custom_resolver); + const auto encoding{sourcemeta::jsonbinpack::load(schema)}; + + const std::filesystem::path document{options.positional().front()}; + const auto original_size{std::filesystem::file_size(document)}; + std::println(stderr, "original file size: {} bytes", original_size); + + if (document.extension() == ".jsonl") { + LOG_VERBOSE(options) + << "Interpreting input as JSONL: " + << sourcemeta::core::weakly_canonical(document).string() << "\n"; + + auto stream{sourcemeta::core::read_file(document)}; + std::ofstream output_stream( + sourcemeta::core::weakly_canonical(options.positional().at(1)), + std::ios::binary); + output_stream.exceptions(std::ios_base::badbit); + sourcemeta::jsonbinpack::Encoder encoder{output_stream}; + std::size_t count{0}; + for (const auto &entry : sourcemeta::core::JSONL{stream}) { + LOG_VERBOSE(options) << "Encoding entry #" << count << "\n"; + encoder.write(entry, encoding); + count += 1; + } + + output_stream.flush(); + const auto total_size{static_cast(output_stream.tellp())}; + output_stream.close(); + std::println(stderr, "encoded file size: {} bytes", total_size); + std::println(stderr, "compression ratio: {}%", + total_size * 100 / original_size); + } else { + const auto entry{ + sourcemeta::core::read_yaml_or_json(options.positional().front())}; + std::ofstream output_stream( + sourcemeta::core::weakly_canonical(options.positional().at(1)), + std::ios::binary); + output_stream.exceptions(std::ios_base::badbit); + sourcemeta::jsonbinpack::Encoder encoder{output_stream}; + encoder.write(entry, encoding); + output_stream.flush(); + const auto total_size{static_cast(output_stream.tellp())}; + output_stream.close(); + std::println(stderr, "encoded file size: {} bytes", total_size); + std::println(stderr, "compression ratio: {}%", + total_size * 100 / original_size); + } +} diff --git a/vendor/jsonschema/src/command_fmt.cc b/vendor/jsonschema/src/command_fmt.cc new file mode 100644 index 000000000..7652774d6 --- /dev/null +++ b/vendor/jsonschema/src/command_fmt.cc @@ -0,0 +1,264 @@ +#include +#include +#include +#include + +#include // std::cerr, std::cout +#include // std::ostringstream +#include // std::move +#include // std::vector + +#include "command.h" +#include "error.h" +#include "input.h" +#include "logger.h" +#include "resolver.h" +#include "utils.h" + +auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) + -> void { + validate_http_headers(options); + const bool output_json{options.contains("json")}; + bool result{true}; + std::vector failed_files; + const auto indentation{parse_indentation(options)}; + + const auto handle_stdin = [&]() { + const auto current_path{std::filesystem::current_path()}; + const auto configuration_path{find_configuration(current_path)}; + const auto &configuration{ + read_configuration(options, configuration_path, current_path)}; + const auto display_path{stdin_path()}; + + std::string raw_stdin; + const auto parsed{read_from_stdin(&raw_stdin)}; + if (parsed.yaml) { + throw YAMLInputError{"This command does not support YAML input files yet", + display_path}; + } + + const auto &document{parsed.document}; + const auto dialect{default_dialect(options, configuration)}; + const auto is_test_document = + dialect.empty() && looks_like_test_document(document); + const auto effective_dialect = + is_test_document ? TEST_DOCUMENT_DEFAULT_DIALECT : dialect; + if (is_test_document) { + std::cerr << "Interpreting as a test file: " << display_path.string() + << "\n"; + } + const auto &custom_resolver{resolver(options, options.contains("http"), + effective_dialect, configuration)}; + const auto stdin_label{display_path.string()}; + + try { + if (options.contains("check")) { + std::ostringstream expected; + if (options.contains("keep-ordering")) { + sourcemeta::core::prettify(document, expected, indentation); + } else { + auto copy = document; + sourcemeta::blaze::format(copy, sourcemeta::blaze::schema_walker, + custom_resolver, effective_dialect); + sourcemeta::core::prettify(copy, expected, indentation); + } + expected << "\n"; + + if (raw_stdin == expected.str()) { + LOG_VERBOSE(options) << "ok: " << stdin_label << "\n"; + } else if (output_json) { + failed_files.push_back(stdin_label); + result = false; + } else { + std::cerr << "fail: " << stdin_label << "\n"; + result = false; + } + } else { + if (options.contains("keep-ordering")) { + sourcemeta::core::prettify(document, std::cout, indentation); + } else { + auto copy = document; + sourcemeta::blaze::format(copy, sourcemeta::blaze::schema_walker, + custom_resolver, effective_dialect); + sourcemeta::core::prettify(copy, std::cout, indentation); + } + std::cout << "\n"; + } + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError( + display_path, error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError( + display_path, error); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{parsed.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + display_path, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>(display_path, error); + } catch (const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError + &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( + display_path, error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaResolutionError>(display_path, error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>(display_path); + } catch (const sourcemeta::blaze::SchemaError &error) { + throw sourcemeta::core::FileError( + display_path, error.what()); + } + }; + + const auto handle_file_entry = [&](const InputJSON &entry) { + if (entry.yaml) { + throw YAMLInputError{"This command does not support YAML input files yet", + entry.resolution_base}; + } + + if (options.contains("check")) { + LOG_VERBOSE(options) << "Checking: " << entry.first << "\n"; + } else { + LOG_VERBOSE(options) << "Formatting: " << entry.first << "\n"; + } + + try { + const auto configuration_path{find_configuration(entry.resolution_base)}; + const auto &configuration{read_configuration(options, configuration_path, + entry.resolution_base)}; + const auto dialect{default_dialect(options, configuration)}; + const auto is_test_document = + dialect.empty() && looks_like_test_document(entry.second); + const auto effective_dialect = + is_test_document ? TEST_DOCUMENT_DEFAULT_DIALECT : dialect; + if (is_test_document) { + std::cerr << "Interpreting as a test file: " << entry.first << "\n"; + } + const auto &custom_resolver{resolver(options, options.contains("http"), + effective_dialect, configuration)}; + + std::ostringstream expected; + if (options.contains("keep-ordering")) { + sourcemeta::core::prettify(entry.second, expected, indentation); + } else { + auto copy = entry.second; + sourcemeta::blaze::format(copy, sourcemeta::blaze::schema_walker, + custom_resolver, effective_dialect); + sourcemeta::core::prettify(copy, expected, indentation); + } + expected << "\n"; + + const auto current{ + sourcemeta::core::read_file_to_string(entry.resolution_base)}; + + if (options.contains("check")) { + if (current == expected.str()) { + LOG_VERBOSE(options) << "ok: " << entry.first << "\n"; + } else if (output_json) { + failed_files.push_back(entry.first); + result = false; + } else { + std::cerr << "fail: " << entry.first << "\n"; + result = false; + } + } else { + if (current != expected.str()) { + sourcemeta::core::atomic_write_file(entry.resolution_base, + expected.str()); + } + } + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError( + entry.resolution_base, error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError( + entry.resolution_base, error); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{entry.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + entry.resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>(entry.resolution_base, + error); + } catch (const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError + &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( + entry.resolution_base, error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaResolutionError>(entry.resolution_base, + error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>( + entry.resolution_base); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>(entry.resolution_base); + } catch (const sourcemeta::blaze::SchemaError &error) { + throw sourcemeta::core::FileError( + entry.resolution_base, error.what()); + } + }; + + // Process arguments in order to preserve argument ordering semantics. + // When no positional arguments are given, default to for_each_json(options) + // which scans the current directory. + if (options.positional().empty()) { + for (const auto &entry : for_each_json(options)) { + handle_file_entry(entry); + } + } else { + check_no_duplicate_stdin(options.positional()); + for (const auto &arg : options.positional()) { + if (arg == "-") { + handle_stdin(); + } else { + for (const auto &entry : for_each_json({arg}, options)) { + handle_file_entry(entry); + } + } + } + } + + if (options.contains("check") && output_json) { + auto output_json_object{sourcemeta::core::JSON::make_object()}; + output_json_object.assign("valid", sourcemeta::core::JSON{result}); + + if (!result) { + auto errors_array{sourcemeta::core::JSON::make_array()}; + for (auto &file_path : failed_files) { + errors_array.push_back(sourcemeta::core::JSON{std::move(file_path)}); + } + + output_json_object.assign("errors", sourcemeta::core::JSON{errors_array}); + } + + sourcemeta::core::prettify(output_json_object, std::cout, indentation); + std::cout << "\n"; + } + + if (!result) { + if (!output_json) { + std::cerr << "\nRun the `fmt` command without `--check/-c` to fix the " + "formatting" + << "\n"; + } + + throw Fail{EXIT_EXPECTED_FAILURE}; + } +} diff --git a/vendor/jsonschema/src/command_inspect.cc b/vendor/jsonschema/src/command_inspect.cc new file mode 100644 index 000000000..8da39ef09 --- /dev/null +++ b/vendor/jsonschema/src/command_inspect.cc @@ -0,0 +1,263 @@ +#include +#include +#include +#include + +#include // std::cout +#include // std::ostream +#include // std::unreachable + +#include "command.h" +#include "configuration.h" +#include "error.h" +#include "input.h" +#include "resolver.h" +#include "utils.h" + +auto print_frame(std::ostream &stream, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::core::PointerPositionTracker &positions) + -> void { + if (frame.locations().empty()) { + return; + } + + for (auto iterator = frame.locations().cbegin(); + iterator != frame.locations().cend(); iterator++) { + const auto &location{*iterator}; + + switch (location.second.type) { + case sourcemeta::blaze::SchemaFrame::LocationType::Resource: + stream << "(RESOURCE)"; + break; + case sourcemeta::blaze::SchemaFrame::LocationType::Anchor: + stream << "(ANCHOR)"; + break; + case sourcemeta::blaze::SchemaFrame::LocationType::Pointer: + stream << "(POINTER)"; + break; + case sourcemeta::blaze::SchemaFrame::LocationType::Subschema: + stream << "(SUBSCHEMA)"; + break; + default: + std::unreachable(); + } + + stream << " URI: " << location.first.second << "\n"; + + if (location.first.first == + sourcemeta::blaze::SchemaReferenceType::Static) { + stream << " Type : Static\n"; + } else { + stream << " Type : Dynamic\n"; + } + + stream << " Root : " + << (frame.root().empty() ? "" : frame.root()) << "\n"; + + if (location.second.pointer.empty()) { + stream << " Pointer :\n"; + } else { + stream << " Pointer : "; + sourcemeta::core::stringify(location.second.pointer, stream); + stream << "\n"; + } + + const auto position{ + positions.get(sourcemeta::core::to_pointer(location.second.pointer))}; + if (position.has_value()) { + const auto [line, column, end_line, end_column] = position.value(); + stream << " File Position : " << line << ":" << column << "\n"; + } else { + stream << " File Position : :\n"; + } + + stream << " Base : " << location.second.base << "\n"; + + const auto relative_pointer{ + location.second.pointer.slice(location.second.relative_pointer)}; + if (relative_pointer.empty()) { + stream << " Relative Pointer :\n"; + } else { + stream << " Relative Pointer : "; + sourcemeta::core::stringify(relative_pointer, stream); + stream << "\n"; + } + + stream << " Dialect : " << location.second.dialect << "\n"; + stream << " Base Dialect : " + << sourcemeta::blaze::to_string(location.second.base_dialect) + << "\n"; + + if (location.second.parent.has_value()) { + if (location.second.parent.value().empty()) { + stream << " Parent :\n"; + } else { + stream << " Parent : "; + sourcemeta::core::stringify(location.second.parent.value(), stream); + stream << "\n"; + } + } else { + stream << " Parent : \n"; + } + + if (location.second.property_name) { + stream << " Property Name : yes\n"; + } else { + stream << " Property Name : no\n"; + } + + if (location.second.orphan) { + stream << " Orphan : yes\n"; + } else { + stream << " Orphan : no\n"; + } + + if (std::next(iterator) != frame.locations().cend()) { + stream << "\n"; + } + } + + for (auto iterator = frame.references().cbegin(); + iterator != frame.references().cend(); iterator++) { + stream << "\n"; + const auto &reference{*iterator}; + stream << "(REFERENCE) ORIGIN: "; + sourcemeta::core::stringify(reference.first.second, stream); + stream << "\n"; + + if (reference.first.first == + sourcemeta::blaze::SchemaReferenceType::Static) { + stream << " Type : Static\n"; + } else { + stream << " Type : Dynamic\n"; + } + + const auto position{ + positions.get(sourcemeta::core::to_pointer(reference.first.second))}; + if (position.has_value()) { + const auto [line, column, end_line, end_column] = position.value(); + stream << " File Position : " << line << ":" << column << "\n"; + } else { + stream << " File Position : :\n"; + } + + stream << " Destination : " << reference.second.destination + << "\n"; + stream << " - (w/o fragment) : " + << (reference.second.base.empty() ? "" : reference.second.base) + << "\n"; + stream << " - (fragment) : " + << reference.second.fragment.value_or("") << "\n"; + } +} + +auto sourcemeta::jsonschema::inspect(const sourcemeta::core::Options &options) + -> void { + if (options.positional().size() < 1) { + throw PositionalArgumentError{"This command expects a path to a schema", + "jsonschema inspect path/to/schema.json"}; + } + + validate_http_headers(options); + + const std::filesystem::path schema_path{options.positional().front()}; + const bool schema_from_stdin = (schema_path == "-"); + + if (!schema_from_stdin && std::filesystem::is_directory(schema_path)) { + throw sourcemeta::core::IOIsADirectoryError{schema_path}; + } + + const auto schema_config_base{ + schema_from_stdin ? std::filesystem::current_path() : schema_path}; + const auto schema_resolution_base{schema_from_stdin ? stdin_path() + : schema_path}; + + sourcemeta::core::PointerPositionTracker positions; + auto property_storage = std::make_shared>(); + const sourcemeta::core::JSON schema{[&]() { + if (schema_from_stdin) { + auto parsed{read_from_stdin()}; + positions = std::move(parsed.positions); + property_storage = std::move(parsed.property_storage); + return std::move(parsed.document); + } + sourcemeta::core::JSON document{sourcemeta::core::JSON{nullptr}}; + auto callback = make_position_callback(positions, property_storage); + sourcemeta::core::read_yaml_or_json(schema_path, document, callback); + return document; + }()}; + + if (!sourcemeta::blaze::is_schema(schema)) { + throw NotSchemaError{schema_from_stdin ? stdin_path() + : schema_resolution_base}; + } + + const auto configuration_path{find_configuration(schema_config_base)}; + const auto &configuration{ + read_configuration(options, configuration_path, schema_config_base)}; + const auto dialect{default_dialect(options, configuration)}; + + sourcemeta::blaze::SchemaFrame frame{ + sourcemeta::blaze::SchemaFrame::Mode::References}; + + try { + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + const auto identifier{ + sourcemeta::blaze::identify(schema, custom_resolver, dialect)}; + + frame.analyse( + schema, sourcemeta::blaze::schema_walker, custom_resolver, dialect, + + // Only use the file-based URI if the schema has no + // identifier, as otherwise we make the output unnecessarily + // hard when it comes to debugging schemas + !identifier.empty() + ? "" + : sourcemeta::jsonschema::default_id(schema_resolution_base)); + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>(schema_resolution_base, + error); + } catch ( + const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>( + schema_resolution_base); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>(schema_resolution_base); + } catch (const sourcemeta::blaze::SchemaError &error) { + throw sourcemeta::core::FileError( + schema_resolution_base, error.what()); + } + + if (options.contains("json")) { + sourcemeta::core::prettify(frame.to_json(positions), std::cout); + std::cout << "\n"; + } else { + print_frame(std::cout, frame, positions); + } +} diff --git a/vendor/jsonschema/src/command_install.cc b/vendor/jsonschema/src/command_install.cc new file mode 100644 index 000000000..a64712898 --- /dev/null +++ b/vendor/jsonschema/src/command_install.cc @@ -0,0 +1,467 @@ +#include +#include +#include + +#include // assert +#include // std::uint8_t +#include // std::filesystem +#include // std::cerr, std::cout +#include // std::optional +#include // std::ostream +#include // std::string +#include // std::move, std::to_underlying + +#include "command.h" +#include "configuration.h" +#include "error.h" +#include "logger.h" +#include "resolver.h" + +namespace { + +auto padded_label(const std::string_view label) -> std::string { + assert(label.size() <= 14); + std::string result{label}; + result.append(14 - label.size(), ' '); + result += " : "; + return result; +} + +auto atomic_write_json(const std::filesystem::path &path, + const sourcemeta::core::JSON &document) -> void { + sourcemeta::core::atomic_write_file( + path, [&document](std::ostream &stream) -> void { + sourcemeta::core::prettify(document, stream); + stream << "\n"; + }); +} + +auto dependency_fetch(const sourcemeta::core::Options &options, + const std::filesystem::path &configuration_path, + std::string_view uri) -> sourcemeta::core::JSON { + auto result{sourcemeta::jsonschema::fetch_schema(options, uri, true, true)}; + if (result.has_value()) { + return std::move(result.value()); + } + + throw sourcemeta::core::FileError( + configuration_path, uri, "Could not resolve schema"); +} + +auto dependency_resolve(const sourcemeta::core::Options &options, + const sourcemeta::blaze::Configuration &configuration, + std::string_view identifier) + -> std::optional { + const std::string string_identifier{identifier}; + + const auto mapped{sourcemeta::jsonschema::resolve_map_uri(configuration, + string_identifier)}; + if (mapped.has_value()) { + try { + auto result{ + sourcemeta::jsonschema::fetch_schema(options, mapped.value())}; + if (result.has_value()) { + return result; + } + } catch (...) { + } + } + + for (const auto &[dependency_uri, dependency_path] : + configuration.dependencies) { + if (dependency_uri == string_identifier && + std::filesystem::exists(dependency_path)) { + return sourcemeta::core::read_json(dependency_path); + } + } + + try { + return sourcemeta::jsonschema::fetch_schema(options, identifier); + } catch (...) { + return std::nullopt; + } +} + +auto emit_debug(const sourcemeta::core::Options &options, + const sourcemeta::blaze::Configuration::FetchEvent &event) + -> void { + if (!options.contains("debug")) { + return; + } + + using Type = sourcemeta::blaze::Configuration::FetchEvent::Type; + static const char *type_names[] = { + "fetch/start", "fetch/end", "bundle/start", "bundle/end", + "write/start", "write/end", "verify/start", "verify/end", + "up-to-date", "file-missing", "orphaned", "mismatched", + "path-mismatch", "untracked", "error"}; + static_assert(sizeof(type_names) / sizeof(type_names[0]) == + std::to_underlying(Type::Error) + 1); + const auto type_index{std::to_underlying(event.type)}; + std::cerr << "debug: " << type_names[type_index] << ": " << event.uri << " (" + << (event.index + 1) << "/" << event.total << ")"; + if (!event.path.empty()) { + std::cerr << " -> " << event.path.string(); + } + std::cerr << "\n"; +} + +auto emit_json(sourcemeta::core::JSON &events_array, + const std::string_view type, const std::string_view key, + const std::string_view value) -> void { + auto json_event{sourcemeta::core::JSON::make_object()}; + json_event.assign("type", sourcemeta::core::JSON{type}); + json_event.assign(key, sourcemeta::core::JSON{value}); + events_array.push_back(std::move(json_event)); +} + +auto output_json(sourcemeta::core::JSON &events_array) -> void { + auto result{sourcemeta::core::JSON::make_object()}; + result.assign("events", std::move(events_array)); + sourcemeta::core::prettify(result, std::cout); + std::cout << "\n"; +} + +enum class OrphanedBehavior : std::uint8_t { Delete, Error }; + +auto make_on_event(const sourcemeta::core::Options &options, + int &error_exit_code, const bool is_json, + sourcemeta::core::JSON &events_array, + const OrphanedBehavior orphaned_behavior) + -> sourcemeta::blaze::Configuration::FetchEvent::Callback { + return [&options, &error_exit_code, is_json, &events_array, + orphaned_behavior]( + const sourcemeta::blaze::Configuration::FetchEvent &event) + -> bool { + using Type = sourcemeta::blaze::Configuration::FetchEvent::Type; + + emit_debug(options, event); + + switch (event.type) { + case Type::FetchStart: + if (is_json) { + emit_json(events_array, "fetching", "uri", event.uri); + } else { + std::cerr << padded_label("Fetching") << event.uri << "\n"; + } + + break; + case Type::FetchEnd: + break; + case Type::BundleStart: + sourcemeta::jsonschema::LOG_VERBOSE(options) + << padded_label("Bundling") << event.uri << "\n"; + break; + case Type::BundleEnd: + break; + case Type::WriteStart: + sourcemeta::jsonschema::LOG_VERBOSE(options) + << padded_label("Writing") << event.path.string() << "\n"; + break; + case Type::WriteEnd: + break; + case Type::VerifyStart: + sourcemeta::jsonschema::LOG_VERBOSE(options) + << padded_label("Verifying") << event.path.string() << "\n"; + break; + case Type::VerifyEnd: + if (is_json) { + auto json_event{sourcemeta::core::JSON::make_object()}; + json_event.assign("type", sourcemeta::core::JSON{"installed"}); + json_event.assign("uri", sourcemeta::core::JSON{event.uri}); + json_event.assign("path", + sourcemeta::core::JSON{event.path.string()}); + events_array.push_back(std::move(json_event)); + } else { + std::cerr << padded_label("Installed") << event.path.string() << "\n"; + } + + break; + case Type::UpToDate: + if (is_json) { + emit_json(events_array, "up-to-date", "uri", event.uri); + } else { + std::cerr << padded_label("Up to date") << event.uri << "\n"; + } + + break; + case Type::FileMissing: + if (is_json) { + emit_json(events_array, "file-missing", "path", event.path.string()); + } else { + std::cerr << padded_label("File missing") << event.path.string() + << "\n"; + } + + break; + case Type::Mismatched: + if (is_json) { + emit_json(events_array, "mismatched", "path", event.path.string()); + } else { + std::cerr << padded_label("Mismatched") << event.path.string() + << "\n"; + } + + break; + case Type::PathMismatch: + if (is_json) { + emit_json(events_array, "path-mismatch", "uri", event.uri); + } else { + std::cerr << padded_label("Path mismatch") << event.uri << "\n"; + } + + break; + case Type::Untracked: + error_exit_code = sourcemeta::jsonschema::EXIT_EXPECTED_FAILURE; + if (is_json) { + emit_json(events_array, "untracked", "uri", event.uri); + } else { + std::cerr << padded_label("Untracked") << event.uri << "\n"; + } + + break; + case Type::Orphaned: + if (is_json) { + emit_json(events_array, "orphaned", "uri", event.uri); + } else { + std::cerr << padded_label("Orphaned") << event.uri << "\n"; + } + + if (orphaned_behavior == OrphanedBehavior::Delete) { + std::filesystem::remove(event.path); + } else { + error_exit_code = sourcemeta::jsonschema::EXIT_EXPECTED_FAILURE; + } + + break; + case Type::Error: + error_exit_code = orphaned_behavior == OrphanedBehavior::Error + ? sourcemeta::jsonschema::EXIT_EXPECTED_FAILURE + : sourcemeta::jsonschema::EXIT_OTHER_INPUT_ERROR; + if (is_json) { + auto json_event{sourcemeta::core::JSON::make_object()}; + json_event.assign("type", sourcemeta::core::JSON{"error"}); + json_event.assign("uri", sourcemeta::core::JSON{event.uri}); + json_event.assign("message", sourcemeta::core::JSON{event.details}); + events_array.push_back(std::move(json_event)); + } else { + sourcemeta::jsonschema::try_catch(options, [&]() -> int { + throw sourcemeta::jsonschema::InstallError{event.details, + event.uri}; + }); + } + + break; + } + + return true; + }; +} + +} // namespace + +auto sourcemeta::jsonschema::install(const sourcemeta::core::Options &options) + -> void { + const auto is_json{options.contains("json")}; + const auto is_frozen{options.contains("frozen")}; + auto events_array{sourcemeta::core::JSON::make_array()}; + + const auto &positional_arguments{options.positional()}; + if (positional_arguments.size() != 0 && positional_arguments.size() != 2) { + throw PositionalArgumentError{ + "The install command takes either zero or two positional arguments", + "jsonschema install https://example.com/schema ./vendor/schema.json"}; + } + + if (is_frozen && options.contains("force")) { + throw OptionConflictError{ + "The --frozen and --force options cannot be used together"}; + } + + if (is_frozen && !positional_arguments.empty()) { + throw PositionalArgumentError{ + "Do not use --frozen when adding a new dependency", + "jsonschema install https://example.com/schema ./vendor/schema.json"}; + } + + validate_http_headers(options); + + auto configuration_path{ + sourcemeta::blaze::Configuration::find(std::filesystem::current_path())}; + + if (positional_arguments.size() == 2) { + const std::string dependency_uri{positional_arguments.at(0)}; + const std::filesystem::path input_path{positional_arguments.at(1)}; + + sourcemeta::blaze::Configuration add_configuration; + const bool has_existing_config{configuration_path.has_value()}; + if (has_existing_config) { + try { + add_configuration = sourcemeta::blaze::Configuration::read_json( + configuration_path.value(), + sourcemeta::core::read_file_to_string<>); + } catch (const sourcemeta::blaze::ConfigurationParseError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::ConfigurationParseError>( + configuration_path.value(), error.what(), error.location()); + } catch (const sourcemeta::core::JSONParseError &error) { + throw sourcemeta::core::JSONFileParseError(configuration_path.value(), + error); + } + } else { + configuration_path = std::filesystem::current_path() / "jsonschema.json"; + add_configuration.absolute_path = std::filesystem::current_path(); + add_configuration.base_path = std::filesystem::current_path(); + } + + const auto absolute_target{std::filesystem::weakly_canonical( + std::filesystem::current_path() / input_path)}; + try { + const sourcemeta::core::URI uri{dependency_uri}; + add_configuration.dependencies.erase( + sourcemeta::core::URI::canonicalize(uri.recompose())); + add_configuration.add_dependency(uri, absolute_target); + } catch (const sourcemeta::core::URIParseError &) { + throw PositionalArgumentError{ + "The given URI is not valid", + "jsonschema install https://example.com/schema ./vendor/schema.json"}; + } catch (const sourcemeta::blaze::ConfigurationParseError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::ConfigurationParseError>( + configuration_path.value(), error.what(), error.location()); + } + auto config_json{ + has_existing_config + ? sourcemeta::core::read_json(configuration_path.value()) + : sourcemeta::core::JSON::make_object()}; + config_json.assign("dependencies", + add_configuration.to_json().at("dependencies")); + atomic_write_json(configuration_path.value(), config_json); + + auto relative_target{std::filesystem::relative( + absolute_target, add_configuration.absolute_path) + .generic_string()}; + if (!relative_target.starts_with("..")) { + relative_target = "./" + relative_target; + } + + if (is_json) { + auto json_event{sourcemeta::core::JSON::make_object()}; + json_event.assign("type", sourcemeta::core::JSON{"adding"}); + json_event.assign("uri", sourcemeta::core::JSON{dependency_uri}); + json_event.assign("path", sourcemeta::core::JSON{relative_target}); + events_array.push_back(std::move(json_event)); + } else { + std::cerr << padded_label("Adding") << dependency_uri << " -> " + << relative_target << "\n"; + } + } + + if (!configuration_path.has_value()) { + throw ConfigurationNotFoundError{std::filesystem::current_path()}; + } + + sourcemeta::blaze::Configuration configuration; + try { + configuration = sourcemeta::blaze::Configuration::read_json( + configuration_path.value(), sourcemeta::core::read_file_to_string<>); + } catch (const sourcemeta::blaze::ConfigurationParseError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::ConfigurationParseError>( + configuration_path.value(), error.what(), error.location()); + } catch (const sourcemeta::core::JSONParseError &error) { + throw sourcemeta::core::JSONFileParseError(configuration_path.value(), + error); + } + + if (configuration.dependencies.empty()) { + if (is_json) { + output_json(events_array); + } else { + std::cerr << "No dependencies found\n at " + << configuration_path.value().string() << "\n"; + } + + return; + } + + const auto lock_path{configuration.base_path / "jsonschema.lock.json"}; + sourcemeta::blaze::Configuration::Lock lock; + if (is_frozen) { + if (!std::filesystem::exists(lock_path)) { + throw LockNotFoundError{lock_path}; + } + + try { + lock = sourcemeta::blaze::Configuration::Lock::from_json( + sourcemeta::core::read_json(lock_path), configuration.base_path); + } catch (const sourcemeta::core::JSONParseError &error) { + throw sourcemeta::core::JSONFileParseError(lock_path, error); + } catch (...) { + throw LockParseError{lock_path}; + } + } else if (std::filesystem::exists(lock_path)) { + try { + lock = sourcemeta::blaze::Configuration::Lock::from_json( + sourcemeta::core::read_json(lock_path), configuration.base_path); + } catch (...) { + if (is_json) { + emit_json(events_array, "warning", "message", + "Ignoring corrupted lock file"); + } else { + std::cerr << "warning: Ignoring corrupted lock file\n at " + << lock_path.string() << "\n"; + } + } + } + + const sourcemeta::blaze::Configuration::WriteCallback writer{ + [](const std::filesystem::path &path, + const sourcemeta::core::JSON &document) -> void { + std::filesystem::create_directories(path.parent_path()); + atomic_write_json(path, document); + }}; + + const sourcemeta::blaze::Configuration::FetchCallback fetcher{ + [&options, + &configuration_path](std::string_view uri) -> sourcemeta::core::JSON { + return dependency_fetch(options, configuration_path.value(), uri); + }}; + + const sourcemeta::blaze::SchemaResolver resolver{ + [&options, &configuration](std::string_view identifier) + -> std::optional { + return dependency_resolve(options, configuration, identifier); + }}; + + int error_exit_code{0}; + const auto on_event{make_on_event( + options, error_exit_code, is_json, events_array, + is_frozen ? OrphanedBehavior::Error : OrphanedBehavior::Delete)}; + + if (is_frozen) { + configuration.fetch(lock, fetcher, resolver, + sourcemeta::core::read_file_to_string<>, writer, + on_event); + } else { + const auto fetch_mode{ + options.contains("force") + ? sourcemeta::blaze::Configuration::FetchMode::All + : sourcemeta::blaze::Configuration::FetchMode::Missing}; + configuration.fetch(lock, fetcher, resolver, + sourcemeta::core::read_file_to_string<>, writer, + on_event, fetch_mode); + } + + if (is_json) { + output_json(events_array); + } + + if (error_exit_code != 0) { + throw Fail{error_exit_code}; + } + + if (!is_frozen) { + atomic_write_json(lock_path, lock.to_json(configuration.base_path)); + } +} diff --git a/vendor/jsonschema/src/command_lint.cc b/vendor/jsonschema/src/command_lint.cc new file mode 100644 index 000000000..64dfe7208 --- /dev/null +++ b/vendor/jsonschema/src/command_lint.cc @@ -0,0 +1,657 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +#include // EXIT_SUCCESS +#include // std::filesystem::current_path +#include // std::cerr, std::cout +#include // std::accumulate +#include // std::optional +#include // std::ostream +#include // std::ostringstream + +#include "command.h" +#include "configuration.h" +#include "error.h" +#include "input.h" +#include "logger.h" +#include "resolver.h" +#include "utils.h" + +static const sourcemeta::core::JSON::String EXCLUDE_KEYWORD{"x-lint-exclude"}; + +template +static auto disable_lint_rules(sourcemeta::blaze::SchemaTransformer &bundle, + const Options &options, Iterator first, + Iterator last) -> void { + for (auto iterator = first; iterator != last; ++iterator) { + if (bundle.remove(*iterator)) { + sourcemeta::jsonschema::LOG_VERBOSE(options) + << "Disabling rule: " << *iterator << "\n"; + } else { + sourcemeta::jsonschema::LOG_WARNING() + << "Cannot exclude unknown rule: " << *iterator << "\n"; + } + } +} + +static auto reindent(const std::string_view &value, + const std::string &indentation, std::ostream &stream) + -> void { + if (!value.empty()) { + stream << indentation; + } + + for (std::size_t index = 0; index < value.size(); index++) { + const auto character{value[index]}; + stream.put(character); + if (character == '\n' && index != value.size() - 1) { + stream << indentation; + } + } +} + +static auto get_lint_callback(sourcemeta::core::JSON &errors_array, + const sourcemeta::jsonschema::InputJSON &entry, + const bool output_json, const bool fixing, + bool &printed_progress) -> auto { + return [&entry, &errors_array, output_json, fixing, &printed_progress]( + const auto &pointer, const auto &name, const auto &message, + const auto &result, const auto applied) { + if (fixing && applied) { + if (!output_json) { + std::cerr << "."; + printed_progress = true; + } + + return; + } + + if (printed_progress) { + std::cerr << "\n"; + printed_progress = false; + } + + std::vector locations; + if (result.locations.empty()) { + locations.emplace_back(); + } else { + for (const auto &location : result.locations) { + locations.push_back(location); + } + } + + for (const auto &location : locations) { + const auto schema_location{pointer.concat(location)}; + const auto position{entry.positions.get(schema_location)}; + + if (output_json) { + auto error_obj = sourcemeta::core::JSON::make_object(); + + error_obj.assign("path", sourcemeta::core::JSON{entry.first}); + error_obj.assign("id", sourcemeta::core::JSON{name}); + error_obj.assign("message", sourcemeta::core::JSON{message}); + error_obj.assign("description", + sourcemeta::core::to_json(result.description)); + error_obj.assign("schemaLocation", + sourcemeta::core::to_json(schema_location)); + if (position.has_value()) { + error_obj.assign("position", + sourcemeta::core::to_json(position.value())); + } else { + error_obj.assign("position", sourcemeta::core::to_json(nullptr)); + } + + errors_array.push_back(error_obj); + } else { + if (entry.from_stdin) { + std::cout << "/dev/stdin"; + } else { + std::cout + << std::filesystem::relative(entry.resolution_base).string(); + } + if (position.has_value()) { + const auto [line, column, end_line, end_column] = position.value(); + std::cout << ":"; + std::cout << line; + std::cout << ":"; + std::cout << column; + } else { + std::cout << "::"; + } + + std::cout << ":\n"; + std::cout << " " << message << " (" << name << ")\n"; + std::cout << " at location \""; + sourcemeta::core::stringify(schema_location, std::cout); + std::cout << "\"\n"; + + if (result.description.has_value()) { + reindent(result.description.value(), " ", std::cout); + if (result.description.value().back() != '\n') { + std::cout << "\n"; + } + } + } + } + }; +} + +static auto load_rule(sourcemeta::blaze::SchemaTransformer &bundle, + std::unordered_set &rule_names, + const std::filesystem::path &rule_path, + const std::string_view dialect, + const sourcemeta::blaze::SchemaResolver &custom_resolver, + const std::optional &tweaks) + -> void { + auto rule_schema{sourcemeta::core::read_yaml_or_json(rule_path)}; + if (!rule_schema.defines("description")) { + rule_schema.assign("description", + sourcemeta::core::JSON{""}); + } + + if (rule_schema.defines("title") && rule_schema.at("title").is_string()) { + const auto rule_name{rule_schema.at("title").to_string()}; + if (rule_names.contains(rule_name)) { + throw sourcemeta::core::FileError< + sourcemeta::jsonschema::DuplicateLintRuleError>(rule_path, rule_name); + } + + rule_names.emplace(rule_name); + } + + try { + bundle.add( + rule_schema, sourcemeta::blaze::schema_walker, custom_resolver, + sourcemeta::blaze::default_schema_compiler, dialect, tweaks); + } catch (const sourcemeta::blaze::SchemaRuleMissingNameError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRuleMissingNameError>(rule_path, error); + } catch (const sourcemeta::blaze::SchemaRuleInvalidNameError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRuleInvalidNameError>(rule_path, error); + } catch (const sourcemeta::blaze::SchemaRuleInvalidNamePatternError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRuleInvalidNamePatternError>(rule_path, error); + } catch ( + const sourcemeta::blaze::CompilerReferenceTargetNotSchemaError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerReferenceTargetNotSchemaError>(rule_path, + error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>(rule_path); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>(rule_path); + } catch (const sourcemeta::blaze::SchemaVocabularyError &error) { + throw sourcemeta::core::FileError( + rule_path, error.uri(), error.what()); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError( + rule_path, error); + } +} + +auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) + -> void { + validate_http_headers(options); + const bool output_json = options.contains("json"); + + sourcemeta::blaze::SchemaTransformer bundle; + sourcemeta::blaze::add(bundle, sourcemeta::blaze::AlterSchemaMode::Linter); + + std::unordered_set rule_names; + for (const auto &entry : bundle) { + const auto &[rule, check_flag, fix_flag] = entry; + rule_names.emplace(rule->name()); + } + + std::unordered_set seen_configurations; + std::vector input_paths; + if (options.positional().empty()) { + input_paths.emplace_back(std::filesystem::current_path()); + } else { + for (const auto &argument : options.positional()) { + if (argument == "-") { + input_paths.emplace_back(std::filesystem::current_path()); + } else { + input_paths.emplace_back(std::filesystem::weakly_canonical(argument)); + } + } + } + + for (const auto &input_path : input_paths) { + const auto configuration_path{find_configuration(input_path)}; + if (!configuration_path.has_value()) { + continue; + } + + const auto canonical_configuration_path{ + std::filesystem::weakly_canonical(configuration_path.value()).string()}; + if (seen_configurations.contains(canonical_configuration_path)) { + continue; + } + + seen_configurations.emplace(canonical_configuration_path); + const auto &configuration{read_configuration(options, configuration_path)}; + if (!configuration.has_value() || + configuration.value().lint.rules.empty()) { + continue; + } + + const auto dialect{default_dialect(options, configuration)}; + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + for (const auto &rule_path : configuration.value().lint.rules) { + LOG_VERBOSE(options) << "Loading custom rule from configuration: " + << rule_path.string() << "\n"; + load_rule(bundle, rule_names, rule_path, dialect, custom_resolver, + sourcemeta::jsonschema::format_assertion_tweaks(options)); + } + } + + if (options.contains("rule")) { + for (const auto &rule_path_string : options.at("rule")) { + const std::filesystem::path rule_path{ + std::filesystem::weakly_canonical(rule_path_string)}; + LOG_VERBOSE(options) << "Loading custom rule: " << rule_path.string() + << "\n"; + const auto configuration_path{find_configuration(rule_path)}; + const auto &configuration{ + read_configuration(options, configuration_path, rule_path)}; + const auto dialect{default_dialect(options, configuration)}; + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + load_rule(bundle, rule_names, rule_path, dialect, custom_resolver, + sourcemeta::jsonschema::format_assertion_tweaks(options)); + } + } + + if (options.contains("only")) { + if (options.contains("exclude")) { + throw OptionConflictError{ + "Cannot use --only and --exclude at the same time"}; + } + + std::unordered_set blacklist; + for (const auto &[rule, check_flag, fix_flag] : bundle) { + blacklist.emplace(rule->name()); + } + + for (const auto &only : options.at("only")) { + LOG_VERBOSE(options) << "Only enabling rule: " << only << "\n"; + if (blacklist.erase(only) == 0) { + throw InvalidLintRuleError{"The following linting rule does not exist", + std::string{only}}; + } + } + + for (const auto &name : blacklist) { + bundle.remove(name); + } + } else if (options.contains("exclude")) { + disable_lint_rules(bundle, options, options.at("exclude").cbegin(), + options.at("exclude").cend()); + } + + if (options.contains("list")) { + std::vector> rules; + for (const auto &[rule, check_flag, fix_flag] : bundle) { + rules.emplace_back(rule->name(), rule->message()); + } + + std::sort( + rules.begin(), rules.end(), [](const auto &left, const auto &right) { + return left.first < right.first || + (left.first == right.first && left.second < right.second); + }); + + std::size_t count{0}; + for (const auto &entry : rules) { + std::cout << entry.first << "\n"; + std::cout << " " << entry.second << "\n\n"; + count += 1; + } + + std::cout << "Number of rules: " << count << "\n"; + return; + } + + const bool format_output{options.contains("format")}; + const bool keep_ordering{options.contains("keep-ordering")}; + + if (format_output && !options.contains("fix")) { + throw OptionConflictError{"The --format option requires --fix to be set"}; + } + + if (keep_ordering && !format_output) { + throw OptionConflictError{ + "The --keep-ordering option requires --format to be set"}; + } + + bool result{true}; + auto errors_array = sourcemeta::core::JSON::make_array(); + std::vector scores; + const auto indentation{parse_indentation(options)}; + + if (options.contains("fix")) { + const auto entries = for_each_json(options); + + for (const auto &entry : entries) { + const auto configuration_path{find_configuration(entry.resolution_base)}; + const auto &configuration{read_configuration(options, configuration_path, + entry.resolution_base)}; + const auto dialect{default_dialect(options, configuration)}; + + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + LOG_VERBOSE(options) << "Linting: " << entry.first << "\n"; + if (entry.yaml) { + throw YAMLInputError{ + "The --fix option is not supported for YAML input files", + entry.resolution_base}; + } + + auto copy = entry.second; + bool printed_progress{false}; + + const auto wrapper_result = + sourcemeta::jsonschema::try_catch(options, [&]() { + try { + const auto apply_result = bundle.apply( + copy, sourcemeta::blaze::schema_walker, custom_resolver, + get_lint_callback(errors_array, entry, output_json, true, + printed_progress), + dialect, sourcemeta::jsonschema::default_id(entry), + EXCLUDE_KEYWORD); + if (printed_progress) { + std::cerr << "\n"; + } + scores.emplace_back(apply_result.second); + if (!apply_result.first) { + return EXIT_EXPECTED_FAILURE; + } + + return EXIT_SUCCESS; + } catch ( + const sourcemeta::blaze::SchemaTransformRuleProcessedTwiceError + &error) { + if (printed_progress) { + std::cerr << "\n"; + } + + throw LintAutoFixError{error.what(), entry.resolution_base, + error.location()}; + } catch ( + const sourcemeta::blaze::SchemaBrokenReferenceError &error) { + if (printed_progress) { + std::cerr << "\n"; + } + + throw LintAutoFixError{ + "Could not autofix the schema without breaking its internal " + "references", + entry.resolution_base, error.location()}; + } catch ( + const sourcemeta::blaze::CompilerInvalidRegexError &error) { + if (printed_progress) { + std::cerr << "\n"; + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerInvalidRegexError>( + entry.resolution_base, error); + } catch ( + const sourcemeta::blaze::CompilerReferenceTargetNotSchemaError + &error) { + if (printed_progress) { + std::cerr << "\n"; + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerReferenceTargetNotSchemaError>( + entry.resolution_base, error); + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + if (printed_progress) { + std::cerr << "\n"; + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaKeywordError>(entry.resolution_base, + error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + if (printed_progress) { + std::cerr << "\n"; + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaFrameError>(entry.resolution_base, + error); + } catch ( + const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + if (printed_progress) { + std::cerr << "\n"; + } + + const auto position{entry.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), + std::get<1>(position.value()), entry.resolution_base, + error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>( + entry.resolution_base, error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + if (printed_progress) { + std::cerr << "\n"; + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>( + entry.resolution_base); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + if (printed_progress) { + std::cerr << "\n"; + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>( + entry.resolution_base); + } catch (const sourcemeta::blaze::SchemaVocabularyError &error) { + if (printed_progress) { + std::cerr << "\n"; + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaVocabularyError>( + entry.resolution_base, error.uri(), error.what()); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + if (printed_progress) { + std::cerr << "\n"; + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaResolutionError>( + entry.resolution_base, error); + } catch (...) { + if (printed_progress) { + std::cerr << "\n"; + } + + throw; + } + }); + + if (wrapper_result == EXIT_SUCCESS || + wrapper_result == EXIT_EXPECTED_FAILURE) { + if (wrapper_result != EXIT_SUCCESS) { + result = false; + } + + if (entry.from_stdin) { + if (format_output) { + if (!keep_ordering) { + sourcemeta::blaze::format(copy, sourcemeta::blaze::schema_walker, + custom_resolver, dialect); + } + } + + sourcemeta::core::prettify(copy, std::cout, indentation); + std::cout << "\n"; + } else if (format_output) { + if (!keep_ordering) { + sourcemeta::blaze::format(copy, sourcemeta::blaze::schema_walker, + custom_resolver, dialect); + } + + std::ostringstream expected; + sourcemeta::core::prettify(copy, expected, indentation); + expected << "\n"; + + const auto current{ + sourcemeta::core::read_file_to_string(entry.resolution_base)}; + + if (current != expected.str()) { + sourcemeta::core::atomic_write_file(entry.resolution_base, + expected.str()); + } + } else if (copy != entry.second) { + sourcemeta::core::atomic_write_file( + entry.resolution_base, + [©, &indentation](std::ostream &stream) -> void { + sourcemeta::core::prettify(copy, stream, indentation); + stream << "\n"; + }); + } + } else { + throw Fail{wrapper_result}; + } + } + } else { + for (const auto &entry : for_each_json(options)) { + const auto configuration_path{find_configuration(entry.resolution_base)}; + const auto &configuration{read_configuration(options, configuration_path, + entry.resolution_base)}; + const auto dialect{default_dialect(options, configuration)}; + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + LOG_VERBOSE(options) << "Linting: " << entry.first << "\n"; + + bool printed_progress{false}; + const auto wrapper_result = + sourcemeta::jsonschema::try_catch(options, [&]() { + try { + const auto subresult = bundle.check( + entry.second, sourcemeta::blaze::schema_walker, + custom_resolver, + get_lint_callback(errors_array, entry, output_json, false, + printed_progress), + dialect, sourcemeta::jsonschema::default_id(entry), + EXCLUDE_KEYWORD); + scores.emplace_back(subresult.second); + if (subresult.first) { + return EXIT_SUCCESS; + } else { + // Return 2 for logical lint failures + return EXIT_EXPECTED_FAILURE; + } + } catch ( + const sourcemeta::blaze::CompilerInvalidRegexError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerInvalidRegexError>( + entry.resolution_base, error); + } catch ( + const sourcemeta::blaze::CompilerReferenceTargetNotSchemaError + &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerReferenceTargetNotSchemaError>( + entry.resolution_base, error); + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaKeywordError>(entry.resolution_base, + error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaFrameError>(entry.resolution_base, + error); + } catch ( + const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{entry.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), + std::get<1>(position.value()), entry.resolution_base, + error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>( + entry.resolution_base, error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>( + entry.resolution_base); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>( + entry.resolution_base); + } catch (const sourcemeta::blaze::SchemaVocabularyError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaVocabularyError>( + entry.resolution_base, error.uri(), error.what()); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaResolutionError>( + entry.resolution_base, error); + } + }); + + if (wrapper_result == EXIT_EXPECTED_FAILURE) { + result = false; + } else if (wrapper_result != EXIT_SUCCESS) { + throw Fail{wrapper_result}; + } + } + } + + if (output_json) { + std::sort(errors_array.as_array().begin(), errors_array.as_array().end(), + [](const sourcemeta::core::JSON &left, + const sourcemeta::core::JSON &right) { + return left.at("position").front() < + right.at("position").front(); + }); + + auto output_json_object = sourcemeta::core::JSON::make_object(); + output_json_object.assign("valid", sourcemeta::core::JSON{result}); + + if (scores.empty()) { + output_json_object.assign("health", sourcemeta::core::JSON{nullptr}); + } else { + const auto health{std::accumulate(scores.cbegin(), scores.cend(), 0ull) / + scores.size()}; + output_json_object.assign( + "health", sourcemeta::core::JSON{static_cast(health)}); + } + + output_json_object.assign("errors", sourcemeta::core::JSON{errors_array}); + sourcemeta::core::prettify(output_json_object, std::cout, indentation); + std::cout << "\n"; + } + + if (!result) { + throw Fail{EXIT_EXPECTED_FAILURE}; + } +} diff --git a/vendor/jsonschema/src/command_metaschema.cc b/vendor/jsonschema/src/command_metaschema.cc new file mode 100644 index 000000000..af1e5559f --- /dev/null +++ b/vendor/jsonschema/src/command_metaschema.cc @@ -0,0 +1,156 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include // assert +#include // std::cout, std::cerr +#include // std::map +#include // std::string + +#include "command.h" +#include "configuration.h" +#include "error.h" +#include "input.h" +#include "logger.h" +#include "resolver.h" +#include "utils.h" + +auto sourcemeta::jsonschema::metaschema( + const sourcemeta::core::Options &options) -> void { + validate_http_headers(options); + const auto trace{options.contains("trace")}; + const auto json_output{options.contains("json")}; + + bool result{true}; + sourcemeta::blaze::Evaluator evaluator; + + std::map cache; + + for (const auto &entry : for_each_json(options)) { + if (!sourcemeta::blaze::is_schema(entry.second)) { + throw NotSchemaError{entry.from_stdin ? stdin_path() + : entry.resolution_base}; + } + + const auto configuration_path{find_configuration(entry.resolution_base)}; + const auto &configuration{ + read_configuration(options, configuration_path, entry.resolution_base)}; + const auto default_dialect_option{default_dialect(options, configuration)}; + + const auto &custom_resolver{resolver(options, options.contains("http"), + default_dialect_option, + configuration)}; + + try { + const auto dialect{ + sourcemeta::blaze::dialect(entry.second, default_dialect_option)}; + if (dialect.empty()) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>( + entry.resolution_base); + } + + const auto metaschema{sourcemeta::blaze::metaschema( + entry.second, custom_resolver, default_dialect_option)}; + const sourcemeta::core::JSON bundled{sourcemeta::blaze::bundle( + metaschema, sourcemeta::blaze::schema_walker, custom_resolver, + sourcemeta::blaze::BundleMode::References, default_dialect_option)}; + sourcemeta::blaze::SchemaFrame frame{ + sourcemeta::blaze::SchemaFrame::Mode::References}; + frame.analyse(bundled, sourcemeta::blaze::schema_walker, custom_resolver, + default_dialect_option); + + if (!cache.contains(std::string{dialect})) { + const auto metaschema_template{sourcemeta::blaze::compile( + bundled, sourcemeta::blaze::schema_walker, custom_resolver, + sourcemeta::blaze::default_schema_compiler, frame, frame.root(), + sourcemeta::blaze::Mode::Exhaustive, + sourcemeta::jsonschema::format_assertion_tweaks(options))}; + cache.insert({std::string{dialect}, metaschema_template}); + } + + if (trace) { + sourcemeta::blaze::TraceOutput output{ + sourcemeta::blaze::schema_walker, custom_resolver, + trace_callback(entry.positions, std::cout), + sourcemeta::core::empty_weak_pointer, frame}; + result = evaluator.validate(cache.at(std::string{dialect}), + entry.second, std::ref(output)); + } else if (json_output) { + // Otherwise its impossible to correlate the output + // when validating i.e. a directory of schemas + std::cerr << entry.first << "\n"; + const auto output{sourcemeta::blaze::standard( + evaluator, cache.at(std::string{dialect}), entry.second, + sourcemeta::blaze::StandardOutput::Basic, entry.positions)}; + assert(output.is_object()); + assert(output.defines("valid")); + assert(output.at("valid").is_boolean()); + if (!output.at("valid").to_boolean()) { + result = false; + } + + sourcemeta::core::prettify(output, std::cout); + std::cout << "\n"; + } else { + sourcemeta::blaze::SimpleOutput output{entry.second}; + if (evaluator.validate(cache.at(std::string{dialect}), entry.second, + std::ref(output))) { + LOG_VERBOSE(options) + << "ok: " << entry.first << "\n matches " << dialect << "\n"; + } else { + std::cerr << "fail: " << entry.first << "\n"; + print(output, entry.positions, std::cerr); + result = false; + } + } + } catch (const sourcemeta::blaze::CompilerInvalidRegexError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerInvalidRegexError>(entry.resolution_base, + error); + } catch ( + const sourcemeta::blaze::CompilerReferenceTargetNotSchemaError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerReferenceTargetNotSchemaError>( + entry.resolution_base, error); + } catch (const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError + &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( + entry.resolution_base, error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaResolutionError>(entry.resolution_base, + error); + } catch (const sourcemeta::blaze::SchemaVocabularyError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaVocabularyError>(entry.resolution_base, + error.uri(), error.what()); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>(entry.resolution_base); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{entry.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + entry.resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>(entry.resolution_base, + error); + } + } + + if (!result) { + throw Fail{EXIT_EXPECTED_FAILURE}; + } +} diff --git a/vendor/jsonschema/src/command_test.cc b/vendor/jsonschema/src/command_test.cc new file mode 100644 index 000000000..33a84a370 --- /dev/null +++ b/vendor/jsonschema/src/command_test.cc @@ -0,0 +1,393 @@ +#include +#include +#include + +#include + +#include // std::find, std::distance +#include // std::chrono +#include // std::cout +#include // std::optional +#include // std::ostringstream +#include // std::string +#include // std::string_view +#include // std::this_thread + +#include "command.h" +#include "configuration.h" +#include "configure.h" +#include "error.h" +#include "input.h" +#include "resolver.h" +#include "utils.h" + +namespace { + +auto parse_test_suite(const sourcemeta::jsonschema::InputJSON &entry, + const sourcemeta::blaze::SchemaResolver &schema_resolver, + const std::string_view dialect, const bool json_output, + const std::optional &tweaks) + -> sourcemeta::blaze::TestSuite { + try { + return sourcemeta::blaze::TestSuite::parse( + entry.second, entry.positions, entry.resolution_base.parent_path(), + schema_resolver, sourcemeta::blaze::schema_walker, + sourcemeta::blaze::default_schema_compiler, dialect, "", tweaks); + } catch (const sourcemeta::blaze::TestParseError &error) { + if (!json_output) { + std::cout << entry.first << ":\n"; + } + throw sourcemeta::core::FileError{ + entry.resolution_base, error.what(), error.location(), error.line(), + error.column()}; + } catch ( + const sourcemeta::blaze::CompilerReferenceTargetNotSchemaError &error) { + if (!json_output) { + std::cout << entry.first << ":\n"; + } + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerReferenceTargetNotSchemaError>{ + entry.resolution_base, error}; + } catch ( + const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError &error) { + if (!json_output) { + std::cout << entry.first << ":\n"; + } + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>{ + entry.resolution_base, error}; + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + if (!json_output) { + std::cout << entry.first << ":\n"; + } + throw sourcemeta::core::FileError{ + entry.resolution_base, error}; + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + if (!json_output) { + std::cout << entry.first << ":\n"; + } + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>{ + entry.resolution_base}; + } catch (const sourcemeta::blaze::SchemaVocabularyError &error) { + if (!json_output) { + std::cout << entry.first << ":\n"; + } + throw sourcemeta::core::FileError{ + entry.resolution_base, error.uri(), error.what()}; + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + if (!json_output) { + std::cout << entry.first << ":\n"; + } + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>{entry.resolution_base}; + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + if (!json_output) { + std::cout << entry.first << ":\n"; + } + + const auto position{entry.positions.get(error.location())}; + if (position.has_value()) { + throw sourcemeta::jsonschema::PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + entry.resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>{entry.resolution_base, + error}; + } catch (...) { + if (!json_output) { + std::cout << entry.first << ":\n"; + } + throw; + } +} + +auto report_as_text(const sourcemeta::core::Options &options) -> void { + bool result{true}; + const auto verbose{options.contains("verbose") || options.contains("debug")}; + + for (const auto &entry : sourcemeta::jsonschema::for_each_json(options)) { + const auto configuration_path{ + sourcemeta::jsonschema::find_configuration(entry.resolution_base)}; + const auto &configuration{sourcemeta::jsonschema::read_configuration( + options, configuration_path)}; + const auto dialect{ + sourcemeta::jsonschema::default_dialect(options, configuration)}; + const auto &schema_resolver{sourcemeta::jsonschema::resolver( + options, options.contains("http"), dialect, configuration)}; + + auto test_suite{parse_test_suite( + entry, schema_resolver, dialect, false, + sourcemeta::jsonschema::format_assertion_tweaks(options))}; + + std::cout << entry.first << ":"; + + const auto multi_target{test_suite.targets.size() > 1}; + sourcemeta::core::JSON::String last_target_header; + bool last_target_header_set{false}; + + const auto suite_result{test_suite.run( + [&](const sourcemeta::core::JSON::String &target, std::size_t index, + std::size_t total, const sourcemeta::blaze::TestCase &test_case, + bool actual, sourcemeta::blaze::TestTimestamp, + sourcemeta::blaze::TestTimestamp) { + if (verbose && index == 1) { + std::cout << "\n"; + } + + const auto entry_indent{multi_target ? " " : " "}; + + const auto emit_target_header{[&]() { + if (multi_target && + (!last_target_header_set || last_target_header != target)) { + std::cout << " " << target << ":\n"; + last_target_header = target; + last_target_header_set = true; + } + }}; + + const auto &description{test_case.description.empty() + ? "" + : test_case.description}; + + if (test_case.valid == actual) { + if (verbose) { + emit_target_header(); + std::cout << entry_indent << index << "/" << total << " PASS " + << description << "\n"; + } + } else if (!test_case.valid && actual) { + if (!verbose) { + std::cout << "\n"; + } + emit_target_header(); + std::cout << entry_indent << index << "/" << total << " FAIL " + << description << "\n\n" + << "error: Passed but was expected to fail\n"; + + if (index != total && verbose) { + std::cout << "\n"; + } + } else { + const std::string ref{"$ref"}; + sourcemeta::blaze::SimpleOutput output{test_case.data, + {std::cref(ref)}}; + const auto target_index{static_cast( + std::distance(test_suite.targets.cbegin(), + std::find(test_suite.targets.cbegin(), + test_suite.targets.cend(), target)))}; + test_suite.evaluator.validate( + test_suite.schemas_exhaustive[target_index], test_case.data, + std::ref(output)); + + if (!verbose) { + std::cout << "\n"; + } + emit_target_header(); + std::cout << entry_indent << index << "/" << total << " FAIL " + << description << "\n\n"; + sourcemeta::jsonschema::print(output, test_case.tracker, std::cout); + + if (index != total && verbose) { + std::cout << "\n"; + } + } + })}; + + if (suite_result.passed != suite_result.total) { + result = false; + } + + if (suite_result.total == 0) { + std::cout << " NO TESTS\n"; + } else if (!verbose && suite_result.passed == suite_result.total) { + std::cout << " PASS " << suite_result.passed << "/" << suite_result.total + << "\n"; + } + } + + if (!result) { + throw sourcemeta::jsonschema::Fail{ + sourcemeta::jsonschema::EXIT_EXPECTED_FAILURE}; + } +} + +auto timestamp_to_unix_ms( + const sourcemeta::blaze::TestTimestamp ×tamp, + const std::chrono::system_clock::time_point &system_ref, + const sourcemeta::blaze::TestTimestamp &steady_ref) -> std::int64_t { + const auto offset{timestamp - steady_ref}; + const auto unix_time{system_ref + offset}; + return std::chrono::duration_cast( + unix_time.time_since_epoch()) + .count(); +} + +auto duration_ms(const sourcemeta::blaze::TestTimestamp &start, + const sourcemeta::blaze::TestTimestamp &end) -> std::int64_t { + return std::chrono::duration_cast(end - start) + .count(); +} + +auto report_as_ctrf(const sourcemeta::core::Options &options) -> void { + bool result{true}; + + const auto system_ref{std::chrono::system_clock::now()}; + const auto steady_ref{std::chrono::steady_clock::now()}; + + auto ctrf_tests{sourcemeta::core::JSON::make_array()}; + std::size_t total_passed{0}; + std::size_t total_failed{0}; + sourcemeta::blaze::TestTimestamp global_start{}; + sourcemeta::blaze::TestTimestamp global_end{}; + bool first_suite{true}; + + for (const auto &entry : sourcemeta::jsonschema::for_each_json(options)) { + const auto configuration_path{ + sourcemeta::jsonschema::find_configuration(entry.resolution_base)}; + const auto &configuration{sourcemeta::jsonschema::read_configuration( + options, configuration_path)}; + const auto dialect{ + sourcemeta::jsonschema::default_dialect(options, configuration)}; + const auto &schema_resolver{sourcemeta::jsonschema::resolver( + options, options.contains("http"), dialect, configuration)}; + + auto test_suite{parse_test_suite( + entry, schema_resolver, dialect, true, + sourcemeta::jsonschema::format_assertion_tweaks(options))}; + + const auto file_path{entry.first}; + + const auto suite_result{test_suite.run( + [&](const sourcemeta::core::JSON::String &target, std::size_t, + std::size_t, const sourcemeta::blaze::TestCase &test_case, + bool actual, sourcemeta::blaze::TestTimestamp start, + sourcemeta::blaze::TestTimestamp end) { + auto test_object{sourcemeta::core::JSON::make_object()}; + + const auto &name{test_case.description.empty() + ? "" + : test_case.description}; + test_object.assign("name", sourcemeta::core::JSON{name}); + + const bool passed{test_case.valid == actual}; + test_object.assign( + "status", sourcemeta::core::JSON{passed ? "passed" : "failed"}); + + test_object.assign("duration", + sourcemeta::core::JSON{duration_ms(start, end)}); + auto suite{sourcemeta::core::JSON::make_array()}; + suite.push_back(sourcemeta::core::JSON{target}); + test_object.assign("suite", std::move(suite)); + test_object.assign("type", sourcemeta::core::JSON{"unit"}); + test_object.assign("filePath", sourcemeta::core::JSON{file_path}); + + const auto [test_line, test_column, test_end_line, test_end_column] = + test_case.position; + test_object.assign("line", sourcemeta::core::JSON{ + static_cast(test_line)}); + test_object.assign( + "retries", sourcemeta::core::JSON{static_cast(0)}); + test_object.assign("flaky", sourcemeta::core::JSON{false}); + std::ostringstream thread_id_stream; + thread_id_stream << std::this_thread::get_id(); + test_object.assign("threadId", + sourcemeta::core::JSON{thread_id_stream.str()}); + + if (!passed) { + if (!test_case.valid && actual) { + test_object.assign("message", + sourcemeta::core::JSON{"Passed but was " + "expected to fail"}); + } else { + std::ostringstream trace_stream; + const std::string ref{"$ref"}; + sourcemeta::blaze::SimpleOutput output{test_case.data, + {std::cref(ref)}}; + const auto target_index{static_cast( + std::distance(test_suite.targets.cbegin(), + std::find(test_suite.targets.cbegin(), + test_suite.targets.cend(), target)))}; + test_suite.evaluator.validate( + test_suite.schemas_exhaustive[target_index], test_case.data, + std::ref(output)); + sourcemeta::jsonschema::print(output, test_case.tracker, + trace_stream); + test_object.assign("trace", + sourcemeta::core::JSON{trace_stream.str()}); + } + } + + ctrf_tests.push_back(test_object); + })}; + + if (first_suite) { + global_start = suite_result.start; + first_suite = false; + } + global_end = suite_result.end; + + total_passed += suite_result.passed; + total_failed += suite_result.total - suite_result.passed; + + if (suite_result.passed != suite_result.total) { + result = false; + } + } + + // Build CTRF output + auto summary{sourcemeta::core::JSON::make_object()}; + summary.assign("tests", sourcemeta::core::JSON{static_cast( + total_passed + total_failed)}); + summary.assign("passed", sourcemeta::core::JSON{ + static_cast(total_passed)}); + summary.assign("failed", sourcemeta::core::JSON{ + static_cast(total_failed)}); + summary.assign("pending", + sourcemeta::core::JSON{static_cast(0)}); + summary.assign("skipped", + sourcemeta::core::JSON{static_cast(0)}); + summary.assign("other", sourcemeta::core::JSON{static_cast(0)}); + summary.assign("start", sourcemeta::core::JSON{timestamp_to_unix_ms( + global_start, system_ref, steady_ref)}); + summary.assign("stop", sourcemeta::core::JSON{timestamp_to_unix_ms( + global_end, system_ref, steady_ref)}); + + auto tool{sourcemeta::core::JSON::make_object()}; + tool.assign("name", sourcemeta::core::JSON{"jsonschema"}); + tool.assign("version", + sourcemeta::core::JSON{sourcemeta::jsonschema::PROJECT_VERSION}); + + auto results{sourcemeta::core::JSON::make_object()}; + results.assign("tool", std::move(tool)); + results.assign("summary", std::move(summary)); + results.assign("tests", std::move(ctrf_tests)); + + auto ctrf{sourcemeta::core::JSON::make_object()}; + ctrf.assign("reportFormat", sourcemeta::core::JSON{"CTRF"}); + ctrf.assign("specVersion", sourcemeta::core::JSON{"0.0.0"}); + ctrf.assign("results", std::move(results)); + + sourcemeta::core::prettify(ctrf, std::cout); + std::cout << "\n"; + + if (!result) { + throw sourcemeta::jsonschema::Fail{ + sourcemeta::jsonschema::EXIT_EXPECTED_FAILURE}; + } +} + +} // namespace + +auto sourcemeta::jsonschema::test(const sourcemeta::core::Options &options) + -> void { + validate_http_headers(options); + if (options.contains("json")) { + report_as_ctrf(options); + } else { + report_as_text(options); + } +} diff --git a/vendor/jsonschema/src/command_upgrade.cc b/vendor/jsonschema/src/command_upgrade.cc new file mode 100644 index 000000000..a873ad2ba --- /dev/null +++ b/vendor/jsonschema/src/command_upgrade.cc @@ -0,0 +1,210 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +#include // std::filesystem +#include // std::cout +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::ignore +#include // std::move + +#include "command.h" +#include "configuration.h" +#include "error.h" +#include "input.h" +#include "logger.h" +#include "resolver.h" +#include "utils.h" + +namespace { + +auto parse_target_dialect(const std::string_view value) + -> std::optional { + if (value == "draft4") { + return sourcemeta::blaze::AlterSchemaMode::UpgradeDraft4; + } else if (value == "draft6") { + return sourcemeta::blaze::AlterSchemaMode::UpgradeDraft6; + } else if (value == "draft7") { + return sourcemeta::blaze::AlterSchemaMode::UpgradeDraft7; + } else if (value == "2019-09") { + return sourcemeta::blaze::AlterSchemaMode::Upgrade201909; + } else if (value == "2020-12") { + return sourcemeta::blaze::AlterSchemaMode::Upgrade202012; + } + + throw sourcemeta::jsonschema::InvalidOptionEnumerationValueError{ + "The given target dialect is not supported", + "to", + {"draft4", "draft6", "draft7", "2019-09", "2020-12"}}; +} + +} // namespace + +auto sourcemeta::jsonschema::upgrade(const sourcemeta::core::Options &options) + -> void { + if (options.positional().size() < 1) { + throw PositionalArgumentError{"This command expects a path to a schema", + "jsonschema upgrade path/to/schema.json"}; + } + + const auto target_value{options.contains("to") ? options.at("to").front() + : std::string_view{"2020-12"}}; + const auto target_dialect_mode{parse_target_dialect(target_value)}; + + const std::filesystem::path schema_path{options.positional().front()}; + const bool schema_from_stdin = (schema_path == "-"); + + if (!schema_from_stdin && std::filesystem::is_directory(schema_path)) { + throw sourcemeta::core::IOIsADirectoryError{schema_path}; + } + + const auto schema_resolution_base{ + schema_from_stdin ? std::filesystem::current_path() : schema_path}; + const auto schema_display_path{schema_from_stdin ? stdin_path() + : schema_path}; + + const auto configuration_path{find_configuration(schema_resolution_base)}; + const auto &configuration{ + read_configuration(options, configuration_path, schema_resolution_base)}; + const auto dialect{default_dialect(options, configuration)}; + auto parsed_schema{schema_from_stdin ? read_from_stdin() + : read_file(schema_path)}; + + if (!sourcemeta::blaze::is_schema(parsed_schema.document)) { + throw NotSchemaError{schema_display_path}; + } + + auto &schema{parsed_schema.document}; + + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + + sourcemeta::blaze::SchemaFrame frame{ + sourcemeta::blaze::SchemaFrame::Mode::Locations}; + + try { + frame.analyse(schema, sourcemeta::blaze::schema_walker, custom_resolver, + dialect, + sourcemeta::jsonschema::default_id(schema_resolution_base)); + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError( + schema_display_path, error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError( + schema_display_path, error); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{parsed_schema.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_display_path, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>(schema_display_path, + error); + } catch (const sourcemeta::blaze::SchemaReferenceError &error) { + throw sourcemeta::core::FileError( + schema_display_path, error.identifier(), error.location(), + error.what()); + } catch ( + const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( + schema_display_path, error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError( + schema_display_path, error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>(schema_display_path); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>(schema_display_path); + } catch (const sourcemeta::blaze::SchemaError &error) { + throw sourcemeta::core::FileError( + schema_display_path, error.what()); + } + + for (const auto &entry : frame.locations()) { + switch (entry.second.base_dialect) { + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_2020_12: + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_2020_12_Hyper: + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_2019_09: + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_2019_09_Hyper: + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_Draft_7: + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_Draft_7_Hyper: + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_Draft_6: + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_Draft_6_Hyper: + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_Draft_4: + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_Draft_4_Hyper: + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_Draft_3: + case sourcemeta::blaze::SchemaBaseDialect::JSON_Schema_Draft_3_Hyper: + continue; + default: + break; + } + + const auto unsupported_location_pointer{ + sourcemeta::core::to_pointer(entry.second.pointer)}; + auto unsupported_dialect{std::string{entry.second.dialect}}; + const auto unsupported_position{ + parsed_schema.positions.get(unsupported_location_pointer)}; + if (unsupported_position.has_value()) { + throw PositionError{ + std::get<0>(unsupported_position.value()), + std::get<1>(unsupported_position.value()), schema_display_path, + unsupported_location_pointer, std::move(unsupported_dialect)}; + } + + throw UnsupportedDialectUpgradeError{schema_display_path, + unsupported_location_pointer, + std::move(unsupported_dialect)}; + } + + for (const auto &entry : frame.locations()) { + if (sourcemeta::blaze::is_known_schema(entry.second.dialect)) { + continue; + } + + const auto custom_location_pointer{ + sourcemeta::core::to_pointer(entry.second.pointer)}; + auto custom_dialect{std::string{entry.second.dialect}}; + const auto position{parsed_schema.positions.get(custom_location_pointer)}; + if (position.has_value()) { + throw PositionError{ + std::get<0>(position.value()), std::get<1>(position.value()), + schema_display_path, custom_location_pointer, + std::move(custom_dialect)}; + } + + throw CustomMetaschemaUpgradeError{schema_display_path, + custom_location_pointer, + std::move(custom_dialect)}; + } + + if (target_dialect_mode.has_value()) { + sourcemeta::blaze::SchemaTransformer transformer; + sourcemeta::blaze::add(transformer, target_dialect_mode.value()); + std::ignore = transformer.apply( + schema, sourcemeta::blaze::schema_walker, custom_resolver, + [](const auto &, const auto, const auto, const auto &, const auto) {}, + dialect, sourcemeta::jsonschema::default_id(schema_resolution_base), "", + options.contains("meta")); + } + + sourcemeta::blaze::format(schema, sourcemeta::blaze::schema_walker, + custom_resolver, dialect); + + sourcemeta::core::prettify(schema, std::cout); + std::cout << "\n"; +} diff --git a/vendor/jsonschema/src/command_validate.cc b/vendor/jsonschema/src/command_validate.cc new file mode 100644 index 000000000..aec3781a8 --- /dev/null +++ b/vendor/jsonschema/src/command_validate.cc @@ -0,0 +1,650 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include // std::chrono +#include // std::sqrt +#include // std::cerr +#include // std::string +#include // std::string_view + +#include "command.h" +#include "configuration.h" +#include "error.h" +#include "input.h" +#include "logger.h" +#include "resolver.h" +#include "utils.h" + +namespace { + +auto get_precompiled_schema_template_path( + const sourcemeta::core::Options &options) + -> std::optional { + if (options.contains("template") && !options.at("template").empty()) { + return options.at("template").front(); + } else { + return std::nullopt; + } +} + +auto get_schema_template(const sourcemeta::core::JSON &bundled, + const sourcemeta::blaze::SchemaResolver &resolver, + const sourcemeta::blaze::SchemaFrame &frame, + const std::string &entrypoint_uri, + const bool fast_mode, + const sourcemeta::core::Options &options) + -> sourcemeta::blaze::Template { + const auto precompiled{get_precompiled_schema_template_path(options)}; + if (precompiled.has_value()) { + sourcemeta::jsonschema::LOG_VERBOSE(options) + << "Parsing pre-compiled schema template: " + << sourcemeta::core::weakly_canonical(precompiled.value()).string() + << "\n"; + const auto schema_template{ + sourcemeta::core::read_yaml_or_json(precompiled.value())}; + const auto precompiled_result{ + sourcemeta::blaze::from_json(schema_template)}; + if (precompiled_result.has_value()) { + return precompiled_result.value(); + } else { + sourcemeta::jsonschema::LOG_WARNING() + << "Failed to parse pre-compiled schema template. " + "Compiling from scratch\n"; + } + } + + return sourcemeta::blaze::compile( + bundled, sourcemeta::blaze::schema_walker, resolver, + sourcemeta::blaze::default_schema_compiler, frame, entrypoint_uri, + fast_mode ? sourcemeta::blaze::Mode::FastValidation + : sourcemeta::blaze::Mode::Exhaustive, + sourcemeta::jsonschema::format_assertion_tweaks(options)); +} + +auto parse_loop(const sourcemeta::core::Options &options) -> std::uint64_t { + if (options.contains("loop")) { + return std::stoull(options.at("loop").front().data()); + } else { + return 1; + } +} + +// validate instance in a loop to measure avg and stdev +auto run_loop(sourcemeta::blaze::Evaluator &evaluator, + const sourcemeta::blaze::Template &schema_template, + const sourcemeta::core::JSON &instance, + const std::string &instance_path, const int64_t instance_index, + const uint64_t loop) -> bool { + const auto iterations = static_cast(loop); + double sum = 0.0, sum2 = 0.0, empty = 0.0; + bool result = true; + + // Overhead evaluation, if not to optimize out! + for (auto index = loop; index; index--) { + const auto start{std::chrono::high_resolution_clock::now()}; + const auto end{std::chrono::high_resolution_clock::now()}; + empty += + static_cast( + std::chrono::duration_cast(end - start) + .count()) / + 1000.0; + } + empty /= iterations; + + // Actual performance loop + for (auto index = loop; index; index--) { + const auto start{std::chrono::high_resolution_clock::now()}; + result = evaluator.validate(schema_template, instance); + const auto end{std::chrono::high_resolution_clock::now()}; + + const auto raw_delay = + static_cast( + std::chrono::duration_cast(end - start) + .count()) / + 1000.0; + const auto delay = std::max(0.0, raw_delay - empty); + sum += delay; + sum2 += delay * delay; + } + + // Display json source, result and performance + auto avg = sum / iterations; + auto stdev = loop == 1 ? 0.0 : std::sqrt(sum2 / iterations - avg * avg); + + std::cout << instance_path; + if (instance_index >= 0) + std::cout << "[" << instance_index << "]"; + std::cout << std::fixed; + std::cout.precision(3); + std::cout << ": " << (result ? "PASS" : "FAIL") << " " << avg << " +- " + << stdev << " us (" << empty << ")\n"; + + return result; +} + +// Returns false if iteration should stop +auto process_entry( + const sourcemeta::jsonschema::InputJSON &entry, + sourcemeta::blaze::Evaluator &evaluator, + const sourcemeta::blaze::Template &schema_template, + const sourcemeta::jsonschema::CustomResolver &custom_resolver, + const sourcemeta::blaze::SchemaFrame &frame, bool benchmark, + std::uint64_t benchmark_loop, bool trace, bool fast_mode, bool json_output, + bool continue_on_error, bool schema_from_stdin, + const std::filesystem::path &schema_resolution_base, + const sourcemeta::core::Options &options, bool &result) -> bool { + std::ostringstream error; + sourcemeta::blaze::SimpleOutput output{entry.second}; + sourcemeta::blaze::TraceOutput trace_output{ + sourcemeta::blaze::schema_walker, custom_resolver, + sourcemeta::jsonschema::trace_callback(entry.positions, std::cout), + sourcemeta::core::empty_weak_pointer, frame}; + bool subresult{true}; + if (benchmark) { + subresult = run_loop(evaluator, schema_template, entry.second, entry.first, + entry.multidocument + ? static_cast(entry.index + 1) + : static_cast(-1), + benchmark_loop); + if (!subresult) { + error << "error: Schema validation failure\n"; + result = false; + } + } else if (trace) { + subresult = evaluator.validate(schema_template, entry.second, + std::ref(trace_output)); + } else if (fast_mode) { + subresult = evaluator.validate(schema_template, entry.second); + } else if (!json_output) { + subresult = + evaluator.validate(schema_template, entry.second, std::ref(output)); + } + + if (benchmark) { + return true; + } else if (trace) { + result = result && subresult; + } else if (json_output) { + if (!entry.multidocument) { + std::cerr << entry.first << "\n"; + } + const auto suboutput{sourcemeta::blaze::standard( + evaluator, schema_template, entry.second, + fast_mode ? sourcemeta::blaze::StandardOutput::Flag + : sourcemeta::blaze::StandardOutput::Basic, + entry.positions)}; + assert(suboutput.is_object()); + assert(suboutput.defines("valid")); + assert(suboutput.at("valid").is_boolean()); + sourcemeta::core::prettify(suboutput, std::cout); + std::cout << "\n"; + if (!suboutput.at("valid").to_boolean()) { + result = false; + if (entry.multidocument && !continue_on_error) { + return false; + } + } + } else if (subresult) { + if (continue_on_error && entry.multidocument && !result) { + sourcemeta::jsonschema::LOG_VERBOSE(options) << "\n"; + } + sourcemeta::jsonschema::LOG_VERBOSE(options) << "ok: " << entry.first; + if (entry.multidocument) { + sourcemeta::jsonschema::LOG_VERBOSE(options) + << " (entry #" << entry.index + 1 << ")"; + } + sourcemeta::jsonschema::LOG_VERBOSE(options) + << "\n matches " + << (schema_from_stdin + ? "/dev/stdin" + : sourcemeta::core::weakly_canonical(schema_resolution_base) + .string()) + << "\n"; + sourcemeta::jsonschema::print_annotations(output, options, entry.positions, + std::cerr); + } else { + if (continue_on_error && entry.multidocument && !result) { + std::cerr << "\n"; + } + std::cerr << "fail: " << entry.first; + if (entry.multidocument) { + std::cerr << " (entry #" << entry.index + 1 << ")\n\n"; + sourcemeta::core::prettify(entry.second, std::cerr); + std::cerr << "\n\n"; + } else { + std::cerr << "\n"; + } + std::cerr << error.str(); + sourcemeta::jsonschema::print(output, entry.positions, std::cerr); + result = false; + if (entry.multidocument && !continue_on_error) { + return false; + } + } + + return true; +} + +} // namespace + +auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) + -> void { + if (options.positional().size() < 1) { + throw PositionalArgumentError{ + "This command expects a path to a schema and a path to an\n" + "instance to validate against the schema", + "jsonschema validate path/to/schema.json path/to/instance.json"}; + } + + validate_http_headers(options); + + const auto &schema_path{options.positional().at(0)}; + const bool schema_from_stdin = (schema_path == "-"); + + // Centralized duplicate stdin check for all positional arguments + check_no_duplicate_stdin(options.positional()); + + if (!schema_from_stdin && std::filesystem::is_directory(schema_path)) { + throw sourcemeta::core::IOIsADirectoryError{schema_path}; + } + + const auto schema_config_base{schema_from_stdin + ? std::filesystem::current_path() + : std::filesystem::path(schema_path)}; + const auto schema_resolution_base{ + schema_from_stdin ? stdin_path() : std::filesystem::path(schema_path)}; + + const auto configuration_path{find_configuration(schema_config_base)}; + const auto &configuration{ + read_configuration(options, configuration_path, schema_config_base)}; + const auto dialect{default_dialect(options, configuration)}; + + auto parsed_schema{schema_from_stdin ? read_from_stdin() + : read_file(schema_path)}; + + if (!sourcemeta::blaze::is_schema(parsed_schema.document)) { + throw NotSchemaError{schema_from_stdin ? stdin_path() + : schema_resolution_base}; + } + + const auto &schema{parsed_schema.document}; + + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + + const auto fast_mode{options.contains("fast")}; + const auto benchmark{options.contains("benchmark")}; + const auto benchmark_loop{parse_loop(options)}; + if (benchmark_loop == 0) { + throw OptionConflictError{"The loop number cannot be zero"}; + } + + const auto trace{options.contains("trace")}; + const auto json_output{options.contains("json")}; + const auto continue_on_error{options.contains("continue")}; + + if (options.contains("entrypoint") && !options.at("entrypoint").empty() && + options.contains("template") && !options.at("template").empty()) { + throw OptionConflictError{ + "The --entrypoint option cannot be used with --template"}; + } + + if (options.contains("format-assertion") && options.contains("template") && + !options.at("template").empty()) { + throw OptionConflictError{ + "The --format-assertion option cannot be used with --template. " + "Re-compile the template with --format-assertion instead"}; + } + + const auto schema_default_id{ + sourcemeta::jsonschema::default_id(schema_resolution_base)}; + + const sourcemeta::core::JSON bundled{[&]() { + try { + return sourcemeta::blaze::bundle( + schema, sourcemeta::blaze::schema_walker, custom_resolver, + sourcemeta::blaze::BundleMode::References, dialect, + schema_default_id); + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{parsed_schema.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>(schema_resolution_base, + error); + } catch (const sourcemeta::blaze::SchemaReferenceError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaReferenceError>( + schema_resolution_base, error.identifier(), error.location(), + error.what()); + } catch (const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError + &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaResolutionError>(schema_resolution_base, + error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>( + schema_resolution_base); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>(schema_resolution_base); + } catch (const sourcemeta::blaze::SchemaError &error) { + throw sourcemeta::core::FileError( + schema_resolution_base, error.what()); + } catch ( + const sourcemeta::blaze::SchemaReferenceObjectResourceError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaReferenceObjectResourceError>( + schema_resolution_base, error.identifier()); + } + }()}; + + sourcemeta::blaze::SchemaFrame frame{ + sourcemeta::blaze::SchemaFrame::Mode::References}; + + try { + frame.analyse(bundled, sourcemeta::blaze::schema_walker, custom_resolver, + dialect, schema_default_id); + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{parsed_schema.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>(schema_resolution_base, + error); + } catch ( + const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>( + schema_resolution_base); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>(schema_resolution_base); + } catch (const sourcemeta::blaze::SchemaError &error) { + throw sourcemeta::core::FileError( + schema_resolution_base, error.what()); + } + + std::string entrypoint_uri{frame.root()}; + if (options.contains("entrypoint") && !options.at("entrypoint").empty()) { + try { + entrypoint_uri = + resolve_entrypoint(frame, options.at("entrypoint").front()); + } catch (const sourcemeta::blaze::CompilerInvalidEntryPoint &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerInvalidEntryPoint>(schema_resolution_base, + error); + } + } + + const auto schema_template{[&]() { + try { + return get_schema_template(bundled, custom_resolver, frame, + entrypoint_uri, fast_mode, options); + } catch (const sourcemeta::blaze::CompilerInvalidEntryPoint &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerInvalidEntryPoint>(schema_resolution_base, + error); + } catch (const sourcemeta::blaze::CompilerInvalidRegexError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerInvalidRegexError>(schema_resolution_base, + error); + } catch ( + const sourcemeta::blaze::CompilerReferenceTargetNotSchemaError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::CompilerReferenceTargetNotSchemaError>( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{parsed_schema.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>(schema_resolution_base, + error); + } catch (const sourcemeta::blaze::SchemaReferenceError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaReferenceError>( + schema_resolution_base, error.identifier(), error.location(), + error.what()); + } catch (const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError + &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( + schema_resolution_base, error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaResolutionError>(schema_resolution_base, + error); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>( + schema_resolution_base); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>(schema_resolution_base); + } catch (const sourcemeta::blaze::SchemaVocabularyError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaVocabularyError>(schema_resolution_base, + error.uri(), error.what()); + } catch (const sourcemeta::blaze::SchemaError &error) { + throw sourcemeta::core::FileError( + schema_resolution_base, error.what()); + } + }()}; + + sourcemeta::blaze::Evaluator evaluator; + + bool result{true}; + + std::vector instance_arguments; + if (options.positional().size() > 1) { + instance_arguments.assign(options.positional().cbegin() + 1, + options.positional().cend()); + } + + if (trace && benchmark) { + throw OptionConflictError{ + "The `--trace/-t` and `--benchmark/-b` options are mutually exclusive"}; + } + + if (trace && instance_arguments.size() > 1) { + throw OptionConflictError{ + "The `--trace/-t` option is only allowed given a single instance"}; + } + + if (benchmark && instance_arguments.size() > 1) { + throw OptionConflictError{ + "The `--benchmark/-b` option is only allowed given a single instance"}; + } + + if (instance_arguments.empty()) { + if (trace) { + throw OptionConflictError{ + "The `--trace/-t` option is only allowed given a single instance"}; + } + + if (benchmark) { + throw OptionConflictError{"The `--benchmark/-b` option is only allowed " + "given a single instance"}; + } + + for (const auto &entry : for_each_json({}, options)) { + if (!process_entry(entry, evaluator, schema_template, custom_resolver, + frame, benchmark, benchmark_loop, trace, fast_mode, + json_output, continue_on_error, schema_from_stdin, + schema_resolution_base, options, result)) { + break; + } + } + } else { + for (const auto &instance_path_view : instance_arguments) { + const std::filesystem::path instance_path{instance_path_view}; + if (trace && (instance_path.extension() == ".jsonl" || + instance_path.string().ends_with(".jsonl.gz"))) { + throw OptionConflictError{ + "The `--trace/-t` option is only allowed given a single instance"}; + } + + if (trace && instance_path_view != "-" && + std::filesystem::is_directory(instance_path)) { + throw OptionConflictError{ + "The `--trace/-t` option is only allowed given a single instance"}; + } + + if (benchmark && instance_path_view != "-" && + std::filesystem::is_directory(instance_path)) { + throw OptionConflictError{"The `--benchmark/-b` option is only allowed " + "given a single instance"}; + } + + if (instance_path_view == "-" || + std::filesystem::is_directory(instance_path) || + instance_path.extension() == ".jsonl" || + instance_path.string().ends_with(".jsonl.gz") || + instance_path.extension() == ".yaml" || + instance_path.extension() == ".yml") { + for (const auto &entry : for_each_json({instance_path_view}, options)) { + if (!process_entry(entry, evaluator, schema_template, custom_resolver, + frame, benchmark, benchmark_loop, trace, fast_mode, + json_output, continue_on_error, schema_from_stdin, + schema_resolution_base, options, result)) { + break; + } + } + } else { + sourcemeta::core::PointerPositionTracker tracker; + auto property_storage = std::make_shared>(); + const bool track_positions{(!fast_mode && !benchmark) || trace}; + const auto instance{[&]() -> sourcemeta::core::JSON { + if (track_positions) { + sourcemeta::core::JSON document{sourcemeta::core::JSON{nullptr}}; + auto callback = make_position_callback(tracker, property_storage); + sourcemeta::core::read_yaml_or_json(instance_path, document, + callback); + return document; + } + return sourcemeta::core::read_yaml_or_json(instance_path); + }()}; + std::ostringstream error; + sourcemeta::blaze::SimpleOutput output{instance}; + sourcemeta::blaze::TraceOutput trace_output{ + sourcemeta::blaze::schema_walker, custom_resolver, + trace_callback(tracker, std::cout), + sourcemeta::core::empty_weak_pointer, frame}; + bool subresult{true}; + if (benchmark) { + subresult = + run_loop(evaluator, schema_template, instance, + instance_path.string(), (int64_t)-1, benchmark_loop); + if (!subresult) { + error << "error: Schema validation failure\n"; + result = false; + } + } else if (trace) { + subresult = evaluator.validate(schema_template, instance, + std::ref(trace_output)); + } else if (fast_mode) { + subresult = evaluator.validate(schema_template, instance); + } else if (!json_output) { + subresult = + evaluator.validate(schema_template, instance, std::ref(output)); + } + + if (trace) { + result = result && subresult; + } else if (json_output) { + const auto suboutput{sourcemeta::blaze::standard( + evaluator, schema_template, instance, + fast_mode ? sourcemeta::blaze::StandardOutput::Flag + : sourcemeta::blaze::StandardOutput::Basic, + tracker)}; + assert(suboutput.is_object()); + assert(suboutput.defines("valid")); + assert(suboutput.at("valid").is_boolean()); + if (!suboutput.at("valid").to_boolean()) { + result = false; + } + + sourcemeta::core::prettify(suboutput, std::cout); + std::cout << "\n"; + } else if (subresult) { + LOG_VERBOSE(options) + << "ok: " + << sourcemeta::core::weakly_canonical(instance_path).string() + << "\n matches " + << (schema_from_stdin ? "/dev/stdin" + : sourcemeta::core::weakly_canonical( + schema_resolution_base) + .string()) + << "\n"; + print_annotations(output, options, tracker, std::cerr); + } else { + std::cerr + << "fail: " + << sourcemeta::core::weakly_canonical(instance_path).string() + << "\n"; + std::cerr << error.str(); + print(output, tracker, std::cerr); + result = false; + } + } + } + } + + if (!result) { + throw Fail{EXIT_EXPECTED_FAILURE}; + } +} diff --git a/vendor/jsonschema/src/configuration.h b/vendor/jsonschema/src/configuration.h new file mode 100644 index 000000000..3547661a5 --- /dev/null +++ b/vendor/jsonschema/src/configuration.h @@ -0,0 +1,135 @@ +#ifndef SOURCEMETA_JSONSCHEMA_CLI_CONFIGURATION_H_ +#define SOURCEMETA_JSONSCHEMA_CLI_CONFIGURATION_H_ + +#include +#include +#include +#include +#include +#include + +#include "error.h" +#include "logger.h" + +#include // assert +#include // std::size_t +#include // std::uint64_t +#include // std::deque +#include // std::filesystem +#include // std::map +#include // std::shared_ptr, std::make_shared +#include // std::optional +#include // std::string + +namespace sourcemeta::jsonschema { + +inline auto find_configuration(const std::filesystem::path &path) + -> std::optional { + return sourcemeta::blaze::Configuration::find(path); +} + +inline auto load_configuration( + const sourcemeta::core::Options &options, + const std::optional &configuration_path) + -> const std::optional & { + using CacheKey = std::optional; + static std::map> + configuration_cache; + + auto iterator{configuration_cache.find(configuration_path)}; + if (iterator != configuration_cache.end()) { + return iterator->second; + } + + std::optional result{std::nullopt}; + if (configuration_path.has_value()) { + LOG_DEBUG(options) << "Using configuration file: " + << sourcemeta::core::weakly_canonical( + configuration_path.value()) + .string() + << "\n"; + sourcemeta::core::PointerPositionTracker positions; + auto property_storage = std::make_shared>(); + try { + const auto contents{ + sourcemeta::core::read_file_to_string(configuration_path.value())}; + sourcemeta::core::JSON config_json{nullptr}; + sourcemeta::core::parse_json( + contents, config_json, + [&positions, &property_storage]( + const sourcemeta::core::JSON::ParsePhase phase, + const sourcemeta::core::JSON::Type type, const std::uint64_t line, + const std::uint64_t column, + const sourcemeta::core::JSON::ParseContext context, + const std::size_t index, + const sourcemeta::core::JSON::String &property) { + property_storage->emplace_back(property); + positions(phase, type, line, column, context, index, + property_storage->back()); + }); + result = sourcemeta::blaze::Configuration::from_json( + config_json, configuration_path.value().parent_path()); + } catch (const sourcemeta::blaze::ConfigurationParseError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::ConfigurationParseError>( + configuration_path.value(), error); + } + + assert(result.has_value()); + for (const auto &[resolve_uri, resolve_value] : result.value().resolve) { + const sourcemeta::core::URI value_uri{resolve_value}; + if (value_uri.is_relative()) { + const auto resolved_path{std::filesystem::weakly_canonical( + result.value().base_path / value_uri.to_path())}; + if (!std::filesystem::exists(resolved_path)) { + const sourcemeta::core::Pointer resolve_pointer{"resolve", + resolve_uri}; + const auto position{positions.get(resolve_pointer)}; + assert(position.has_value()); + throw ConfigurationResolveFileNotFoundError( + configuration_path.value(), resolve_pointer, resolved_path, + std::get<0>(position.value()), std::get<1>(position.value())); + } + } + } + + if (result.value().default_dialect.has_value()) { + try { + const sourcemeta::core::URI dialect_uri{ + result.value().default_dialect.value()}; + static_cast(dialect_uri); + } catch (const sourcemeta::core::URIParseError &) { + throw sourcemeta::core::FileError{ + configuration_path.value(), result.value().default_dialect.value()}; + } + } + } + + auto [inserted_iterator, inserted] = + configuration_cache.emplace(configuration_path, std::move(result)); + return inserted_iterator->second; +} + +inline auto read_configuration( + const sourcemeta::core::Options &options, + const std::optional &configuration_path, + const std::optional &schema_path = std::nullopt) + -> const std::optional & { + const auto &configuration{load_configuration(options, configuration_path)}; + if (configuration.has_value() && schema_path.has_value() && + !configuration.value().applies_to(schema_path.value())) { + LOG_DEBUG(options) + << "Ignoring configuration file given extensions mismatch: " + << sourcemeta::core::weakly_canonical(configuration_path.value()) + .string() + << "\n"; + static const std::optional empty{ + std::nullopt}; + return empty; + } + return configuration; +} + +} // namespace sourcemeta::jsonschema + +#endif diff --git a/vendor/jsonschema/src/configure.h.in b/vendor/jsonschema/src/configure.h.in new file mode 100644 index 000000000..b51c52cd2 --- /dev/null +++ b/vendor/jsonschema/src/configure.h.in @@ -0,0 +1,10 @@ +#ifndef SOURCEMETA_JSONSCHEMA_CLI_CONFIGURE_H_ +#define SOURCEMETA_JSONSCHEMA_CLI_CONFIGURE_H_ + +#include // std::string_view + +namespace sourcemeta::jsonschema { +constexpr std::string_view PROJECT_VERSION{"@PROJECT_VERSION@"}; +} + +#endif diff --git a/vendor/jsonschema/src/error.h b/vendor/jsonschema/src/error.h new file mode 100644 index 000000000..d9ba3afe0 --- /dev/null +++ b/vendor/jsonschema/src/error.h @@ -0,0 +1,1154 @@ +#ifndef SOURCEMETA_JSONSCHEMA_CLI_ERROR_H_ +#define SOURCEMETA_JSONSCHEMA_CLI_ERROR_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // std::same_as, std::convertible_to +#include // std::uint64_t +#include // std::filesystem +#include // std::function +#include // std::initializer_list +#include // std::cout, std::cerr +#include // std::runtime_error +#include // std::string +#include // std::is_base_of_v, std::is_same_v +#include // std::forward +#include // std::vector + +#include "exit_code.h" + +namespace sourcemeta::jsonschema { + +template class PositionError : public T { +public: + template + PositionError(const std::uint64_t line, const std::uint64_t column, + Args &&...args) + : T{std::forward(args)...}, line_{line}, column_{column} {} + + [[nodiscard]] auto line() const noexcept -> std::uint64_t { + return this->line_; + } + + [[nodiscard]] auto column() const noexcept -> std::uint64_t { + return this->column_; + } + +private: + std::uint64_t line_; + std::uint64_t column_; +}; + +class PositionalArgumentError : public std::runtime_error { +public: + PositionalArgumentError(std::string message, std::string example) + : std::runtime_error{message}, example_{std::move(example)} {} + + [[nodiscard]] auto example() const noexcept -> const std::string & { + return this->example_; + } + +private: + std::string example_; +}; + +class InvalidOptionEnumerationValueError : public std::runtime_error { +public: + InvalidOptionEnumerationValueError(std::string message, std::string option, + std::initializer_list values) + : std::runtime_error{message}, option_{std::move(option)}, + values_{values} {} + + [[nodiscard]] auto option() const noexcept -> const std::string & { + return this->option_; + } + + [[nodiscard]] auto values() const noexcept + -> const std::vector & { + return this->values_; + } + +private: + std::string option_; + std::vector values_; +}; + +class InvalidDefaultDialectError : public std::runtime_error { +public: + InvalidDefaultDialectError(std::string value) + : std::runtime_error{"The default dialect is not a valid URI reference"}, + value_{std::move(value)} {} + + [[nodiscard]] auto value() const noexcept -> const std::string & { + return this->value_; + } + +private: + std::string value_; +}; + +class NotSchemaError : public std::runtime_error { +public: + NotSchemaError(std::filesystem::path path) + : std::runtime_error{"The schema file you provided does not represent a " + "valid JSON " + "Schema"}, + path_{std::move(path)} {} + + [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & { + return this->path_; + } + +private: + std::filesystem::path path_; +}; + +class YAMLInputError : public std::runtime_error { +public: + YAMLInputError(std::string message, std::filesystem::path path) + : std::runtime_error{std::move(message)}, path_{std::move(path)} {} + + [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & { + return this->path_; + } + +private: + std::filesystem::path path_; +}; + +class OptionConflictError : public std::runtime_error { +public: + OptionConflictError(std::string message) + : std::runtime_error{std::move(message)} {} +}; + +class StdinError : public std::runtime_error { +public: + explicit StdinError(std::string message) + : std::runtime_error{std::move(message)} {} +}; + +class InvalidLintRuleError : public std::runtime_error { +public: + InvalidLintRuleError(std::string message, std::string rule) + : std::runtime_error{std::move(message)}, rule_{std::move(rule)} {} + + [[nodiscard]] auto rule() const noexcept -> const std::string & { + return this->rule_; + } + +private: + std::string rule_; +}; + +class DuplicateLintRuleError : public std::runtime_error { +public: + DuplicateLintRuleError(std::string rule) + : std::runtime_error{"A lint rule with this name already exists"}, + rule_{std::move(rule)} {} + + [[nodiscard]] auto rule() const noexcept -> const std::string & { + return this->rule_; + } + +private: + std::string rule_; +}; + +class InvalidIncludeIdentifier : public std::runtime_error { +public: + InvalidIncludeIdentifier(std::string identifier) + : std::runtime_error{"The include identifier is not a valid C/C++ " + "identifier"}, + identifier_{std::move(identifier)} {} + + [[nodiscard]] auto identifier() const noexcept -> const std::string & { + return this->identifier_; + } + +private: + std::string identifier_; +}; + +class LintAutoFixError : public std::runtime_error { +public: + LintAutoFixError(std::string message, std::filesystem::path path, + sourcemeta::core::Pointer location) + : std::runtime_error{std::move(message)}, path_{std::move(path)}, + location_{std::move(location)} {} + + [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & { + return this->path_; + } + + [[nodiscard]] auto location() const noexcept + -> const sourcemeta::core::Pointer & { + return this->location_; + } + +private: + std::filesystem::path path_; + sourcemeta::core::Pointer location_; +}; + +class CustomMetaschemaUpgradeError : public std::runtime_error { +public: + CustomMetaschemaUpgradeError(std::filesystem::path path, + sourcemeta::core::Pointer location, + std::string dialect) + : std::runtime_error{"Cannot upgrade a schema that uses a custom " + "meta-schema"}, + path_{std::move(path)}, location_{std::move(location)}, + dialect_{std::move(dialect)} {} + + [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & { + return this->path_; + } + + [[nodiscard]] auto location() const noexcept + -> const sourcemeta::core::Pointer & { + return this->location_; + } + + [[nodiscard]] auto uri() const noexcept -> const std::string & { + return this->dialect_; + } + +private: + std::filesystem::path path_; + sourcemeta::core::Pointer location_; + std::string dialect_; +}; + +class UnsupportedDialectUpgradeError : public std::runtime_error { +public: + UnsupportedDialectUpgradeError(std::filesystem::path path, + sourcemeta::core::Pointer location, + std::string dialect) + : std::runtime_error{"Upgrading schemas from this dialect is not " + "supported yet"}, + path_{std::move(path)}, location_{std::move(location)}, + dialect_{std::move(dialect)} {} + + [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & { + return this->path_; + } + + [[nodiscard]] auto location() const noexcept + -> const sourcemeta::core::Pointer & { + return this->location_; + } + + [[nodiscard]] auto uri() const noexcept -> const std::string & { + return this->dialect_; + } + +private: + std::filesystem::path path_; + sourcemeta::core::Pointer location_; + std::string dialect_; +}; + +class UnknownCommandError : public std::runtime_error { +public: + UnknownCommandError(std::string command) + : std::runtime_error{"Unknown command"}, command_{std::move(command)} {} + + [[nodiscard]] auto command() const noexcept -> const std::string & { + return this->command_; + } + +private: + std::string command_; +}; + +class ConfigurationNotFoundError : public std::runtime_error { +public: + ConfigurationNotFoundError(std::filesystem::path path) + : std::runtime_error{"Could not find a jsonschema.json configuration " + "file"}, + path_{std::move(path)} {} + + [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & { + return this->path_; + } + +private: + std::filesystem::path path_; +}; + +class LockNotFoundError : public std::runtime_error { +public: + LockNotFoundError(std::filesystem::path path) + : std::runtime_error{"Lock file not found"}, path_{std::move(path)} {} + + [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & { + return this->path_; + } + +private: + std::filesystem::path path_; +}; + +class LockParseError : public std::runtime_error { +public: + LockParseError(std::filesystem::path path) + : std::runtime_error{"Lock file is corrupted"}, path_{std::move(path)} {} + + [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & { + return this->path_; + } + +private: + std::filesystem::path path_; +}; + +class InstallError : public std::runtime_error { +public: + InstallError(std::string message, std::string uri) + : std::runtime_error{std::move(message)}, uri_{std::move(uri)} {} + + [[nodiscard]] auto uri() const noexcept -> const std::string & { + return this->uri_; + } + +private: + std::string uri_; +}; + +class ConfigurationResolveFileNotFoundError : public std::runtime_error { +public: + ConfigurationResolveFileNotFoundError( + std::filesystem::path configuration_path, + sourcemeta::core::Pointer location, std::filesystem::path resolve_path, + std::uint64_t line, std::uint64_t column) + : std::runtime_error{"The resolve target does not exist on the " + "filesystem"}, + configuration_path_{std::move(configuration_path)}, + location_{std::move(location)}, resolve_path_{std::move(resolve_path)}, + line_{line}, column_{column} {} + + [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & { + return this->configuration_path_; + } + + [[nodiscard]] auto location() const noexcept + -> const sourcemeta::core::Pointer & { + return this->location_; + } + + [[nodiscard]] auto resolve_path() const noexcept + -> const std::filesystem::path & { + return this->resolve_path_; + } + + [[nodiscard]] auto line() const noexcept -> std::uint64_t { + return this->line_; + } + + [[nodiscard]] auto column() const noexcept -> std::uint64_t { + return this->column_; + } + +private: + std::filesystem::path configuration_path_; + sourcemeta::core::Pointer location_; + std::filesystem::path resolve_path_; + std::uint64_t line_; + std::uint64_t column_; +}; + +class Fail : public std::runtime_error { +public: + Fail(int exit_code) : std::runtime_error{"Fail"}, exit_code_{exit_code} {} + + [[nodiscard]] auto exit_code() const noexcept -> int { + return this->exit_code_; + } + +private: + int exit_code_; +}; + +inline auto stdin_path() -> std::filesystem::path { +#ifdef _WIN32 + return std::filesystem::path{""}; +#else + return std::filesystem::path{"/dev/stdin"}; +#endif +} + +inline auto stdin_path_string(const std::filesystem::path &p) -> std::string { +#ifdef _WIN32 + if (p.string() == "") { + return ""; + } +#else + if (p == std::filesystem::path{"/dev/stdin"}) { + return "/dev/stdin"; + } +#endif + return sourcemeta::core::weakly_canonical(p).string(); +} + +template +inline auto print_exception(const bool is_json, const Exception &exception) + -> void { + auto error_json{sourcemeta::core::JSON::make_object()}; + + if constexpr (std::is_same_v) { + if (is_json) { + error_json.assign("error", + sourcemeta::core::JSON{"No such file or directory"}); + } else { + std::cerr << "error: No such file or directory\n"; + } + } else if constexpr (std::is_same_v) { + if (is_json) { + error_json.assign( + "error", + sourcemeta::core::JSON{ + "The input was supposed to be a file but it is a directory"}); + } else { + std::cerr << "error: The input was supposed to be a file but it is a " + "directory\n"; + } + } else if constexpr (std::is_base_of_v) { + if (is_json) { + error_json.assign("error", sourcemeta::core::JSON{exception.what()}); + } else { + std::cerr << "error: " << exception.what() << "\n"; + } + } else { + if (is_json) { + error_json.assign("error", sourcemeta::core::JSON{exception.what()}); + } else { + std::cerr << "error: " << exception.what() << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { current.identifier(); }) { + if (is_json) { + error_json.assign("identifier", + sourcemeta::core::JSON{exception.identifier()}); + } else { + std::cerr << " at identifier " << exception.identifier() << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { current.value(); }) { + if (is_json) { + error_json.assign("value", sourcemeta::core::JSON{exception.value()}); + } else { + std::cerr << " at value " << exception.value() << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { + { + current.keyword() + } -> std::convertible_to; + }) { + if (is_json) { + error_json.assign("keyword", sourcemeta::core::JSON{exception.keyword()}); + } else { + std::cerr << " at keyword " << exception.keyword() << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { + { + current.resolve_path() + } -> std::convertible_to; + }) { + const auto &resolve_path_value{exception.resolve_path()}; + if (is_json) { + error_json.assign("resolvePath", + sourcemeta::core::JSON{resolve_path_value.string()}); + } else { + std::cerr << " at resolve path " << resolve_path_value.string() << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { current.line(); }) { + if (is_json) { + error_json.assign("line", sourcemeta::core::JSON{static_cast( + exception.line())}); + } else { + std::cerr << " at line " << exception.line() << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { current.column(); }) { + if (is_json) { + error_json.assign( + "column", + sourcemeta::core::JSON{static_cast(exception.column())}); + } else { + std::cerr << " at column " << exception.column() << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { current.regex(); }) { + if (is_json) { + error_json.assign("regex", sourcemeta::core::JSON{exception.regex()}); + } else { + std::cerr << " at regex " << exception.regex() << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { + { + current.path() + } -> std::convertible_to; + }) { + const auto &error_path{exception.path()}; + const auto error_path_string{stdin_path_string(error_path)}; + if (is_json) { + error_json.assign("filePath", sourcemeta::core::JSON{error_path_string}); + } else { + std::cerr << " at file path " << error_path_string << "\n"; + } + } else if constexpr (requires(const Exception ¤t) { + { + current.path1() + } -> std::convertible_to; + }) { + if (is_json) { + error_json.assign( + "filePath", + sourcemeta::core::JSON{ + sourcemeta::core::weakly_canonical(exception.path1()).string()}); + } else { + std::cerr + << " at file path " + << sourcemeta::core::weakly_canonical(exception.path1()).string() + << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { current.location(); }) { + if (is_json) { + error_json.assign("location", + sourcemeta::core::JSON{ + sourcemeta::core::to_string(exception.location())}); + } else { + std::cerr << " at location \"" + << sourcemeta::core::to_string(exception.location()) << "\"\n"; + } + } + + if constexpr (requires(const Exception ¤t) { + { + current.other() + } -> std::convertible_to; + }) { + if (is_json) { + error_json.assign("otherLocation", + sourcemeta::core::JSON{ + sourcemeta::core::to_string(exception.other())}); + } else { + std::cerr << " at other location \"" + << sourcemeta::core::to_string(exception.other()) << "\"\n"; + } + } + + if constexpr (requires(const Exception ¤t) { + { + current.status() + } -> std::same_as; + }) { + const auto status{exception.status()}; + if (is_json) { + error_json.assign("status", sourcemeta::core::JSON{ + static_cast(status.code)}); + } else { + std::cerr << " with status "; + if (status.wire.empty()) { + std::cerr << status.code; + } else { + std::cerr << status.wire; + } + std::cerr << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { + { + current.method() + } -> std::same_as; + }) { + if (is_json) { + error_json.assign( + "method", + sourcemeta::core::JSON{std::string{ + sourcemeta::core::http_method_string(exception.method())}}); + } else { + std::cerr << " with method " + << sourcemeta::core::http_method_string(exception.method()) + << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { + { current.url() } -> std::convertible_to; + }) { + if (is_json) { + error_json.assign("url", + sourcemeta::core::JSON{std::string{exception.url()}}); + } else { + std::cerr << " at url " << exception.url() << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { + current.base().recompose(); + }) { + if (is_json) { + error_json.assign("baseURI", + sourcemeta::core::JSON{exception.base().recompose()}); + } else { + std::cerr << " at base uri " << exception.base().recompose() << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { current.uri(); }) { + if (is_json) { + error_json.assign("uri", sourcemeta::core::JSON{exception.uri()}); + } else { + std::cerr << " at uri " << exception.uri() << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { + { current.rule() } -> std::convertible_to; + }) { + if (is_json) { + error_json.assign("rule", sourcemeta::core::JSON{exception.rule()}); + } else { + std::cerr << " at rule " << exception.rule() << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { + { current.command() } -> std::convertible_to; + }) { + if (is_json) { + error_json.assign("command", sourcemeta::core::JSON{exception.command()}); + } else { + std::cerr << " at command " << exception.command() << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { + { current.option() } -> std::convertible_to; + }) { + if (is_json) { + error_json.assign("option", sourcemeta::core::JSON{exception.option()}); + } else { + std::cerr << " at option " << exception.option() << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { + { + current.values() + } -> std::convertible_to &>; + }) { + if (is_json) { + auto values_array{sourcemeta::core::JSON::make_array()}; + for (const auto &value : exception.values()) { + values_array.push_back(sourcemeta::core::JSON{value}); + } + error_json.assign("values", std::move(values_array)); + } else { + std::cerr << " with values\n"; + for (const auto &value : exception.values()) { + std::cerr << " - " << value << "\n"; + } + } + } + + if constexpr (requires(const Exception ¤t) { + { + current.variable() + } -> std::convertible_to; + }) { + if (is_json) { + error_json.assign("environmentVariable", + sourcemeta::core::JSON{exception.variable()}); + } else { + std::cerr << " with environment variable " << exception.variable() + << "\n"; + } + } + + if constexpr (requires(const Exception ¤t) { + { + current.paths() + } -> std::convertible_to &>; + }) { + if (is_json) { + auto paths_array{sourcemeta::core::JSON::make_array()}; + for (const auto &path : exception.paths()) { + paths_array.push_back(sourcemeta::core::JSON{path}); + } + error_json.assign("paths", std::move(paths_array)); + } else { + std::cerr << " with paths\n"; + for (const auto &path : exception.paths()) { + std::cerr << " - " << path << "\n"; + } + } + } + + if (is_json) { + sourcemeta::core::prettify(error_json, std::cout); + std::cout << "\n"; + } +} + +inline auto try_catch(const sourcemeta::core::Options &options, + const std::function &callback) noexcept -> int { + try { + return callback(); + } catch (const Fail &error) { + return error.exit_code(); + } catch (const InstallError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const ConfigurationNotFoundError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\nLearn more here: " + "https://github.com/sourcemeta/jsonschema/blob/main/" + "docs/install.markdown\n"; + } + + return EXIT_OTHER_INPUT_ERROR; + } catch (const LockNotFoundError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const LockParseError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const NotSchemaError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const YAMLInputError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_NOT_SUPPORTED; + } catch (const PositionError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_NOT_SUPPORTED; + } catch (const UnsupportedDialectUpgradeError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_NOT_SUPPORTED; + } catch (const InvalidLintRuleError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_INVALID_CLI_ARGUMENTS; + } catch (const sourcemeta::core::FileError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRuleInvalidNameError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRuleInvalidNamePatternError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRuleMissingNameError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const InvalidIncludeIdentifier &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_INVALID_CLI_ARGUMENTS; + } catch (const LintAutoFixError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\n"; + std::cerr << "This is an unexpected error, as making the auto-fix " + "functionality work in all\n"; + std::cerr << "cases is tricky. We are working hard to improve the " + "auto-fixing functionality\n"; + std::cerr << "to handle all possible edge cases, but for now, try again " + "without `--fix/-f`\n"; + std::cerr << "and apply the suggestions by hand.\n\n"; + std::cerr << "Also consider consider reporting this problematic case to " + "the issue tracker,\n"; + std::cerr << "so we can add it to the test suite and fix it:\n\n"; + std::cerr << "https://github.com/sourcemeta/jsonschema/issues\n"; + } + + return EXIT_UNEXPECTED_ERROR; + } catch (const PositionError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\n"; + std::cerr << "Schemas that declare a custom meta-schema cannot be " + "upgraded in place\n"; + std::cerr << "by this command. Please upgrade the meta-schema and the " + "schema manually.\n"; + } + + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const CustomMetaschemaUpgradeError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\n"; + std::cerr << "Schemas that declare a custom meta-schema cannot be " + "upgraded in place\n"; + std::cerr << "by this command. Please upgrade the meta-schema and the " + "schema manually.\n"; + } + + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const sourcemeta::core::FileError + &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\n"; + std::cerr << "Learn more here: " + "https://github.com/sourcemeta/jsonschema/blob/main/" + "docs/test.markdown\n"; + } + + return EXIT_OTHER_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::CompilerReferenceTargetNotSchemaError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\n"; + + if (!error.location().empty() && error.location().back().is_property() && + error.location().back().to_property() == "$defs") { + std::cerr << "Maybe you meant to use `definitions` instead of `$defs` " + "in this dialect?\n"; + } else { + std::cerr << "Are you sure the reported location is a valid JSON " + "Schema keyword in this dialect?\n"; + } + } + + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::CompilerInvalidEntryPoint> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr + << "\nUse the `inspect` command to find valid schema locations\n"; + } + + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::CompilerInvalidRegexError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\nDetailed regex error messages are not yet supported\n" + "Try tools like https://regex101.com to debug further\n"; + } + + return EXIT_SCHEMA_INPUT_ERROR; + } catch ( + const sourcemeta::core::FileError + &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const ConfigurationResolveFileNotFoundError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::ConfigurationParseError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::SchemaResolutionError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + if (error.identifier().starts_with("file://")) { + std::cerr << "\nThis is likely because the file does not exist\n"; + } else { + std::cerr + << "\nThis is likely because you forgot to import such schema " + "using `--resolve/-r`\n"; + } + } + + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\nAre you sure the input is a valid JSON Schema and its " + "base dialect is known?\n"; + std::cerr + << "If the input does not declare the `$schema` keyword, you might " + "want to\n"; + std::cerr << "explicitly declare a default dialect using " + "`--default-dialect/-d`\n"; + } + + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\nAre you sure the input is a valid JSON Schema and its " + "dialect is known?\n"; + std::cerr + << "If the input does not declare the `$schema` keyword, you might " + "want to\n"; + std::cerr << "explicitly declare a default dialect using " + "`--default-dialect/-d`\n"; + } + + return EXIT_SCHEMA_INPUT_ERROR; + } catch ( + const sourcemeta::core::FileError + &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\nAre you sure the input is a valid JSON Schema and it is " + "valid according to its meta-schema?\n"; + } + + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const PositionError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; + } catch ( + const sourcemeta::core::FileError + &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::SchemaReferenceObjectResourceError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const sourcemeta::core::FileError + &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::SchemaVocabularyError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::CodegenUnsupportedKeywordError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_NOT_SUPPORTED; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::CodegenUnsupportedKeywordValueError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_NOT_SUPPORTED; + } catch (const sourcemeta::core::FileError< + sourcemeta::blaze::CodegenUnexpectedSchemaError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_NOT_SUPPORTED; + } catch (const sourcemeta::core::JSONFileParseError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const sourcemeta::core::JSONParseError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + + // Command line parsing handling + } catch (const StdinError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_INVALID_CLI_ARGUMENTS; + } catch (const OptionConflictError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_INVALID_CLI_ARGUMENTS; + } catch (const InvalidOptionEnumerationValueError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\nRun the `help` command for usage information\n"; + } + + return EXIT_INVALID_CLI_ARGUMENTS; + } catch ( + const sourcemeta::core::FileError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const InvalidDefaultDialectError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_INVALID_CLI_ARGUMENTS; + } catch (const PositionalArgumentError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\nFor example: " << error.example() << "\n"; + } + + return EXIT_INVALID_CLI_ARGUMENTS; + } catch (const UnknownCommandError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\nRun the `help` command for usage information\n"; + } + + return EXIT_INVALID_CLI_ARGUMENTS; + } catch (const sourcemeta::core::OptionsUnexpectedValueFlagError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\nRun the `help` command for usage information\n"; + } + + return EXIT_INVALID_CLI_ARGUMENTS; + } catch (const sourcemeta::core::OptionsMissingOptionValueError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\nRun the `help` command for usage information\n"; + } + + return EXIT_INVALID_CLI_ARGUMENTS; + } catch (const sourcemeta::core::OptionsUnknownOptionError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\nRun the `help` command for usage information\n"; + } + + return EXIT_INVALID_CLI_ARGUMENTS; + + } catch ( + const sourcemeta::core::FileError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const sourcemeta::core::HTTPStatusError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_UNEXPECTED_ERROR; + } catch (const sourcemeta::core::HTTPError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_UNEXPECTED_ERROR; + } catch (const sourcemeta::core::HTTPSystemBackendError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_NOT_SUPPORTED; + + // Standard library handlers + } catch (const sourcemeta::core::IOFileNotFoundError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const sourcemeta::core::IOIsADirectoryError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const sourcemeta::core::IOFilePermissionError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const sourcemeta::core::IONotADirectoryError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const sourcemeta::core::IOFileAlreadyExistsError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const std::filesystem::filesystem_error &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_OTHER_INPUT_ERROR; + } catch (const std::runtime_error &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_UNEXPECTED_ERROR; + } catch (const std::exception &error) { + const auto is_json{options.contains("json")}; + if (is_json) { + auto error_json{sourcemeta::core::JSON::make_object()}; + error_json.assign("error", sourcemeta::core::JSON{error.what()}); + sourcemeta::core::prettify(error_json, std::cout); + std::cout << "\n"; + } else { + std::cerr << "unexpected error: " << error.what() + << "\nPlease report it at " + << "https://github.com/sourcemeta/jsonschema\n"; + } + + return EXIT_UNEXPECTED_ERROR; + } +} + +} // namespace sourcemeta::jsonschema + +#endif diff --git a/vendor/jsonschema/src/exit_code.h b/vendor/jsonschema/src/exit_code.h new file mode 100644 index 000000000..76861feca --- /dev/null +++ b/vendor/jsonschema/src/exit_code.h @@ -0,0 +1,15 @@ +#ifndef SOURCEMETA_JSONSCHEMA_CLI_EXIT_CODE_H_ +#define SOURCEMETA_JSONSCHEMA_CLI_EXIT_CODE_H_ + +namespace sourcemeta::jsonschema { + +constexpr int EXIT_UNEXPECTED_ERROR = 1; +constexpr int EXIT_EXPECTED_FAILURE = 2; +constexpr int EXIT_NOT_SUPPORTED = 3; +constexpr int EXIT_SCHEMA_INPUT_ERROR = 4; +constexpr int EXIT_INVALID_CLI_ARGUMENTS = 5; +constexpr int EXIT_OTHER_INPUT_ERROR = 6; + +} // namespace sourcemeta::jsonschema + +#endif diff --git a/vendor/jsonschema/src/input.h b/vendor/jsonschema/src/input.h new file mode 100644 index 000000000..c7c9bc93b --- /dev/null +++ b/vendor/jsonschema/src/input.h @@ -0,0 +1,515 @@ +#ifndef SOURCEMETA_JSONSCHEMA_CLI_INPUT_H_ +#define SOURCEMETA_JSONSCHEMA_CLI_INPUT_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "configuration.h" +#include "logger.h" + +#include // std::any_of, std::none_of, std::sort, std::count +#include // std::size_t +#include // std::uintptr_t +#include // std::deque +#include // std::filesystem +#include // std::ref, std::hash +#include // std::cin +#include // std::shared_ptr, std::make_shared +#include // std::optional +#include // std::set +#include // std::ostringstream, std::istringstream +#include // std::string +#include // std::unordered_set +#include // std::vector + +namespace sourcemeta::jsonschema { + +struct InputJSON { + std::string first; + std::filesystem::path resolution_base; + sourcemeta::core::JSON second; + sourcemeta::core::PointerPositionTracker positions; + std::size_t index{0}; + bool multidocument{false}; + bool yaml{false}; + bool from_stdin{false}; + std::shared_ptr> property_storage; + auto operator<(const InputJSON &other) const noexcept -> bool { + return this->first < other.first; + } +}; + +inline auto parse_extensions( + const sourcemeta::core::Options &options, + const std::optional &configuration) + -> const std::set & { + using CacheKey = + std::pair>; + static std::map> cache; + + CacheKey cache_key{reinterpret_cast(&options), + configuration.has_value() + ? std::optional{configuration.value().absolute_path} + : std::nullopt}; + + const auto iterator{cache.find(cache_key)}; + if (iterator != cache.end()) { + return iterator->second; + } + + std::set result; + + if (options.contains("extension")) { + for (const auto &extension : options.at("extension")) { + if (extension.empty() || extension.starts_with('.')) { + result.emplace(extension); + } else { + std::ostringstream normalised_extension; + normalised_extension << '.' << extension; + result.emplace(normalised_extension.str()); + } + } + } + + if (configuration.has_value()) { + for (const auto &extension : configuration.value().extension) { + if (extension.empty() || extension.starts_with('.')) { + result.emplace(extension); + } else { + std::ostringstream normalised_extension; + normalised_extension << '.' << extension; + result.emplace(normalised_extension.str()); + } + } + } + + for (const auto &extension : result) { + if (extension.empty()) { + LOG_WARNING() << "Matching files with no extension\n"; + } else { + LOG_VERBOSE(options) << "Using extension: " << extension << "\n"; + } + } + + if (result.empty()) { + result.insert({".json"}); + result.insert({".yaml"}); + result.insert({".yml"}); + } + + return cache.emplace(std::move(cache_key), std::move(result)).first->second; +} + +inline auto parse_ignore(const sourcemeta::core::Options &options) + -> std::set { + std::set result; + + if (options.contains("ignore")) { + for (const auto &ignore : options.at("ignore")) { + const auto canonical{std::filesystem::weakly_canonical(ignore)}; + LOG_VERBOSE(options) << "Ignoring path: " << canonical << "\n"; + result.insert(canonical); + } + } + + return result; +} + +inline auto +merge_configuration_ignore(const std::filesystem::path &configuration_path, + std::set &blacklist, + const sourcemeta::core::Options &options) -> void { + const auto &configuration{load_configuration(options, configuration_path)}; + assert(configuration.has_value()); + for (const auto &ignore_path : configuration.value().ignore) { + LOG_VERBOSE(options) << "Ignoring path from configuration: " << ignore_path + << "\n"; + blacklist.insert(ignore_path); + } +} + +namespace { + +struct ParsedJSON { + sourcemeta::core::JSON document; + sourcemeta::core::PointerPositionTracker positions; + std::shared_ptr> property_storage; + bool yaml{false}; +}; + +inline auto +make_position_callback(sourcemeta::core::PointerPositionTracker &tracker, + std::shared_ptr> &storage) + -> sourcemeta::core::JSON::ParseCallback { + return + [&tracker, &storage](const sourcemeta::core::JSON::ParsePhase phase, + const sourcemeta::core::JSON::Type type, + const std::uint64_t line, const std::uint64_t column, + const sourcemeta::core::JSON::ParseContext context, + const std::size_t index, + const sourcemeta::core::JSON::String &property) { + storage->emplace_back(property); + tracker(phase, type, line, column, context, index, storage->back()); + }; +} + +inline auto read_file(const std::filesystem::path &path) -> ParsedJSON { + const auto extension{path.extension()}; + sourcemeta::core::PointerPositionTracker positions; + auto property_storage = std::make_shared>(); + sourcemeta::core::JSON document{sourcemeta::core::JSON{nullptr}}; + + if (extension == ".yaml" || extension == ".yml") { + auto callback = make_position_callback(positions, property_storage); + sourcemeta::core::read_yaml(path, document, callback); + return {std::move(document), std::move(positions), + std::move(property_storage), true}; + } else if (extension == ".json") { + auto callback = make_position_callback(positions, property_storage); + sourcemeta::core::read_json(path, document, callback); + return {std::move(document), std::move(positions), + std::move(property_storage), false}; + } + + try { + auto callback = make_position_callback(positions, property_storage); + sourcemeta::core::read_json(path, document, callback); + return {std::move(document), std::move(positions), + std::move(property_storage), false}; + } catch (const sourcemeta::core::JSONParseError &) { + sourcemeta::core::PointerPositionTracker yaml_positions; + auto yaml_property_storage = std::make_shared>(); + auto callback = + make_position_callback(yaml_positions, yaml_property_storage); + sourcemeta::core::read_yaml(path, document, callback); + return {std::move(document), std::move(yaml_positions), + std::move(yaml_property_storage), true}; + } +} + +// Read stdin into a buffer and try JSON first, then YAML +inline auto read_from_stdin(std::string *raw_input = nullptr) -> ParsedJSON { + const auto input{sourcemeta::core::read_stdin()}; + if (raw_input != nullptr) { + *raw_input = input; + } + + try { + std::istringstream json_stream{input}; + sourcemeta::core::PointerPositionTracker positions; + auto property_storage = std::make_shared>(); + sourcemeta::core::JSON document{sourcemeta::core::JSON{nullptr}}; + auto callback = make_position_callback(positions, property_storage); + sourcemeta::core::parse_json(json_stream, document, callback); + return {std::move(document), std::move(positions), + std::move(property_storage), false}; + } catch (const sourcemeta::core::JSONParseError &json_error) { + try { + std::istringstream yaml_stream{input}; + sourcemeta::core::PointerPositionTracker positions; + auto property_storage = std::make_shared>(); + sourcemeta::core::JSON document{sourcemeta::core::JSON{nullptr}}; + auto callback = make_position_callback(positions, property_storage); + sourcemeta::core::parse_yaml(yaml_stream, document, callback); + return {std::move(document), std::move(positions), + std::move(property_storage), true}; + } catch (...) { + throw sourcemeta::core::JSONFileParseError(stdin_path(), json_error); + } + } +} + +inline auto +handle_json_entry(const std::filesystem::path &entry_path, + const std::set &blacklist, + const std::set &extensions, + std::vector &result, + const sourcemeta::core::Options &options) -> void { + if (entry_path == "-") { + auto parsed{read_from_stdin()}; + const auto path{stdin_path()}; + result.push_back({path.string(), path, std::move(parsed.document), + std::move(parsed.positions), 0, false, parsed.yaml, true, + std::move(parsed.property_storage)}); + return; + } + + if (std::filesystem::is_directory(entry_path)) { + for (auto const &entry : + std::filesystem::recursive_directory_iterator{entry_path}) { + auto canonical{sourcemeta::core::weakly_canonical(entry.path())}; + if (!std::filesystem::is_directory(entry) && + std::any_of(extensions.cbegin(), extensions.cend(), + [&canonical](const auto &extension) { + return extension.empty() + ? !canonical.has_extension() + : canonical.string().ends_with(extension); + }) && + std::none_of(blacklist.cbegin(), blacklist.cend(), + [&canonical](const auto &prefix) { + return sourcemeta::core::is_under_path(canonical, + prefix); + })) { + if (std::filesystem::is_empty(canonical)) { + continue; + } + + // TODO: Print a verbose message for what is getting parsed + auto parsed{read_file(canonical)}; + result.push_back({canonical.string(), std::move(canonical), + std::move(parsed.document), + std::move(parsed.positions), 0, false, parsed.yaml, + false, std::move(parsed.property_storage)}); + } + } + } else { + const auto canonical{sourcemeta::core::weakly_canonical(entry_path)}; + if (std::none_of(blacklist.cbegin(), blacklist.cend(), + [&canonical](const auto &prefix) { + return sourcemeta::core::is_under_path(canonical, + prefix); + })) { + const auto canonical_string{canonical.string()}; + if (canonical_string.ends_with(".jsonl.gz")) { + LOG_VERBOSE(options) << "Interpreting input as GZIP-compressed JSONL: " + << canonical_string << "\n"; + std::ifstream stream{sourcemeta::core::canonical(canonical), + std::ios::binary}; + stream.exceptions(std::ifstream::badbit); + std::size_t index{0}; + try { + for (const auto &document : sourcemeta::core::JSONL{ + stream, sourcemeta::core::JSONL::Mode::GZIP}) { + // TODO: Get real positions for JSONL + sourcemeta::core::PointerPositionTracker positions; + result.push_back({canonical.string(), + canonical, + document, + std::move(positions), + index, + true, + false, + false, + {}}); + index += 1; + } + } catch (const sourcemeta::core::GZIPError &error) { + throw sourcemeta::core::FileError( + canonical, error.what()); + } catch (const sourcemeta::core::JSONParseError &error) { + throw sourcemeta::core::JSONFileParseError(canonical, error); + } + + if (index == 0) { + LOG_WARNING() << "The JSONL file is empty\n"; + } + } else if (canonical.extension() == ".jsonl") { + LOG_VERBOSE(options) + << "Interpreting input as JSONL: " << canonical.string() << "\n"; + auto stream{sourcemeta::core::read_file(canonical)}; + std::size_t index{0}; + try { + for (const auto &document : sourcemeta::core::JSONL{stream}) { + // TODO: Get real positions for JSONL + sourcemeta::core::PointerPositionTracker positions; + result.push_back({canonical.string(), + canonical, + document, + std::move(positions), + index, + true, + false, + false, + {}}); + index += 1; + } + } catch (const sourcemeta::core::JSONParseError &error) { + throw sourcemeta::core::JSONFileParseError(canonical, error); + } + + if (index == 0) { + LOG_WARNING() << "The JSONL file is empty\n"; + } + } else if (canonical.extension() == ".yaml" || + canonical.extension() == ".yml") { + if (std::filesystem::is_empty(canonical)) { + return; + } + auto stream{sourcemeta::core::read_file(canonical)}; + struct MultiDocEntry { + sourcemeta::core::JSON document; + sourcemeta::core::PointerPositionTracker positions; + std::shared_ptr> property_storage; + }; + std::vector documents; + std::uint64_t line_offset{0}; + std::uint64_t max_line{0}; + while (stream.peek() != std::char_traits::eof()) { + sourcemeta::core::PointerPositionTracker positions; + auto property_storage = std::make_shared>(); + const std::uint64_t current_offset{line_offset}; + max_line = 0; + auto callback = + [&positions, &property_storage, current_offset, + &max_line](const sourcemeta::core::JSON::ParsePhase phase, + const sourcemeta::core::JSON::Type type, + const std::uint64_t line, const std::uint64_t column, + const sourcemeta::core::JSON::ParseContext context, + const std::size_t index, + const sourcemeta::core::JSON::String &property) { + max_line = std::max(max_line, line); + property_storage->emplace_back(property); + positions(phase, type, line + current_offset, column, context, + index, property_storage->back()); + }; + sourcemeta::core::JSON document{sourcemeta::core::JSON{nullptr}}; + sourcemeta::core::parse_yaml(stream, document, callback); + documents.push_back({std::move(document), std::move(positions), + std::move(property_storage)}); + line_offset += max_line > 0 ? max_line - 1 : 0; + } + + if (documents.size() > 1) { + LOG_VERBOSE(options) << "Interpreting input as YAML multi-document: " + << canonical.string() << "\n"; + std::size_t index{0}; + for (auto &entry : documents) { + result.push_back({canonical.string(), canonical, + std::move(entry.document), + std::move(entry.positions), index, true, true, + false, std::move(entry.property_storage)}); + index += 1; + } + } else if (documents.size() == 1) { + result.push_back({canonical.string(), std::move(canonical), + std::move(documents.front().document), + std::move(documents.front().positions), 0, false, + true, false, + std::move(documents.front().property_storage)}); + } + } else { + if (std::filesystem::is_regular_file(canonical) && + std::filesystem::is_empty(canonical)) { + return; + } + // TODO: Print a verbose message for what is getting parsed + auto parsed{read_file(canonical)}; + result.push_back({canonical.string(), std::move(canonical), + std::move(parsed.document), + std::move(parsed.positions), 0, false, parsed.yaml, + false, std::move(parsed.property_storage)}); + } + } + } +} + +} // namespace + +inline auto +check_no_duplicate_stdin(const std::vector &arguments) + -> void { + if (std::count(arguments.cbegin(), arguments.cend(), "-") > 1) { + throw StdinError("Cannot read from standard input more than once"); + } +} + +inline auto for_each_json(const std::vector &arguments, + const sourcemeta::core::Options &options) + -> std::vector { + check_no_duplicate_stdin(arguments); + + auto blacklist{parse_ignore(options)}; + std::vector result; + + if (arguments.empty()) { + const auto current_path{std::filesystem::current_path()}; + const auto configuration_path{find_configuration(current_path)}; + const auto &configuration{read_configuration(options, configuration_path)}; + + const auto &scan_path = configuration.has_value() + ? configuration.value().absolute_path + : current_path; + + if (!configuration_path.has_value()) { + LOG_WARNING() << "Recursively processing every file in " + << sourcemeta::core::weakly_canonical(current_path).string() + << " as no input was provided\n"; + } else if (configuration.has_value() && + !configuration.value().absolute_path_explicit) { + LOG_WARNING() + << "Recursively processing every file in " + << sourcemeta::core::weakly_canonical(scan_path).string() + << " as the configuration file does not set an explicit path\n"; + } + + if (configuration_path.has_value()) { + merge_configuration_ignore(configuration_path.value(), blacklist, + options); + } + + const auto extensions{parse_extensions(options, configuration)}; + + handle_json_entry(scan_path, blacklist, extensions, result, options); + std::sort(result.begin(), result.end(), + [](const auto &left, const auto &right) { return left < right; }); + } else { + std::unordered_set seen_configurations; + for (const auto &entry : arguments) { + // Skip stdin when looking for configurations + if (entry == "-") { + continue; + } + + const auto entry_path{ + sourcemeta::core::weakly_canonical(std::filesystem::path{entry})}; + const auto configuration_path{ + find_configuration(std::filesystem::is_directory(entry_path) + ? entry_path + : entry_path.parent_path())}; + if (configuration_path.has_value() && + seen_configurations.insert(configuration_path.value().string()) + .second) { + merge_configuration_ignore(configuration_path.value(), blacklist, + options); + } + } + + for (const auto &entry : arguments) { + std::optional entry_configuration_path{ + std::nullopt}; + if (entry != "-") { + const auto entry_path{ + sourcemeta::core::weakly_canonical(std::filesystem::path{entry})}; + entry_configuration_path = + find_configuration(std::filesystem::is_directory(entry_path) + ? entry_path + : entry_path.parent_path()); + } + const auto &entry_configuration{ + load_configuration(options, entry_configuration_path)}; + const auto &extensions{parse_extensions(options, entry_configuration)}; + const auto before{result.size()}; + handle_json_entry(entry, blacklist, extensions, result, options); + std::sort( + result.begin() + static_cast(before), result.end(), + [](const auto &left, const auto &right) { return left < right; }); + } + } + + return result; +} + +inline auto for_each_json(const sourcemeta::core::Options &options) + -> std::vector { + return for_each_json(options.positional(), options); +} + +} // namespace sourcemeta::jsonschema + +#endif diff --git a/vendor/jsonschema/src/logger.h b/vendor/jsonschema/src/logger.h new file mode 100644 index 000000000..b05710cc9 --- /dev/null +++ b/vendor/jsonschema/src/logger.h @@ -0,0 +1,40 @@ +#ifndef SOURCEMETA_JSONSCHEMA_CLI_LOGGER_H_ +#define SOURCEMETA_JSONSCHEMA_CLI_LOGGER_H_ + +#include + +#include // std::ofstream +#include // std::cerr +#include // std::ostream + +namespace sourcemeta::jsonschema { + +inline auto LOG_VERBOSE(const sourcemeta::core::Options &options) + -> std::ostream & { + if (options.contains("verbose") || options.contains("debug")) { + return std::cerr; + } + + static std::ofstream null_stream; + return null_stream; +} + +inline auto LOG_DEBUG(const sourcemeta::core::Options &options) + -> std::ostream & { + if (options.contains("debug")) { + std::cerr << "debug: "; + return std::cerr; + } + + static std::ofstream null_stream; + return null_stream; +} + +inline auto LOG_WARNING() -> std::ostream & { + std::cerr << "warning: "; + return std::cerr; +} + +} // namespace sourcemeta::jsonschema + +#endif diff --git a/vendor/jsonschema/src/main.cc b/vendor/jsonschema/src/main.cc new file mode 100644 index 000000000..bbab2381d --- /dev/null +++ b/vendor/jsonschema/src/main.cc @@ -0,0 +1,279 @@ +#include +#include + +#include // EXIT_SUCCESS +#include // std::filesystem +#include // std::print, std::println +#include // std::string +#include // std::string_view + +#include "command.h" +#include "configure.h" +#include "error.h" +#include "utils.h" + +constexpr std::string_view USAGE_DETAILS{R"EOF( +Global Options: + + --verbose, -v Enable verbose output + --debug, -g Enable even higher verbose output + --resolve, -r Import the given JSON Schema (or directory of schemas) + into the resolution context + --default-dialect, -d Specify the URI for the default dialect to be used + if the `$schema` keyword is not set + --json, -j Prefer JSON output if supported + --http, -h Allow network access to resolve remote schemas + --header, -H Send a custom HTTP header on every outgoing + request. May be passed multiple times + +Commands: + + version / --version / -v + + Print the current version of the JSON Schema CLI. + + help / --help / -h + + Print this command reference help. + + validate + [--benchmark/-b] [--loop ] [--extension/-e ] + [--ignore/-i ] [--trace/-t] [--fast/-f] + [--template/-m ] [--entrypoint/-p ] + [--continue/-c] [--format-assertion/-F] + + Validate one or more instances against the given schema. + + By default, schemas are validated in exhaustive mode, which results in + better error messages, at the expense of speed. The --fast/-f option + makes the schema compiler optimise for speed, at the expense of error + messages. Looping in benchmark mode allows to collect the execution time + average and standard deviation over multiple runs. + + You may additionally pass a pre-compiled schema template (see the + `compile` command). However, you still need to pass the original schema + for error reporting purposes. Make sure they match or you will get + non-sense results. + + metaschema [schemas-or-directories...] [--extension/-e ] + [--ignore/-i ] [--trace/-t] + [--format-assertion/-F] + + Validate that a schema or a set of schemas are valid with respect + to their metaschemas. + + compile [--extension/-e ] + [--ignore/-i ] [--fast/-f] [--minify/-m] + [--include/-n ] [--entrypoint/-p ] + [--format-assertion/-F] + + Compile the given schema into an internal optimised representation. + Use --include/-n to output as a C/C++ header file. + Use --entrypoint/-p to compile a subschema by JSON Pointer or URI. + + test [schemas-or-directories...] [--extension/-e ] + [--ignore/-i ] [--format-assertion/-F] + + Run a set of unit tests against a schema. + Pass --json/-j to output results in CTRF format (https://ctrf.io). + + fmt [schemas-or-directories...] [--check/-c] [--extension/-e ] + [--ignore/-i ] [--keep-ordering/-k] + [--indentation/-n ] + + Format the input schemas in-place or check they are formatted. + This command does not support YAML schemas yet. + + lint [schemas-or-directories...] [--fix/-f] [--format/-m] + [--keep-ordering/-k] [--extension/-e ] + [--ignore/-i ] [--exclude/-x ] + [--only/-o ] [--list/-l] [--indentation/-n ] + [--rule/-a ] [--format-assertion/-F] + + Lint the input schemas and potentially fix the reported issues. + The --fix/-f option is not supported when passing YAML schemas. + Use --format/-m with --fix to format the output even when there + are no linting issues. + Use --keep-ordering/-k with --format to preserve key order. + Use --list/-l to print a summary of all enabled rules. + Use --rule/-a to add a custom lint rule defined as a JSON Schema. + + upgrade + [--to/-t draft4|draft6|draft7|2019-09|2020-12] [--meta/-m] + + Upgrade the given schema to a newer JSON Schema dialect. + Defaults to the latest dialect (2020-12). Schemas that declare a + custom meta-schema cannot be upgraded by this command. + Pass --meta/-m as a hint when the input is a meta-schema + (not auto-detectable on Draft 7 and older dialects). + + bundle [--extension/-e ] + [--ignore/-i ] [--without-id/-w] + + Perform JSON Schema Bundling on a schema to inline remote references, + printing the result to standard output. + + inspect + + Statically inspect a schema to display schema locations and + references in a human-readable manner. + + codegen --target/-t [--name/-n ] + + Generate code from a JSON Schema. Currently only supports + --target/-t set to typescript and JSON Schema 2020-12. + + encode + + Encode a JSON document or JSONL dataset using JSON BinPack. + + decode + + Decode a JSON document or JSONL dataset using JSON BinPack. + + install [ ] [--force/-f] [--frozen/-z] + + Fetch and install external schema dependencies declared in + jsonschema.json. Pass a URI and a local file path to add a new + dependency before installing. Pass --force/-f to re-fetch all + dependencies regardless of lock state. Pass --frozen to strictly + verify dependencies against the lock file without modifying it, + intended for CI/CD environments. + +For more documentation, visit https://github.com/sourcemeta/jsonschema +)EOF"}; + +auto jsonschema_main(const std::string &program, const std::string &command, + sourcemeta::core::Options &app, int argc, char *argv[]) + -> int { + if (command == "fmt") { + app.flag("check", {"c"}); + app.flag("keep-ordering", {"k"}); + app.option("extension", {"e"}); + app.option("ignore", {"i"}); + app.option("indentation", {"n"}); + app.parse(argc, argv, {.skip = 1}); + sourcemeta::jsonschema::fmt(app); + return EXIT_SUCCESS; + } else if (command == "inspect") { + app.parse(argc, argv, {.skip = 1}); + sourcemeta::jsonschema::inspect(app); + return EXIT_SUCCESS; + } else if (command == "bundle") { + app.flag("without-id", {"w"}); + app.option("extension", {"e"}); + app.option("ignore", {"i"}); + app.parse(argc, argv, {.skip = 1}); + sourcemeta::jsonschema::bundle(app); + return EXIT_SUCCESS; + } else if (command == "lint") { + app.flag("fix", {"f"}); + app.flag("format", {"m"}); + app.flag("format-assertion", {"F"}); + app.flag("keep-ordering", {"k"}); + app.flag("list", {"l"}); + app.option("extension", {"e"}); + app.option("exclude", {"x"}); + app.option("only", {"o"}); + app.option("ignore", {"i"}); + app.option("indentation", {"n"}); + app.option("rule", {"a"}); + app.parse(argc, argv, {.skip = 1}); + sourcemeta::jsonschema::lint(app); + return EXIT_SUCCESS; + } else if (command == "validate") { + app.flag("benchmark", {"b"}); + app.flag("trace", {"t"}); + app.flag("fast", {"f"}); + app.flag("format-assertion", {"F"}); + app.flag("continue", {"c"}); + app.option("extension", {"e"}); + app.option("ignore", {"i"}); + app.option("template", {"m"}); + app.option("loop", {"l"}); + app.option("entrypoint", {"p"}); + app.parse(argc, argv, {.skip = 1}); + sourcemeta::jsonschema::validate(app); + return EXIT_SUCCESS; + } else if (command == "metaschema") { + app.flag("trace", {"t"}); + app.flag("format-assertion", {"F"}); + app.option("extension", {"e"}); + app.option("ignore", {"i"}); + app.parse(argc, argv, {.skip = 1}); + sourcemeta::jsonschema::metaschema(app); + return EXIT_SUCCESS; + } else if (command == "compile") { + app.flag("fast", {"f"}); + app.flag("format-assertion", {"F"}); + app.flag("minify", {"m"}); + app.option("include", {"n"}); + app.option("entrypoint", {"p"}); + app.parse(argc, argv, {.skip = 1}); + sourcemeta::jsonschema::compile(app); + return EXIT_SUCCESS; + } else if (command == "test") { + app.flag("format-assertion", {"F"}); + app.option("extension", {"e"}); + app.option("ignore", {"i"}); + app.parse(argc, argv, {.skip = 1}); + sourcemeta::jsonschema::test(app); + return EXIT_SUCCESS; + } else if (command == "encode") { + app.parse(argc, argv, {.skip = 1}); + sourcemeta::jsonschema::encode(app); + return EXIT_SUCCESS; + } else if (command == "decode") { + app.parse(argc, argv, {.skip = 1}); + sourcemeta::jsonschema::decode(app); + return EXIT_SUCCESS; + } else if (command == "codegen") { + app.option("name", {"n"}); + app.option("target", {"t"}); + app.parse(argc, argv, {.skip = 1}); + sourcemeta::jsonschema::codegen(app); + return EXIT_SUCCESS; + } else if (command == "install") { + app.flag("force", {"f"}); + app.flag("frozen", {"z"}); + app.parse(argc, argv, {.skip = 1}); + sourcemeta::jsonschema::install(app); + return EXIT_SUCCESS; + } else if (command == "upgrade") { + app.option("to", {"t"}); + app.flag("meta", {"m"}); + app.parse(argc, argv, {.skip = 1}); + sourcemeta::jsonschema::upgrade(app); + return EXIT_SUCCESS; + } else if (command == "help" || command == "--help" || command == "-h") { + std::println("JSON Schema CLI - v{}", + sourcemeta::jsonschema::PROJECT_VERSION); + std::println("Usage: {} [arguments...]", + std::filesystem::path{program}.filename().string()); + std::print("{}", USAGE_DETAILS); + return EXIT_SUCCESS; + } else if (command == "version" || command == "--version" || + command == "-v") { + std::println("{}", sourcemeta::jsonschema::PROJECT_VERSION); + return EXIT_SUCCESS; + } else { + throw sourcemeta::jsonschema::UnknownCommandError{command}; + } +} + +auto main(int argc, char *argv[]) noexcept -> int { + sourcemeta::core::Options app; + app.flag("http", {"h"}); + app.flag("verbose", {"v"}); + app.flag("debug", {"g"}); + app.flag("json", {"j"}); + app.option("resolve", {"r"}); + app.option("default-dialect", {"d"}); + app.option("header", {"H"}); + + return sourcemeta::jsonschema::try_catch(app, [&app, argc, &argv]() { + const std::string program{argv[0]}; + const std::string command{argc > 1 ? argv[1] : "help"}; + return jsonschema_main(program, command, app, argc, argv); + }); +} diff --git a/vendor/jsonschema/src/resolver.h b/vendor/jsonschema/src/resolver.h new file mode 100644 index 000000000..c5684bd1a --- /dev/null +++ b/vendor/jsonschema/src/resolver.h @@ -0,0 +1,468 @@ +#ifndef SOURCEMETA_JSONSCHEMA_CLI_RESOLVER_H_ +#define SOURCEMETA_JSONSCHEMA_CLI_RESOLVER_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "error.h" +#include "input.h" +#include "logger.h" +#include "utils.h" + +#include // assert +#include // std::chrono::seconds +#include // std::uint8_t +#include // std::filesystem +#include // std::function, std::ref +#include // std::cerr +#include // std::map +#include // std::optional +#include // std::string +#include // std::string_view +#include // std::this_thread::sleep_for +#include // std::pair, std::piecewise_construct, std::forward_as_tuple +#include // std::vector + +namespace sourcemeta::jsonschema { + +static constexpr std::uint8_t HTTP_MAXIMUM_RETRIES{3}; + +static inline auto find_resolve_match( + const std::unordered_map &resolve_map, + const std::string &identifier) + -> std::unordered_map::const_iterator { + auto match{resolve_map.find(identifier)}; + if (match == resolve_map.cend() && !identifier.ends_with(".json")) { + match = resolve_map.find(identifier + ".json"); + } + if (match == resolve_map.cend() && identifier.ends_with(".json")) { + match = resolve_map.find(identifier.substr(0, identifier.size() - 5)); + } + return match; +} + +static inline auto +resolve_map_uri(const sourcemeta::blaze::Configuration &configuration, + const std::string &identifier) -> std::optional { + const auto match{find_resolve_match(configuration.resolve, identifier)}; + if (match == configuration.resolve.cend()) { + return std::nullopt; + } + + return resolve_relative_uri(match->second, configuration.base_path); +} + +static constexpr std::string_view HTTP_HEADER_EXAMPLE{ + "--header \"Authorization: Bearer ${TOKEN}\""}; + +static inline auto parse_http_header(const std::string_view input) + -> std::pair { + const auto colon{input.find(':')}; + if (colon == std::string_view::npos) { + throw PositionalArgumentError{ + "HTTP headers must be in the form `Name: Value`", + std::string{HTTP_HEADER_EXAMPLE}}; + } + + const auto raw_name{input.substr(0, colon)}; + if (raw_name.empty()) { + throw PositionalArgumentError{"HTTP header names cannot be empty", + std::string{HTTP_HEADER_EXAMPLE}}; + } + + for (const auto character : raw_name) { + if (character == ' ' || character == '\t') { + throw PositionalArgumentError{ + "HTTP header names cannot contain whitespace", + std::string{HTTP_HEADER_EXAMPLE}}; + } + if (static_cast(character) < 0x20 || + static_cast(character) == 0x7F) { + throw PositionalArgumentError{ + "HTTP header names cannot contain control characters", + std::string{HTTP_HEADER_EXAMPLE}}; + } + } + + auto raw_value{input.substr(colon + 1)}; + while (!raw_value.empty() && + (raw_value.front() == ' ' || raw_value.front() == '\t')) { + raw_value.remove_prefix(1); + } + + for (const auto character : raw_value) { + if (character == '\r' || character == '\n' || character == '\0') { + throw PositionalArgumentError{ + "HTTP header values cannot contain control characters", + std::string{HTTP_HEADER_EXAMPLE}}; + } + } + + return {raw_name, raw_value}; +} + +static inline auto +validate_http_headers(const sourcemeta::core::Options &options) -> void { + if (!options.contains("header")) { + return; + } + for (const auto &raw : options.at("header")) { + parse_http_header(raw); + } +} + +static inline auto +collect_http_headers(const sourcemeta::core::Options &options) + -> std::vector> { + std::vector> headers; + if (!options.contains("header")) { + return headers; + } + for (const auto &raw : options.at("header")) { + headers.emplace_back(parse_http_header(raw)); + } + return headers; +} + +static inline auto http_fetch(const std::string &url, + const sourcemeta::core::Options &options) + -> sourcemeta::core::JSON { + sourcemeta::core::HTTPSystemRequest request{url}; + for (const auto &header : collect_http_headers(options)) { + request.header(std::string{header.first}, std::string{header.second}); + } + + sourcemeta::core::HTTPResponse response; + for (std::uint8_t attempt{1}; attempt <= HTTP_MAXIMUM_RETRIES; ++attempt) { + LOG_VERBOSE(options) << "Resolving over HTTP (attempt " + << static_cast(attempt) << "/" + << static_cast(HTTP_MAXIMUM_RETRIES) + << "): " << url << "\n"; + try { + response = request.send(); + } catch (const sourcemeta::core::HTTPError &error) { + if (attempt == HTTP_MAXIMUM_RETRIES) { + throw; + } + + LOG_VERBOSE(options) << "Request failed (" << error.what() + << "), retrying...\n"; + std::this_thread::sleep_for(std::chrono::seconds(1)); + continue; + } + + if (response.status == sourcemeta::core::HTTP_STATUS_OK) { + break; + } + + if (attempt < HTTP_MAXIMUM_RETRIES) { + LOG_VERBOSE(options) << "Request failed with HTTP " + << response.status.code << ", retrying...\n"; + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + } + + if (response.status != sourcemeta::core::HTTP_STATUS_OK) { + throw sourcemeta::core::HTTPStatusError{sourcemeta::core::HTTPMethod::GET, + url, response.status}; + } + + const auto content_type{ + sourcemeta::core::http_header_find(response.headers, "content-type")}; + if (content_type.has_value() && sourcemeta::core::http_content_type_matches( + content_type.value(), "text/yaml")) { + return sourcemeta::core::parse_yaml(response.body); + } + + return sourcemeta::core::parse_json(response.body); +} + +static inline auto fetch_schema(const sourcemeta::core::Options &options, + std::string_view identifier, + const bool remote = true, + const bool bundle = false) + -> std::optional { + auto official_result{sourcemeta::blaze::schema_resolver(identifier)}; + if (official_result.has_value()) { + return official_result; + } + + sourcemeta::core::URI uri; + try { + uri = sourcemeta::core::URI{identifier}; + } catch (const sourcemeta::core::URIParseError &) { + return std::nullopt; + } + + if (uri.is_file()) { + const auto path{uri.to_path()}; + LOG_DEBUG(options) << "Attempting to read file reference from disk: " + << path.string() << "\n"; + if (std::filesystem::exists(path)) { + return sourcemeta::core::read_yaml_or_json(path); + } + + return std::nullopt; + } + + if (remote) { + const auto scheme{uri.scheme()}; + if (!uri.is_urn() && scheme.has_value() && + (scheme.value() == "https" || scheme.value() == "http")) { + std::string fetch_url{identifier}; + if (bundle) { + // TODO: Use sourcemeta::core::URI to set query parameters once + // the URI module supports setters for query strings + if (fetch_url.find('?') != std::string::npos) { + fetch_url += "&bundle=1"; + } else { + fetch_url += "?bundle=1"; + } + } + + return http_fetch(fetch_url, options); + } + } + + return std::nullopt; +} + +static inline auto +ensure_identifier(sourcemeta::core::JSON &schema, const std::string_view target, + const sourcemeta::blaze::SchemaResolver &resolver) -> void { + if (!sourcemeta::blaze::is_schema(schema) || !schema.is_object()) { + return; + } + + const auto resolved_base_dialect{ + sourcemeta::blaze::base_dialect(schema, resolver, "")}; + if (!resolved_base_dialect.has_value()) { + return; + } + + if (!sourcemeta::blaze::identify(schema, resolved_base_dialect.value()) + .empty()) { + return; + } + + sourcemeta::blaze::reidentify(schema, target, resolved_base_dialect.value()); +} + +class CustomResolver { +public: + CustomResolver( + const sourcemeta::core::Options &options, + const std::optional &configuration, + const bool remote, const std::string_view default_dialect) + : options_{options}, configuration_{configuration}, remote_{remote} { + if (options.contains("resolve")) { + for (const auto &entry : for_each_json(options.at("resolve"), options)) { + LOG_DEBUG(options) << "Detecting schema resources from file: " + << entry.first << "\n"; + + if (!sourcemeta::blaze::is_schema(entry.second)) { + throw sourcemeta::core::FileError( + entry.resolution_base, + "The file you provided does not represent a valid JSON Schema"); + } + + try { + const auto result = this->add( + entry.second, default_dialect, + sourcemeta::jsonschema::default_id(entry), + [&options](const auto &identifier) { + LOG_DEBUG(options) + << "Importing schema into the resolution context: " + << identifier << "\n"; + }); + if (!result) { + LOG_WARNING() + << "No schema resources were imported from this file\n" + << " at " << entry.first << "\n" + << "Are you sure this schema sets any identifiers?\n"; + } + } catch (const sourcemeta::blaze::SchemaKeywordError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaKeywordError>(entry.resolution_base, + error); + } catch (const sourcemeta::blaze::SchemaFrameError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaFrameError>( + entry.resolution_base, error.identifier(), error.what()); + } catch (const sourcemeta::blaze::SchemaAnchorCollisionError &error) { + const auto position{entry.positions.get(error.location())}; + if (position.has_value()) { + throw PositionError>( + std::get<0>(position.value()), std::get<1>(position.value()), + entry.resolution_base, error); + } + + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaAnchorCollisionError>( + entry.resolution_base, error); + } catch (const sourcemeta::blaze::SchemaReferenceError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaReferenceError>( + entry.resolution_base, error.identifier(), error.location(), + error.what()); + } catch (const sourcemeta::blaze::SchemaUnknownBaseDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownBaseDialectError>( + entry.resolution_base); + } catch (const sourcemeta::blaze::SchemaUnknownDialectError &) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaUnknownDialectError>( + entry.resolution_base); + } catch ( + const sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError + &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaRelativeMetaschemaResolutionError>( + entry.resolution_base, error); + } catch (const sourcemeta::blaze::SchemaResolutionError &error) { + throw sourcemeta::core::FileError< + sourcemeta::blaze::SchemaResolutionError>( + entry.resolution_base, error.identifier(), error.what()); + } catch (const sourcemeta::blaze::SchemaError &error) { + throw sourcemeta::core::FileError( + entry.resolution_base, error.what()); + } + } + } + + if (this->configuration_.has_value()) { + for (const auto &[dependency_uri, dependency_path] : + this->configuration_.value().dependencies) { + if (!std::filesystem::exists(dependency_path)) { + continue; + } + + auto schema{sourcemeta::core::read_json(dependency_path)}; + if (!sourcemeta::blaze::is_schema(schema)) { + continue; + } + + try { + this->add(schema, default_dialect); + } catch (...) { + continue; + } + + this->schemas.emplace(dependency_uri, schema); + } + } + } + + auto add(const sourcemeta::core::JSON &schema, + const std::string_view default_dialect = "", + const std::string_view default_id = "", + const std::function + &callback = nullptr) -> bool { + assert(sourcemeta::blaze::is_schema(schema)); + + // Registering the top-level schema is not enough. We need to check + // and register every embedded schema resource too + sourcemeta::blaze::SchemaFrame frame{ + sourcemeta::blaze::SchemaFrame::Mode::References}; + frame.analyse(schema, sourcemeta::blaze::schema_walker, *this, + default_dialect, default_id); + + bool added_any_schema{false}; + for (const auto &[key, entry] : frame.locations()) { + if (entry.type != + sourcemeta::blaze::SchemaFrame::LocationType::Resource) { + continue; + } + + auto subschema{sourcemeta::core::get(schema, entry.pointer)}; + const auto subschema_vocabularies{frame.vocabularies(entry, *this)}; + + // Given we might be resolving embedded resources, we fully + // resolve their dialect and identifiers, otherwise the + // consumer might have no idea what to do with them + subschema.assign("$schema", sourcemeta::core::JSON{entry.dialect}); + sourcemeta::blaze::reidentify(subschema, key.second, entry.base_dialect); + + const auto result{this->schemas.emplace(key.second, subschema)}; + if (!result.second && result.first->second != subschema) { + throw sourcemeta::blaze::SchemaFrameError( + key.second, "Cannot register the same identifier twice"); + } + + if (callback) { + callback(key.second); + } + + added_any_schema = true; + } + + return added_any_schema; + } + + auto operator()(std::string_view identifier) const + -> std::optional { + const std::string string_identifier{identifier}; + const auto mapped_result = this->configuration_.and_then( + [&string_identifier](const sourcemeta::blaze::Configuration &config) + -> std::optional { + return resolve_map_uri(config, string_identifier); + }); + const std::string &target{mapped_result.has_value() ? mapped_result.value() + : string_identifier}; + if (mapped_result.has_value()) { + LOG_DEBUG(this->options_) << "Resolving " << identifier << " as " + << target << " given the configuration file\n"; + } + + if (this->schemas.contains(target)) { + return this->schemas.at(target); + } + + auto fetched{fetch_schema(this->options_, target, this->remote_)}; + if (fetched.has_value()) { + ensure_identifier(fetched.value(), string_identifier, *this); + } + + return fetched; + } + +private: + std::map schemas{}; + const sourcemeta::core::Options &options_; + const std::optional configuration_; + bool remote_{false}; +}; + +inline auto +resolver(const sourcemeta::core::Options &options, const bool remote, + const std::string_view default_dialect, + const std::optional &configuration) + -> const CustomResolver & { + using CacheKey = std::pair; + static std::map resolver_cache; + const CacheKey cache_key{remote, std::string{default_dialect}}; + + // Check if resolver is already cached + auto iterator{resolver_cache.find(cache_key)}; + if (iterator != resolver_cache.end()) { + return iterator->second; + } + + // Construct resolver directly in cache + auto [inserted_iterator, inserted] = resolver_cache.emplace( + std::piecewise_construct, std::forward_as_tuple(cache_key), + std::forward_as_tuple(options, configuration, remote, default_dialect)); + return inserted_iterator->second; +} + +} // namespace sourcemeta::jsonschema + +#endif diff --git a/vendor/jsonschema/src/utils.h b/vendor/jsonschema/src/utils.h new file mode 100644 index 000000000..ea1b47095 --- /dev/null +++ b/vendor/jsonschema/src/utils.h @@ -0,0 +1,299 @@ +#ifndef SOURCEMETA_JSONSCHEMA_CLI_UTILS_H_ +#define SOURCEMETA_JSONSCHEMA_CLI_UTILS_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "error.h" +#include "input.h" + +#include // std::filesystem::path +#include // std::make_shared +#include // std::optional +#include // std::ostream +#include // std::set +#include // std::string, std::stoull +#include // std::string_view +#include // std::unreachable +#include // std::visit + +namespace sourcemeta::jsonschema { + +inline auto default_id(const std::filesystem::path &schema_path) + -> std::string { + return sourcemeta::core::URI::from_path( + sourcemeta::core::weakly_canonical(schema_path)) + .recompose(); +} + +inline auto resolve_relative_uri(const std::string &value, + const std::filesystem::path &base, + const std::set &extensions = {}) + -> std::string { + const sourcemeta::core::URI uri{value}; + if (!uri.is_relative()) { + return value; + } + + const auto canonical{ + sourcemeta::core::weakly_canonical(base / uri.to_path())}; + + if (!extensions.empty() && !std::filesystem::is_regular_file(canonical)) { + for (const auto &extension : extensions) { + if (extension.empty()) { + continue; + } + + std::filesystem::path candidate{canonical}; + candidate += extension; + if (std::filesystem::is_regular_file(candidate)) { + return sourcemeta::core::URI::from_path(candidate).recompose(); + } + } + } + + return sourcemeta::core::URI::from_path(canonical).recompose(); +} + +inline auto default_id(const InputJSON &entry) -> std::string { + return default_id(entry.resolution_base); +} + +inline auto resolve_entrypoint(const sourcemeta::blaze::SchemaFrame &frame, + const std::string_view entrypoint) + -> std::string { + if (entrypoint.empty()) { + return std::string{frame.root()}; + } + + if (entrypoint.front() == '/' && + (entrypoint.size() < 2 || entrypoint[1] != '/')) { + sourcemeta::core::URI result{frame.root()}; + result.fragment(entrypoint); + return result.recompose(); + } + + if (entrypoint.front() == '#') { + const std::string pointer_string{entrypoint.substr(1)}; + sourcemeta::core::URI result{frame.root()}; + result.fragment(pointer_string); + return result.recompose(); + } + + try { + const sourcemeta::core::URI uri{entrypoint}; + return std::string{entrypoint}; + } catch (const sourcemeta::core::URIParseError &) { + throw sourcemeta::blaze::CompilerInvalidEntryPoint{ + entrypoint, "The given entry point is not a valid URI or JSON Pointer"}; + } +} + +constexpr std::string_view TEST_DOCUMENT_DEFAULT_DIALECT{ + "https://json-schema.org/draft/2020-12/schema"}; + +inline auto looks_like_test_document(const sourcemeta::core::JSON &document) + -> bool { + return document.is_object() && !document.defines("$schema") && + document.defines("target") && document.at("target").is_string() && + document.defines("tests") && document.at("tests").is_array(); +} + +inline auto default_dialect( + const sourcemeta::core::Options &options, + const std::optional &configuration) + -> std::string { + if (options.contains("default-dialect")) { + std::string value{options.at("default-dialect").front()}; + try { + const sourcemeta::core::URI uri{value}; + if (!uri.is_relative()) { + return value; + } + } catch (const sourcemeta::core::URIParseError &) { + throw InvalidDefaultDialectError{std::move(value)}; + } + + return resolve_relative_uri(value, std::filesystem::current_path(), + parse_extensions(options, configuration)); + } + + const auto from_config = configuration.and_then( + [](const sourcemeta::blaze::Configuration &config) + -> std::optional { return config.default_dialect; }); + if (from_config.has_value()) { + const sourcemeta::core::URI uri{from_config.value()}; + if (!uri.is_relative()) { + return from_config.value(); + } + + return resolve_relative_uri(from_config.value(), + configuration.value().base_path, + parse_extensions(options, configuration)); + } + + return ""; +} + +inline auto parse_indentation(const sourcemeta::core::Options &options) + -> std::size_t { + if (options.contains("indentation")) { + return std::stoull(std::string{options.at("indentation").front()}); + } + + return 2; +} + +inline auto format_assertion_tweaks(const sourcemeta::core::Options &options) + -> std::optional { + if (options.contains("format-assertion")) { + return sourcemeta::blaze::Tweaks{.format_assertion = true}; + } + + return std::nullopt; +} + +inline auto print(const sourcemeta::blaze::SimpleOutput &output, + const sourcemeta::core::PointerPositionTracker &tracker, + std::ostream &stream) -> void { + stream << "error: Schema validation failure\n"; + for (const auto &entry : output) { + stream << " " << entry.message << "\n"; + stream << " at instance location \""; + sourcemeta::core::stringify(entry.instance_location, stream); + stream << "\""; + + const auto position{ + tracker.get(sourcemeta::core::to_pointer(entry.instance_location))}; + if (position.has_value()) { + const auto [line, column, end_line, end_column] = position.value(); + stream << " (line " << line << ", column " << column << ")"; + } + + stream << "\n"; + stream << " at evaluate path \""; + sourcemeta::core::stringify(entry.evaluate_path, stream); + stream << "\"\n"; + } +} + +inline auto +print_annotations(const sourcemeta::blaze::SimpleOutput &output, + const sourcemeta::core::Options &options, + const sourcemeta::core::PointerPositionTracker &tracker, + std::ostream &stream) -> void { + if (options.contains("verbose")) { + for (const auto &annotation : output.annotations()) { + for (const auto &value : annotation.second) { + stream << "annotation: "; + sourcemeta::core::stringify(value, stream); + stream << "\n at instance location \""; + sourcemeta::core::stringify(annotation.first.instance_location, stream); + stream << "\""; + + const auto position{tracker.get( + sourcemeta::core::to_pointer(annotation.first.instance_location))}; + if (position.has_value()) { + const auto [line, column, end_line, end_column] = position.value(); + stream << " (line " << line << ", column " << column << ")"; + } + + stream << "\n at evaluate path \""; + sourcemeta::core::stringify(annotation.first.evaluate_path, stream); + stream << "\"\n"; + } + } + } +} + +inline auto +trace_callback(const sourcemeta::core::PointerPositionTracker &tracker, + std::ostream &stream) + -> sourcemeta::blaze::TraceOutput::Callback { + auto first = std::make_shared(true); + return [&tracker, &stream, + first](const sourcemeta::blaze::TraceOutput::Entry &entry) -> void { + if (entry.evaluate_path.empty()) { + return; + } + + // To make it easier to read + if (*first) { + *first = false; + } else { + stream << "\n"; + } + + switch (entry.type) { + case sourcemeta::blaze::TraceOutput::EntryType::Push: + stream << "-> (push) "; + break; + case sourcemeta::blaze::TraceOutput::EntryType::Pass: + stream << "<- (pass) "; + break; + case sourcemeta::blaze::TraceOutput::EntryType::Fail: + stream << "<- (fail) "; + break; + case sourcemeta::blaze::TraceOutput::EntryType::Annotation: + stream << "@- (annotation) "; + break; + default: + std::unreachable(); + } + + stream << "\""; + sourcemeta::core::stringify(entry.evaluate_path, stream); + stream << "\""; + stream << " (" << entry.name << ")\n"; + + if (!entry.annotation.is_null()) { + stream << " value "; + + if (entry.annotation.is_object()) { + sourcemeta::core::stringify(entry.annotation, stream); + } else { + sourcemeta::core::prettify(entry.annotation, stream); + } + + stream << "\n"; + } + + stream << " at instance location \""; + sourcemeta::core::stringify(entry.instance_location, stream); + stream << "\""; + + const auto position{ + tracker.get(sourcemeta::core::to_pointer(entry.instance_location))}; + if (position.has_value()) { + const auto [line, column, end_line, end_column] = position.value(); + stream << " (line " << line << ", column " << column << ")"; + } + + stream << "\n"; + stream << " at keyword location \"" << entry.keyword_location << "\"\n"; + + if (entry.vocabulary.first) { + stream << " at vocabulary \""; + if (entry.vocabulary.second.has_value()) { + std::visit([&stream](const auto &vocabulary) { stream << vocabulary; }, + entry.vocabulary.second.value()); + } else { + stream << ""; + } + stream << "\"\n"; + } + }; +} + +} // namespace sourcemeta::jsonschema + +#endif