From 823ab6db18e4705b292ded4aea28e239742eb6c3 Mon Sep 17 00:00:00 2001 From: opsocket Date: Mon, 30 Jan 2023 18:49:22 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20pull=20from=20upstream?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/github-actions.yml | 49 +++- CMakeLists.txt | 89 ++++--- README.md | 2 +- conanfile.py | 36 ++- nlohmann_json_schema_validatorConfig.cmake.in | 3 +- src/json-patch.cpp | 6 +- src/json-validator.cpp | 90 +++++-- src/nlohmann/json-schema.hpp | 4 +- src/string-format-check.cpp | 244 +++++++++++------- test/CMakeLists.txt | 4 + test/JSON-Schema-Test-Suite/CMakeLists.txt | 2 - test/binary-validation.cpp | 2 +- test/issue-98.cpp | 3 +- test/string-format-check-test.cpp | 16 ++ test/uri.cpp | 4 +- 15 files changed, 380 insertions(+), 174 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 5a7d96d..d5d5c42 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -6,12 +6,13 @@ on: - develop - master - release/* + - main pull_request: jobs: build_and_test: runs-on: ubuntu-latest - container: ghcr.io/nlohmann/json-ci:v1.0.0 + container: ghcr.io/nlohmann/json-ci:v2.4.0 steps: - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub." @@ -21,7 +22,7 @@ jobs: with: repository: nlohmann/json path: nlohmann-json - ref: release/3.10.2 + ref: v3.11.2 - name: Build and install nlohmann json run: | cd nlohmann-json @@ -36,3 +37,47 @@ jobs: run: cmake --build build --target all -j$(nproc) - name: test run: cd build && ctest + build_and_test_min_version: + runs-on: ubuntu-latest + container: ghcr.io/nlohmann/json-ci:v2.4.0 + steps: + - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub." + - run: echo "🔎 Branch name is ${{ github.ref }} and repository is ${{ github.repository }}." + - name: Clone nlohmann json + uses: actions/checkout@master + with: + repository: nlohmann/json + path: nlohmann-json + ref: v3.8.0 + - name: Build and install nlohmann json + run: | + cd nlohmann-json + cmake -S . -B build + cmake --build build --target install -j$(nproc) + cd .. + - name: Clone json-schema-validator + uses: actions/checkout@v2 + - name: cmake + run: cmake -S . -B build + - name: build + run: cmake --build build --target all -j$(nproc) + - name: test + run: cd build && ctest + build_conan: + runs-on: ubuntu-latest + container: ghcr.io/nlohmann/json-ci:v2.4.0 + steps: + - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub." + - run: echo "🔎 Branch name is ${{ github.ref }} and repository is ${{ github.repository }}." + - name: Clone json-schema-validator + uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + - run: python -m pip install --upgrade conan + - run: conan config init + - run: conan profile update settings.compiler.libcxx=libstdc++11 default + - name: conan create package + run: conan create . diff --git a/CMakeLists.txt b/CMakeLists.txt index e6aff61..f636734 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,6 @@ cmake_minimum_required(VERSION 3.2) -option(JSON_VALIDATOR_BUILD_TESTS "Build tests" ON) -option(JSON_VALIDATOR_BUILD_EXAMPLES "Build examples" ON) +option(JSON_VALIDATOR_INSTALL "Install target" ON) option(JSON_VALIDATOR_HUNTER "Enable Hunter package manager support" OFF) if(JSON_VALIDATOR_HUNTER) @@ -16,7 +15,7 @@ endif() project(nlohmann_json_schema_validator LANGUAGES CXX) -set(PROJECT_VERSION 2.1.1) +set(PROJECT_VERSION 2.2.0) if(JSON_VALIDATOR_HUNTER) hunter_add_package(nlohmann_json) @@ -24,6 +23,7 @@ endif() # the library add_library(nlohmann_json_schema_validator + src/smtp-address-validator.cpp src/json-schema-draft7.json.cpp src/json-uri.cpp src/json-validator.cpp @@ -42,21 +42,16 @@ target_compile_features(nlohmann_json_schema_validator set_target_properties(nlohmann_json_schema_validator PROPERTIES VERSION ${PROJECT_VERSION} - SOVERSION 1) + SOVERSION 2) # disable tests and examples if project is not super project if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) # I am top-level project. - set(JSON_VALIDATOR_IS_TOP_LEVEL TRUE) + set(JSON_VALIDATOR_IS_TOP_LEVEL ON) endif() -if(JSON_VALIDATOR_IS_TOP_LEVEL) - set(JSON_VALIDATOR_BUILD_TESTS ON) - set(JSON_VALIDATOR_BUILD_EXAMPLES ON) -else() - set(JSON_VALIDATOR_BUILD_TESTS OFF) - set(JSON_VALIDATOR_BUILD_EXAMPLES OFF) -endif() +option(JSON_VALIDATOR_BUILD_TESTS "Build tests" ${JSON_VALIDATOR_IS_TOP_LEVEL}) +option(JSON_VALIDATOR_BUILD_EXAMPLES "Build examples" ${JSON_VALIDATOR_IS_TOP_LEVEL}) if(NOT TARGET nlohmann_json::nlohmann_json) find_package(nlohmann_json REQUIRED) @@ -95,14 +90,16 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") endif() endif() -install(TARGETS nlohmann_json_schema_validator - EXPORT ${PROJECT_NAME}Targets - LIBRARY DESTINATION lib - ARCHIVE DESTINATION lib - RUNTIME DESTINATION bin) +if(JSON_VALIDATOR_INSTALL) + install(TARGETS nlohmann_json_schema_validator + EXPORT ${PROJECT_NAME}Targets + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + RUNTIME DESTINATION bin) -install(FILES src/nlohmann/json-schema.hpp - DESTINATION include/nlohmann) + install(FILES src/nlohmann/json-schema.hpp + DESTINATION include/nlohmann) +endif() if (JSON_VALIDATOR_BUILD_EXAMPLES) # simple nlohmann_json_schema_validator-executable @@ -127,32 +124,34 @@ endif() # Set Up the Project Targets and Config Files for CMake -# Set the install path to the cmake config files (Relative, so install works correctly under Hunter as well) -set(INSTALL_CMAKE_DIR "lib/cmake/${PROJECT_NAME}") -set(INSTALL_CMAKEDIR_ROOT share/cmake) +if(JSON_VALIDATOR_INSTALL) + # Set the install path to the cmake config files (Relative, so install works correctly under Hunter as well) + set(INSTALL_CMAKE_DIR "lib/cmake/${PROJECT_NAME}") + set(INSTALL_CMAKEDIR_ROOT share/cmake) -# Install Targets -install(EXPORT ${PROJECT_NAME}Targets - FILE ${PROJECT_NAME}Targets.cmake - DESTINATION "${INSTALL_CMAKE_DIR}") + # Install Targets + install(EXPORT ${PROJECT_NAME}Targets + FILE ${PROJECT_NAME}Targets.cmake + DESTINATION "${INSTALL_CMAKE_DIR}") -include(CMakePackageConfigHelpers) -write_basic_package_version_file( - ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion - ) - -configure_package_config_file( - ${PROJECT_SOURCE_DIR}/${PROJECT_NAME}Config.cmake.in - ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake - INSTALL_DESTINATION ${INSTALL_CMAKEDIR_ROOT}/${PROJECT_NAME} - ) - -install( - FILES - ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake + include(CMakePackageConfigHelpers) + write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake - DESTINATION - ${INSTALL_CMAKE_DIR} - ) + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion + ) + + configure_package_config_file( + ${PROJECT_SOURCE_DIR}/${PROJECT_NAME}Config.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake + INSTALL_DESTINATION ${INSTALL_CMAKEDIR_ROOT}/${PROJECT_NAME} + ) + + install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake + DESTINATION + ${INSTALL_CMAKE_DIR} + ) +endif() diff --git a/README.md b/README.md index b9dd8d9..1e930bf 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This is a C++ library for validating JSON documents based on a [draft-7 of JSON Schema Validation](http://json-schema.org/schema). First a disclaimer: *It is work in progress and -contributions or hints or discussions are welcome.* Even though a 2.0.0 release is imminent. +contributions or hints or discussions are welcome.* Niels Lohmann et al develop a great JSON parser for C++ called [JSON for Modern C++](https://github.com/nlohmann/json). This validator is based on this diff --git a/conanfile.py b/conanfile.py index d886875..6bec115 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,7 +15,6 @@ def get_version(): except: return None - class JsonSchemaValidatorConan(ConanFile): name = 'JsonSchemaValidator' version = get_version() @@ -24,34 +23,49 @@ class JsonSchemaValidatorConan(ConanFile): settings = 'os', 'compiler', 'build_type', 'arch' options = { 'shared': [True, False], - 'fPIC': [True, False] + 'fPIC': [True, False], + 'build_examples': [True, False], + 'build_tests': [True, False] } default_options = { 'shared': False, - 'fPIC': True + 'fPIC': True, + 'build_examples': True, + 'build_tests': False } - generators = "cmake" + generators = "CMakeDeps" exports_sources = [ 'CMakeLists.txt', 'nlohmann_json_schema_validatorConfig.cmake.in', 'src/*', 'app/*', + 'test/*', ] - requires = ( - 'nlohmann_json/3.7.3' + 'nlohmann_json/3.11.2' ) + _cmake = None + + def _configure_cmake(self): + if self._cmake: + return self._cmake + self._cmake = CMake(self) + self._cmake.definitions['JSON_VALIDATOR_BUILD_EXAMPLES'] = self.options.build_examples + self._cmake.definitions['JSON_VALIDATOR_BUILD_TESTS'] = self.options.build_tests + self._cmake.configure() + return self._cmake + + def layout(self): + build_type = str(self.settings.build_type).lower() + self.folders.build = "build-{}".format(build_type) def build(self): - cmake = CMake(self) - cmake.definitions['nlohmann_json_DIR'] = os.path.join(self.deps_cpp_info['nlohmann_json'].rootpath, 'include') - cmake.definitions['JSON_VALIDATOR_BUILD_EXAMPLES'] = True - cmake.definitions['JSON_VALIDATOR_BUILD_TESTS'] = False + cmake = self._configure_cmake() cmake.configure() cmake.build() def package(self): - cmake = CMake(self) + cmake = self._configure_cmake() cmake.install() def package_info(self): diff --git a/nlohmann_json_schema_validatorConfig.cmake.in b/nlohmann_json_schema_validatorConfig.cmake.in index c47fb16..88960cc 100644 --- a/nlohmann_json_schema_validatorConfig.cmake.in +++ b/nlohmann_json_schema_validatorConfig.cmake.in @@ -1,6 +1,7 @@ @PACKAGE_INIT@ -find_package(nlohmann_json 3.8.0 REQUIRED) +include(CMakeFindDependencyMacro) +find_dependency(nlohmann_json) include("${CMAKE_CURRENT_LIST_DIR}/nlohmann_json_schema_validatorTargets.cmake") check_required_components( diff --git a/src/json-patch.cpp b/src/json-patch.cpp index d0cd2e4..3203543 100644 --- a/src/json-patch.cpp +++ b/src/json-patch.cpp @@ -85,19 +85,19 @@ json_patch::json_patch(const json &patch) json_patch &json_patch::add(const json::json_pointer &ptr, json value) { - j_.push_back(json{{"op", "add"}, {"path", ptr}, {"value", std::move(value)}}); + j_.push_back(json{{"op", "add"}, {"path", ptr.to_string()}, {"value", std::move(value)}}); return *this; } json_patch &json_patch::replace(const json::json_pointer &ptr, json value) { - j_.push_back(json{{"op", "replace"}, {"path", ptr}, {"value", std::move(value)}}); + j_.push_back(json{{"op", "replace"}, {"path", ptr.to_string()}, {"value", std::move(value)}}); return *this; } json_patch &json_patch::remove(const json::json_pointer &ptr) { - j_.push_back(json{{"op", "remove"}, {"path", ptr}}); + j_.push_back(json{{"op", "remove"}, {"path", ptr.to_string()}}); return *this; } diff --git a/src/json-validator.cpp b/src/json-validator.cpp index d2e43e8..b279f23 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -14,6 +14,7 @@ #include #include #include +#include using nlohmann::json; using nlohmann::json_patch; @@ -40,6 +41,16 @@ protected: root_schema *root_; json default_value_ = nullptr; +protected: + virtual std::shared_ptr make_for_default_( + std::shared_ptr<::schema> & /* sch */, + root_schema * /* root */, + std::vector & /* uris */, + nlohmann::json & /* default_value */) const + { + return nullptr; + }; + public: virtual ~schema() = default; @@ -92,6 +103,21 @@ class schema_ref : public schema return default_value_; } +protected: + virtual std::shared_ptr make_for_default_( + std::shared_ptr<::schema> &sch, + root_schema *root, + std::vector &uris, + nlohmann::json &default_value) const override + { + // create a new reference schema using the original reference (which will be resolved later) + // to store this overloaded default value #209 + auto result = std::make_shared(uris[0].to_string(), root); + result->set_target(sch, true); + result->set_default_value(default_value); + return result; + }; + public: schema_ref(const std::string &id, root_schema *root) : schema(root), id_(id) {} @@ -179,7 +205,7 @@ public: auto fragment = new_uri.pointer(); // is there a reference looking for this unknown-keyword, which is thus no longer a unknown keyword but a schema - auto unresolved = file.unresolved.find(fragment); + auto unresolved = file.unresolved.find(fragment.to_string()); if (unresolved != file.unresolved.end()) schema::make(value, this, {}, {{new_uri}}); else { // no, nothing ref'd it, keep for later @@ -283,11 +309,31 @@ public: break; } while (1); - for (const auto &file : files_) - if (file.second.unresolved.size() != 0) + for (const auto &file : files_) { + if (file.second.unresolved.size() != 0) { + // Build a representation of the undefined + // references as a list of comma-separated strings. + auto n_urefs = file.second.unresolved.size(); + std::string urefs = "["; + + decltype(n_urefs) counter = 0; + for (const auto &p : file.second.unresolved) { + urefs += p.first; + + if (counter != n_urefs - 1u) { + urefs += ", "; + } + + ++counter; + } + + urefs += "]"; + throw std::invalid_argument("after all files have been parsed, '" + (file.first == "" ? "" : file.first) + - "' has still undefined references."); + "' has still the following undefined references: " + urefs); + } + } } void validate(const json::json_pointer &ptr, @@ -507,8 +553,23 @@ class type_schema : public schema else_->validate(ptr, instance, patch, e); } } + if (instance.is_null()) { + patch.add(nlohmann::json::json_pointer{}, default_value_); + } } +protected: + virtual std::shared_ptr make_for_default_( + std::shared_ptr<::schema> & /* sch */, + root_schema * /* root */, + std::vector & /* uris */, + nlohmann::json &default_value) const override + { + auto result = std::make_shared(*this); + result->set_default_value(default_value); + return result; + }; + public: type_schema(json &sch, root_schema *root, @@ -892,8 +953,8 @@ class boolean : public schema { if (!true_) { // false schema // empty array - //switch (instance.type()) { - //case json::value_t::array: + // switch (instance.type()) { + // case json::value_t::array: // if (instance.size() != 0) // valid false-schema // e.error(ptr, instance, "false-schema required empty array"); // return; @@ -1076,6 +1137,11 @@ public: propertyNames_ = schema::make(attr.value(), root, {"propertyNames"}, uris); sch.erase(attr); } + + attr = sch.find("default"); + if (attr != sch.end()) { + set_default_value(*attr); + } } }; @@ -1289,16 +1355,8 @@ std::shared_ptr schema::make(json &schema, attr = schema.find("default"); if (attr != schema.end()) { // copy the referenced schema depending on the underlying type and modify the default value - if (dynamic_cast(sch.get())) { - // create a new reference schema use the original reference (which will be resolved later) - // to store this overloaed default value #209 - auto overloaded_ref_sch = std::make_shared(uris[0].to_string(), root); - overloaded_ref_sch->set_target(sch, true); - overloaded_ref_sch->set_default_value(attr.value()); - sch = overloaded_ref_sch; - } else if (auto *type_sch = dynamic_cast(sch.get())) { - sch = std::make_shared(*type_sch); - sch->set_default_value(attr.value()); + if (auto new_sch = sch->make_for_default_(sch, root, uris, attr.value())) { + sch = new_sch; } schema.erase(attr); } diff --git a/src/nlohmann/json-schema.hpp b/src/nlohmann/json-schema.hpp index ebe7cef..07befd3 100644 --- a/src/nlohmann/json-schema.hpp +++ b/src/nlohmann/json-schema.hpp @@ -61,7 +61,7 @@ protected: std::tuple as_tuple() const { - return std::make_tuple(urn_, scheme_, authority_, path_, identifier_ != "" ? identifier_ : pointer_); + return std::make_tuple(urn_, scheme_, authority_, path_, identifier_ != "" ? identifier_ : pointer_.to_string()); } public: @@ -80,7 +80,7 @@ public: std::string fragment() const { if (identifier_ == "") - return pointer_; + return pointer_.to_string(); else return identifier_; } diff --git a/src/string-format-check.cpp b/src/string-format-check.cpp index 98954d9..bd8952f 100644 --- a/src/string-format-check.cpp +++ b/src/string-format-check.cpp @@ -1,5 +1,7 @@ #include +#include "smtp-address-validator.hpp" + #include #include #include @@ -84,10 +86,10 @@ void rfc3339_time_check(const std::string &value) } /** - * @todo Could be made more exact by querying a leap second database and choosing the - * correct maximum in {58,59,60}. This current solution might match some invalid dates - * but it won't lead to false negatives. This only works if we know the full date, however - */ + * @todo Could be made more exact by querying a leap second database and choosing the + * correct maximum in {58,59,60}. This current solution might match some invalid dates + * but it won't lead to false negatives. This only works if we know the full date, however + */ auto day_minutes = hour * 60 + minute - (offsetHour * 60 + offsetMinute); if (day_minutes < 0) @@ -126,7 +128,7 @@ void rfc3339_time_check(const std::string &value) * @endverbatim * NOTE: Per [ABNF] and ISO8601, the "T" and "Z" characters in this * syntax may alternatively be lower case "t" or "z" respectively. -*/ + */ void rfc3339_date_time_check(const std::string &value) { const static std::regex dateTimeRegex{R"(^([0-9]{4}\-[0-9]{2}\-[0-9]{2})[Tt]([0-9]{2}\:[0-9]{2}\:[0-9]{2}(?:\.[0-9]+)?(?:[Zz]|(?:\+|\-)[0-9]{2}\:[0-9]{2}))$)"}; @@ -180,91 +182,151 @@ const std::string uuid{R"([0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a- // from http://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address const std::string hostname{R"(^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$)"}; +bool is_ascii(std::string const& value) +{ + for (auto ch : value) { + if (ch & 0x80) { + return false; + } + } + return true; +} + /** - * @see https://tools.ietf.org/html/rfc5322#section-4.1 + * @see * * @verbatim - * atom = [CFWS] 1*atext [CFWS] - * word = atom / quoted-string - * phrase = 1*word / obs-phrase - * obs-FWS = 1*WSP *(CRLF 1*WSP) - * FWS = ([*WSP CRLF] 1*WSP) / obs-FWS - * ; Folding white space - * ctext = %d33-39 / ; Printable US-ASCII - * %d42-91 / ; characters not including - * %d93-126 / ; "(", ")", or "\" - * obs-ctext - * ccontent = ctext / quoted-pair / comment - * comment = "(" *([FWS] ccontent) [FWS] ")" - * CFWS = (1*([FWS] comment) [FWS]) / FWS - * obs-local-part = word *("." word) - * obs-domain = atom *("." atom) - * obs-dtext = obs-NO-WS-CTL / quoted-pair - * quoted-pair = ("\" (VCHAR / WSP)) / obs-qp - * obs-NO-WS-CTL = %d1-8 / ; US-ASCII control - * %d11 / ; characters that do not - * %d12 / ; include the carriage - * %d14-31 / ; return, line feed, and - * %d127 ; white space characters - * obs-ctext = obs-NO-WS-CTL - * obs-qtext = obs-NO-WS-CTL - * obs-utext = %d0 / obs-NO-WS-CTL / VCHAR - * obs-qp = "\" (%d0 / obs-NO-WS-CTL / LF / CR) - * obs-body = *((*LF *CR *((%d0 / text) *LF *CR)) / CRLF) - * obs-unstruct = *((*LF *CR *(obs-utext *LF *CR)) / FWS) - * obs-phrase = word *(word / "." / CFWS) - * obs-phrase-list = [phrase / CFWS] *("," [phrase / CFWS]) - * qtext = %d33 / ; Printable US-ASCII - * %d35-91 / ; characters not including - * %d93-126 / ; "\" or the quote character - * obs-qtext - * qcontent = qtext / quoted-pair - * quoted-string = [CFWS] - * DQUOTE *([FWS] qcontent) [FWS] DQUOTE - * [CFWS] - * atext = ALPHA / DIGIT / ; Printable US-ASCII - * "!" / "#" / ; characters not including - * "$" / "%" / ; specials. Used for atoms. - * "&" / "'" / - * "*" / "+" / - * "-" / "/" / - * "=" / "?" / - * "^" / "_" / - * "`" / "{" / - * "|" / "}" / - * "~" - * dot-atom-text = 1*atext *("." 1*atext) - * dot-atom = [CFWS] dot-atom-text [CFWS] - * addr-spec = local-part "@" domain - * local-part = dot-atom / quoted-string / obs-local-part - * domain = dot-atom / domain-literal / obs-domain - * domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS] - * dtext = %d33-90 / ; Printable US-ASCII - * %d94-126 / ; characters not including - * obs-dtext ; "[", "]", or "\" - * @endverbatim - * @todo Currently don't have a working tool for this larger ABNF to generate a regex. - * Other options: - * - https://github.com/ldthomas/apg-6.3 - * - https://github.com/akr/abnf + * URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] + * + * hier-part = "//" authority path-abempty + * / path-absolute + * / path-rootless + * / path-empty + * + * URI-reference = URI / relative-ref + * + * absolute-URI = scheme ":" hier-part [ "?" query ] + * + * relative-ref = relative-part [ "?" query ] [ "#" fragment ] + * + * relative-part = "//" authority path-abempty + * / path-absolute + * / path-noscheme + * / path-empty + * + * scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + * + * authority = [ userinfo "@" ] host [ ":" port ] + * userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) + * host = IP-literal / IPv4address / reg-name + * port = *DIGIT + * + * IP-literal = "[" ( IPv6address / IPvFuture ) "]" + * + * IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) + * + * IPv6address = 6( h16 ":" ) ls32 + * / "::" 5( h16 ":" ) ls32 + * / [ h16 ] "::" 4( h16 ":" ) ls32 + * / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + * / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + * / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + * / [ *4( h16 ":" ) h16 ] "::" ls32 + * / [ *5( h16 ":" ) h16 ] "::" h16 + * / [ *6( h16 ":" ) h16 ] "::" + * + * h16 = 1*4HEXDIG + * ls32 = ( h16 ":" h16 ) / IPv4address + * IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet + * dec-octet = DIGIT ; 0-9 + * / %x31-39 DIGIT ; 10-99 + * / "1" 2DIGIT ; 100-199 + * / "2" %x30-34 DIGIT ; 200-249 + * / "25" %x30-35 ; 250-255 + * + * reg-name = *( unreserved / pct-encoded / sub-delims ) + * + * path = path-abempty ; begins with "/" or is empty + * / path-absolute ; begins with "/" but not "//" + * / path-noscheme ; begins with a non-colon segment + * / path-rootless ; begins with a segment + * / path-empty ; zero characters + * + * path-abempty = *( "/" segment ) + * path-absolute = "/" [ segment-nz *( "/" segment ) ] + * path-noscheme = segment-nz-nc *( "/" segment ) + * path-rootless = segment-nz *( "/" segment ) + * path-empty = 0 + * + * segment = *pchar + * segment-nz = 1*pchar + * segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) + * ; non-zero-length segment without any colon ":" + * + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * + * query = *( pchar / "/" / "?" ) + * + * fragment = *( pchar / "/" / "?" ) + * + * pct-encoded = "%" HEXDIG HEXDIG + * + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * reserved = gen-delims / sub-delims + * gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + * + * @endverbatim + * @see adapted from: https://github.com/jhermsmeier/uri.regex/blob/master/uri.regex * - * The problematic thing are the allowed whitespaces (even newlines) in the email. - * Ignoring those and starting with - * @see https://stackoverflow.com/questions/13992403/regex-validation-of-email-addresses-according-to-rfc5321-rfc5322 - * and trying to divide up the complicated regex into understandable ABNF definitions from rfc5322 yields: */ -const std::string obsnowsctl{R"([\x01-\x08\x0b\x0c\x0e-\x1f\x7f])"}; -const std::string obsqp{R"(\\[\x01-\x09\x0b\x0c\x0e-\x7f])"}; -const std::string qtext{R"((?:[\x21\x23-\x5b\x5d-\x7e]|)" + obsnowsctl + ")"}; -const std::string dtext{R"([\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f])"}; -const std::string quotedString{R"("(?:)" + qtext + "|" + obsqp + R"()*")"}; -const std::string atext{R"([A-Za-z0-9!#$%&'*+/=?^_`{|}~-])"}; -const std::string domainLiteral{R"(\[(?:(?:)" + decOctet + R"()\.){3}(?:)" + decOctet + R"(|[A-Za-z0-9-]*[A-Za-z0-9]:(?:)" + dtext + "|" + obsqp + R"()+)\])"}; +void rfc3986_uri_check(const std::string &value) +{ + const static std::string scheme{R"(([A-Za-z][A-Za-z0-9+\-.]*):)"}; + const static std::string hierPart{ + R"((?:(\/\/)(?:((?:[A-Za-z0-9\-._~!$&'()*+,;=:]|)" + R"(%[0-9A-Fa-f]{2})*)@)?((?:\[(?:(?:(?:(?:[0-9A-Fa-f]{1,4}:){6}|)" + R"(::(?:[0-9A-Fa-f]{1,4}:){5}|)" + R"((?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}|)" + R"((?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}|)" + R"((?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}|)" + R"((?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:|)" + R"((?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::)(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|)" + R"((?:(?:25[0-5]|2[0-4][0-9]|)" + R"([01]?[0-9][0-9]?)\.){3}(?:25[0-5]|)" + R"(2[0-4][0-9]|)" + R"([01]?[0-9][0-9]?))|)" + R"((?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|)" + R"((?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)|)" + R"([Vv][0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&'()*+,;=:]+)\]|)" + R"((?:(?:25[0-5]|)" + R"(2[0-4][0-9]|)" + R"([01]?[0-9][0-9]?)\.){3}(?:25[0-5]|)" + R"(2[0-4][0-9]|)" + R"([01]?[0-9][0-9]?)|)" + R"((?:[A-Za-z0-9\-._~!$&'()*+,;=]|)" + R"(%[0-9A-Fa-f]{2})*))(?::([0-9]*))?((?:\/(?:[A-Za-z0-9\-._~!$&'()*+,;=:@]|)" + R"(%[0-9A-Fa-f]{2})*)*)|)" + R"(\/((?:(?:[A-Za-z0-9\-._~!$&'()*+,;=:@]|)" + R"(%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&'()*+,;=:@]|)" + R"(%[0-9A-Fa-f]{2})*)*)?)|)" + R"(((?:[A-Za-z0-9\-._~!$&'()*+,;=:@]|)" + R"(%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~!$&'()*+,;=:@]|)" + R"(%[0-9A-Fa-f]{2})*)*)|))"}; + + const static std::string query{R"((?:\?((?:[A-Za-z0-9\-._~!$&'()*+,;=:@\/?]|%[0-9A-Fa-f]{2})*))?)"}; + const static std::string fragment{ + R"((?:\#((?:[A-Za-z0-9\-._~!$&'()*+,;=:@\/?]|%[0-9A-Fa-f]{2})*))?)"}; + const static std::string uriFormat{scheme + hierPart + query + fragment}; + + const static std::regex uriRegex{uriFormat}; + + if (!std::regex_match(value, uriRegex)) { + throw std::invalid_argument(value + " is not a URI string according to RFC 3986."); + } +} -const std::string dotAtom{"(?:" + atext + R"(+(?:\.)" + atext + "+)*)"}; -const std::string stackoverflowMagicPart{R"((?:[[:alnum:]](?:[[:alnum:]-]*[[:alnum:]])?\.)+)" - R"([[:alnum:]](?:[[:alnum:]-]*[[:alnum:]])?)"}; -const std::string email{"(?:" + dotAtom + "|" + quotedString + ")@(?:" + stackoverflowMagicPart + "|" + domainLiteral + ")"}; } // namespace namespace nlohmann @@ -286,10 +348,18 @@ void default_string_format_check(const std::string &format, const std::string &v rfc3339_date_check(value); } else if (format == "time") { rfc3339_time_check(value); + } else if (format == "uri") { + rfc3986_uri_check(value); } else if (format == "email") { - static const std::regex emailRegex{email}; - if (!std::regex_match(value, emailRegex)) { - throw std::invalid_argument(value + " is not a valid email according to RFC 5322."); + if (!is_ascii(value)) { + throw std::invalid_argument(value + " contains non-ASCII values, not RFC 5321 compliant."); + } + if (!is_address(&*value.begin(), &*value.end())) { + throw std::invalid_argument(value + " is not a valid email according to RFC 5321."); + } + } else if (format == "idn-email") { + if (!is_address(&*value.begin(), &*value.end())) { + throw std::invalid_argument(value + " is not a valid idn-email according to RFC 6531."); } } else if (format == "hostname") { static const std::regex hostRegex{hostname}; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index fd1c309..9fa9c5b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -73,3 +73,7 @@ add_test(NAME issue-149-entry-selection COMMAND issue-149-entry-selection) add_executable(issue-189-default-values issue-189-default-values.cpp) target_link_libraries(issue-189-default-values nlohmann_json_schema_validator) add_test(NAME issue-189-default-values COMMAND issue-189-default-values) + +add_executable(issue-243-root-default-values issue-243-root-default-values.cpp) +target_link_libraries(issue-243-root-default-values nlohmann_json_schema_validator) +add_test(NAME issue-243-root-default-values COMMAND issue-243-root-default-values) diff --git a/test/JSON-Schema-Test-Suite/CMakeLists.txt b/test/JSON-Schema-Test-Suite/CMakeLists.txt index 237254e..3d93b55 100644 --- a/test/JSON-Schema-Test-Suite/CMakeLists.txt +++ b/test/JSON-Schema-Test-Suite/CMakeLists.txt @@ -54,7 +54,6 @@ if(JSON_SCHEMA_TEST_SUITE_PATH) JSON-Suite::Optional::float-overflow JSON-Suite::Optional::ecmascript-regex - JSON-Suite::Optional::Format::idn-email JSON-Suite::Optional::Format::idn-hostname JSON-Suite::Optional::Format::iri-reference JSON-Suite::Optional::Format::iri @@ -62,7 +61,6 @@ if(JSON_SCHEMA_TEST_SUITE_PATH) JSON-Suite::Optional::Format::relative-json-pointer JSON-Suite::Optional::Format::uri-reference JSON-Suite::Optional::Format::uri-template - JSON-Suite::Optional::Format::uri JSON-Suite::Optional::unicode PROPERTIES diff --git a/test/binary-validation.cpp b/test/binary-validation.cpp index 01392cf..4a9c7ea 100644 --- a/test/binary-validation.cpp +++ b/test/binary-validation.cpp @@ -163,7 +163,7 @@ int main() val.set_root_schema(array_of_types_without_binary); val.validate({{"something", binary}}, err); EXPECT_EQ(err.failed_pointers.size(), 1); - EXPECT_EQ(err.failed_pointers[0], "/something"); + EXPECT_EQ(err.failed_pointers[0].to_string(), "/something"); err.reset(); // check that without content callback you get exception with schema with contentEncoding or contentMeditType diff --git a/test/issue-98.cpp b/test/issue-98.cpp index 088b324..63a824c 100644 --- a/test/issue-98.cpp +++ b/test/issue-98.cpp @@ -8,7 +8,8 @@ int main(void) try { validator.set_root_schema(nlBase); // this line will log the caught exception } catch (const std::exception &e) { - if (std::string("after all files have been parsed, '' has still undefined references.") == e.what()) + + if (std::string("after all files have been parsed, '' has still the following undefined references: [/unknown/keywords]") == e.what()) return EXIT_SUCCESS; } return EXIT_FAILURE; diff --git a/test/string-format-check-test.cpp b/test/string-format-check-test.cpp index 2a5fb88..4a207b1 100644 --- a/test/string-format-check-test.cpp +++ b/test/string-format-check-test.cpp @@ -82,5 +82,21 @@ int main() numberOfErrors += testStringFormat("ipv4", ipv4Checks); + const std::vector> uriChecks{ + {"http://www.google.com/search?q=regular%20expression", true}, + {"http://www.google.com/", true}, + {"http://www.google.com/search?q=regular%20expression", true}, + {"www.google.com", false}, + {"http://www.google.comj", true}, + {"ldap://[2001:db8::7]/c=GB?objectClass?one", true}, + {"mailto:John.Doe@example.com", true}, + {"news:comp.infosystems.www.servers.unix", true}, + {"https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top", true}, + {"tel:+1-816-555-1212", true}, + {"telnet://192.0.2.16:80/", true}, + {"urn:oasis:names:specification:docbook:dtd:xml:4.1.2", true}}; + + numberOfErrors += testStringFormat("uri", uriChecks); + return numberOfErrors; } diff --git a/test/uri.cpp b/test/uri.cpp index 9083cd3..38c81ee 100644 --- a/test/uri.cpp +++ b/test/uri.cpp @@ -75,11 +75,11 @@ static void pointer_plain_name(json_uri start, a = a.derive("#foo/looks_like_json/poiner/but/isnt"); EXPECT_EQ(a, full + " # foo/looks_like_json/poiner/but/isnt"); EXPECT_EQ(a.identifier(), "foo/looks_like_json/poiner/but/isnt"); - EXPECT_EQ(a.pointer(), ""); + EXPECT_EQ(a.pointer().to_string(), ""); a = a.derive("#/looks_like_json/poiner/and/it/is"); EXPECT_EQ(a, full + " # /looks_like_json/poiner/and/it/is"); - EXPECT_EQ(a.pointer(), "/looks_like_json/poiner/and/it/is"); + EXPECT_EQ(a.pointer().to_string(), "/looks_like_json/poiner/and/it/is"); EXPECT_EQ(a.identifier(), ""); }